diff --git a/README b/README
index 2f989bf7..34a2037d 100644
--- a/README
+++ b/README
@@ -20,21 +20,49 @@ along with Transdroid. If not, see .
---
Some code/libraries are used in the project:
- Base16Encoder (Apache OpenJPA)
- Marc Prud'hommeaux (Apache License, Version 2.0)
- MultipartEntity (AOSP)
- Apache Software Foundation (Apache License, Version 2.0)
+ ActionBarSherlock
+ Jake Wharton
+ Apache License, Version 2.0
+ http://actionbarsherlock.com/
+ AndroidAnnotations
+ Pierre-Yves Ricau (eBusinessInformations) et al.
+ Apache License, Version 2.0
+ http://androidannotations.org/
+ Crouton
+ Code: Benjamin Weiss (Neofonie Mobile Gmbh) et al.
+ Idea: Cyril Mottier
+ Apache License, Version 2.0
+ https://github.com/keyboardsurfer/Crouton
+ BetterPickers
+ Derek Brameyer
+ Apache License, Version 2.0
+ https://github.com/derekbrameyer/android-betterpickers
+ Base16Encoder
+ Marc Prud'hommeaux
+ Apache OpenJPA
+ http://openjpa.apache.org/
+ MultipartEntity
+ Apache Software Foundation
+ Apache License, Version 2.0
http://source.android.com/
RssParser (learning-android)
- Tane Piper (Public Domain)
+ Tane Piper
+ Public Domain
http://github.com/digitalspaghetti/learning-android
Base64
- Robert Harder (Public Domain)
+ Robert Harder
+ Public Domain
http://iharder.net/base64
android-xmlrpc
- pskink et al. (Apache License, Version 2.0)
+ pskink et al.
+ Apache License, Version 2.0
http://code.google.com/p/android-xmlrpc/
android-ColorPickerPreference
- Daniel Nilsson and Sergey Margaritov (Apache License, Version 2.0)
+ Daniel Nilsson and Sergey Margaritov
+ Apache License, Version 2.0
https://github.com/attenzione/android-ColorPickerPreference
+ CheckableRelativeLayout
+ Cédric Caron (MarvinLabs)
+ Public Domain
+ http://www.marvinlabs.com/2010/10/custom-listview-ability-check-items/
diff --git a/android/.classpath b/android/.classpath
index 348383ad..d8c7af81 100644
--- a/android/.classpath
+++ b/android/.classpath
@@ -5,6 +5,6 @@
-
+
diff --git a/android/src/com/xirvik/transdroid/preferences/PreferencesXirvikServer.java b/android/src/com/xirvik/transdroid/preferences/PreferencesXirvikServer.java
index 0abd68a2..84e8f001 100644
--- a/android/src/com/xirvik/transdroid/preferences/PreferencesXirvikServer.java
+++ b/android/src/com/xirvik/transdroid/preferences/PreferencesXirvikServer.java
@@ -230,7 +230,7 @@ public class PreferencesXirvikServer extends PreferenceActivity {
String url = "https://" + serverValue + ":443/browsers_addons/transdroid_autoconf.txt";
HttpResponse request = httpclient.execute(new HttpGet(url));
InputStream stream = request.getEntity().getContent();
- String folderVal = HttpHelper.ConvertStreamToString(stream).trim();
+ String folderVal = HttpHelper.convertStreamToString(stream).trim();
if (folderVal.startsWith("
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/.factorypath b/core/.factorypath
new file mode 100644
index 00000000..d528d92e
--- /dev/null
+++ b/core/.factorypath
@@ -0,0 +1,3 @@
+
+
+
diff --git a/core/.project b/core/.project
new file mode 100644
index 00000000..1b341fee
--- /dev/null
+++ b/core/.project
@@ -0,0 +1,33 @@
+
+
+ Transdroid Core
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/core/.settings/org.eclipse.jdt.apt.core.prefs b/core/.settings/org.eclipse.jdt.apt.core.prefs
new file mode 100644
index 00000000..7d52ece5
--- /dev/null
+++ b/core/.settings/org.eclipse.jdt.apt.core.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.apt.aptEnabled=true
+org.eclipse.jdt.apt.genSrcDir=.apt_generated
+org.eclipse.jdt.apt.reconcileEnabled=true
diff --git a/core/.settings/org.eclipse.jdt.core.prefs b/core/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000..0b3561ab
--- /dev/null
+++ b/core/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.processAnnotations=enabled
diff --git a/core/AndroidManifest.xml b/core/AndroidManifest.xml
new file mode 100644
index 00000000..34f5d8e2
--- /dev/null
+++ b/core/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/build.xml b/core/build.xml
new file mode 100644
index 00000000..2371fdcc
--- /dev/null
+++ b/core/build.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/compile-libs/androidannotations-3.0-SNAPSHOT.jar b/core/compile-libs/androidannotations-3.0-SNAPSHOT.jar
new file mode 100644
index 00000000..310da002
Binary files /dev/null and b/core/compile-libs/androidannotations-3.0-SNAPSHOT.jar differ
diff --git a/core/custom_rules.xml b/core/custom_rules.xml
new file mode 100644
index 00000000..183179db
--- /dev/null
+++ b/core/custom_rules.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Instrumenting classes from ${out.absolute.dir}/classes...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Creating library output jar file...
+
+
+
+
+
+
+ Custom jar packaging exclusion: ${android.package.excludes}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/libs/androidannotations-api-3.0-SNAPSHOT.jar b/core/libs/androidannotations-api-3.0-SNAPSHOT.jar
new file mode 100644
index 00000000..bf0bfae5
Binary files /dev/null and b/core/libs/androidannotations-api-3.0-SNAPSHOT.jar differ
diff --git a/core/libs/ormlite-android-4.24.jar b/core/libs/ormlite-android-4.24.jar
new file mode 100644
index 00000000..d876135e
Binary files /dev/null and b/core/libs/ormlite-android-4.24.jar differ
diff --git a/core/libs/ormlite-core-4.24.jar b/core/libs/ormlite-core-4.24.jar
new file mode 100644
index 00000000..a9ab582b
Binary files /dev/null and b/core/libs/ormlite-core-4.24.jar differ
diff --git a/core/libs/transdroid-connect.jar b/core/libs/transdroid-connect.jar
new file mode 100644
index 00000000..4c18bffc
Binary files /dev/null and b/core/libs/transdroid-connect.jar differ
diff --git a/core/libs/universal-image-loader-1.8.4.jar b/core/libs/universal-image-loader-1.8.4.jar
new file mode 100644
index 00000000..78369c20
Binary files /dev/null and b/core/libs/universal-image-loader-1.8.4.jar differ
diff --git a/core/local.properties b/core/local.properties
new file mode 100644
index 00000000..47f704fe
--- /dev/null
+++ b/core/local.properties
@@ -0,0 +1,10 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+
+# location of the SDK. This is only used by Ant
+# For customization when using a Version Control System, please read the
+# header note.
+sdk.dir=/home/eric/Dev/android-sdk
diff --git a/core/proguard-project.txt b/core/proguard-project.txt
new file mode 100644
index 00000000..f2fe1559
--- /dev/null
+++ b/core/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/core/project.properties b/core/project.properties
new file mode 100644
index 00000000..0ca2ebd5
--- /dev/null
+++ b/core/project.properties
@@ -0,0 +1,18 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-18
+android.library.reference.1=../external/JakeWharton-ActionBarSherlock/library
+android.library.reference.2=../external/ColorPickerPreference
+android.library=true
+android.library.reference.3=../external/Crouton/library
diff --git a/core/res/drawable-hdpi-v11/ic_stat_notification.png b/core/res/drawable-hdpi-v11/ic_stat_notification.png
new file mode 100644
index 00000000..3d2ae715
Binary files /dev/null and b/core/res/drawable-hdpi-v11/ic_stat_notification.png differ
diff --git a/core/res/drawable-hdpi/ab_bottom_solid_transdroid.9.png b/core/res/drawable-hdpi/ab_bottom_solid_transdroid.9.png
new file mode 100644
index 00000000..64793735
Binary files /dev/null and b/core/res/drawable-hdpi/ab_bottom_solid_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/ab_bottom_solid_transdroid2.9.png b/core/res/drawable-hdpi/ab_bottom_solid_transdroid2.9.png
new file mode 100644
index 00000000..af25e462
Binary files /dev/null and b/core/res/drawable-hdpi/ab_bottom_solid_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/ab_solid_transdroid.9.png b/core/res/drawable-hdpi/ab_solid_transdroid.9.png
new file mode 100644
index 00000000..12ece624
Binary files /dev/null and b/core/res/drawable-hdpi/ab_solid_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/ab_solid_transdroid2.9.png b/core/res/drawable-hdpi/ab_solid_transdroid2.9.png
new file mode 100644
index 00000000..6344cbc2
Binary files /dev/null and b/core/res/drawable-hdpi/ab_solid_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/ab_stacked_solid_transdroid.9.png b/core/res/drawable-hdpi/ab_stacked_solid_transdroid.9.png
new file mode 100644
index 00000000..005445ac
Binary files /dev/null and b/core/res/drawable-hdpi/ab_stacked_solid_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/ab_stacked_solid_transdroid2.9.png b/core/res/drawable-hdpi/ab_stacked_solid_transdroid2.9.png
new file mode 100644
index 00000000..a48652d3
Binary files /dev/null and b/core/res/drawable-hdpi/ab_stacked_solid_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/ab_texture_tile_transdroid2.png b/core/res/drawable-hdpi/ab_texture_tile_transdroid2.png
new file mode 100644
index 00000000..87cc8211
Binary files /dev/null and b/core/res/drawable-hdpi/ab_texture_tile_transdroid2.png differ
diff --git a/core/res/drawable-hdpi/ab_transparent_transdroid.9.png b/core/res/drawable-hdpi/ab_transparent_transdroid.9.png
new file mode 100644
index 00000000..fc241b9f
Binary files /dev/null and b/core/res/drawable-hdpi/ab_transparent_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/ab_transparent_transdroid2.9.png b/core/res/drawable-hdpi/ab_transparent_transdroid2.9.png
new file mode 100644
index 00000000..e517f83b
Binary files /dev/null and b/core/res/drawable-hdpi/ab_transparent_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/btn_cab_done_default_transdroid2.9.png b/core/res/drawable-hdpi/btn_cab_done_default_transdroid2.9.png
new file mode 100644
index 00000000..fc6944ea
Binary files /dev/null and b/core/res/drawable-hdpi/btn_cab_done_default_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/btn_cab_done_focused_transdroid2.9.png b/core/res/drawable-hdpi/btn_cab_done_focused_transdroid2.9.png
new file mode 100644
index 00000000..72a72d8b
Binary files /dev/null and b/core/res/drawable-hdpi/btn_cab_done_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/btn_cab_done_pressed_transdroid2.9.png b/core/res/drawable-hdpi/btn_cab_done_pressed_transdroid2.9.png
new file mode 100644
index 00000000..8c69a5cf
Binary files /dev/null and b/core/res/drawable-hdpi/btn_cab_done_pressed_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/cab_background_bottom_transdroid2.9.png b/core/res/drawable-hdpi/cab_background_bottom_transdroid2.9.png
new file mode 100644
index 00000000..d61abca3
Binary files /dev/null and b/core/res/drawable-hdpi/cab_background_bottom_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/cab_background_top_transdroid2.9.png b/core/res/drawable-hdpi/cab_background_top_transdroid2.9.png
new file mode 100644
index 00000000..6bc2078c
Binary files /dev/null and b/core/res/drawable-hdpi/cab_background_top_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/ic_action_discard_dark.png b/core/res/drawable-hdpi/ic_action_discard_dark.png
new file mode 100644
index 00000000..ffd19d9e
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_discard_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_discard_light.png b/core/res/drawable-hdpi/ic_action_discard_light.png
new file mode 100644
index 00000000..e9ce89e0
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_discard_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_labels_dark.png b/core/res/drawable-hdpi/ic_action_labels_dark.png
new file mode 100644
index 00000000..432e7c00
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_labels_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_labels_light.png b/core/res/drawable-hdpi/ic_action_labels_light.png
new file mode 100644
index 00000000..9b093a67
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_labels_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_new_dark.png b/core/res/drawable-hdpi/ic_action_new_dark.png
new file mode 100644
index 00000000..ad8ada6b
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_new_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_new_light.png b/core/res/drawable-hdpi/ic_action_new_light.png
new file mode 100644
index 00000000..5741995c
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_new_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_pause_dark.png b/core/res/drawable-hdpi/ic_action_pause_dark.png
new file mode 100644
index 00000000..6b435bb0
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_pause_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_pause_light.png b/core/res/drawable-hdpi/ic_action_pause_light.png
new file mode 100644
index 00000000..9661cfbb
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_pause_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_priority_high_dark.png b/core/res/drawable-hdpi/ic_action_priority_high_dark.png
new file mode 100644
index 00000000..84478f76
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_priority_high_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_priority_high_light.png b/core/res/drawable-hdpi/ic_action_priority_high_light.png
new file mode 100644
index 00000000..4edf7a36
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_priority_high_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_priority_low_dark.png b/core/res/drawable-hdpi/ic_action_priority_low_dark.png
new file mode 100644
index 00000000..8802cad4
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_priority_low_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_priority_low_light.png b/core/res/drawable-hdpi/ic_action_priority_low_light.png
new file mode 100644
index 00000000..23ada0ca
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_priority_low_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_priority_normal_dark.png b/core/res/drawable-hdpi/ic_action_priority_normal_dark.png
new file mode 100644
index 00000000..7f7890f2
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_priority_normal_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_priority_normal_light.png b/core/res/drawable-hdpi/ic_action_priority_normal_light.png
new file mode 100644
index 00000000..0246a1b7
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_priority_normal_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_priority_off_dark.png b/core/res/drawable-hdpi/ic_action_priority_off_dark.png
new file mode 100644
index 00000000..ab37b556
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_priority_off_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_priority_off_light.png b/core/res/drawable-hdpi/ic_action_priority_off_light.png
new file mode 100644
index 00000000..7bf6101d
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_priority_off_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_refresh_dark.png b/core/res/drawable-hdpi/ic_action_refresh_dark.png
new file mode 100644
index 00000000..bb9d855f
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_refresh_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_refresh_light.png b/core/res/drawable-hdpi/ic_action_refresh_light.png
new file mode 100644
index 00000000..479aca46
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_refresh_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_remove_dark.png b/core/res/drawable-hdpi/ic_action_remove_dark.png
new file mode 100644
index 00000000..094eea58
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_remove_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_remove_light.png b/core/res/drawable-hdpi/ic_action_remove_light.png
new file mode 100644
index 00000000..cde36e1f
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_remove_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_resume_dark.png b/core/res/drawable-hdpi/ic_action_resume_dark.png
new file mode 100644
index 00000000..738aae1a
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_resume_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_resume_light.png b/core/res/drawable-hdpi/ic_action_resume_light.png
new file mode 100644
index 00000000..b4f692fc
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_resume_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_rss_dark.png b/core/res/drawable-hdpi/ic_action_rss_dark.png
new file mode 100644
index 00000000..02ec51ef
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_rss_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_rss_light.png b/core/res/drawable-hdpi/ic_action_rss_light.png
new file mode 100644
index 00000000..6aa10203
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_rss_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_search_dark.png b/core/res/drawable-hdpi/ic_action_search_dark.png
new file mode 100644
index 00000000..f12e005e
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_search_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_search_light.png b/core/res/drawable-hdpi/ic_action_search_light.png
new file mode 100644
index 00000000..e6b70451
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_search_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_sort_by_size_dark.png b/core/res/drawable-hdpi/ic_action_sort_by_size_dark.png
new file mode 100644
index 00000000..cbb5f451
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_sort_by_size_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_sort_by_size_light.png b/core/res/drawable-hdpi/ic_action_sort_by_size_light.png
new file mode 100644
index 00000000..3b34aaf8
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_sort_by_size_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_start_dark.png b/core/res/drawable-hdpi/ic_action_start_dark.png
new file mode 100644
index 00000000..df8a2ca2
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_start_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_start_light.png b/core/res/drawable-hdpi/ic_action_start_light.png
new file mode 100644
index 00000000..e70f0413
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_start_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_stop_dark.png b/core/res/drawable-hdpi/ic_action_stop_dark.png
new file mode 100644
index 00000000..dd5d6a1c
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_stop_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_stop_light.png b/core/res/drawable-hdpi/ic_action_stop_light.png
new file mode 100644
index 00000000..9c2f96da
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_stop_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_trackers_dark.png b/core/res/drawable-hdpi/ic_action_trackers_dark.png
new file mode 100644
index 00000000..97231b91
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_trackers_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_trackers_light.png b/core/res/drawable-hdpi/ic_action_trackers_light.png
new file mode 100644
index 00000000..89fa0cd0
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_trackers_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_turtle_disabled_light.png b/core/res/drawable-hdpi/ic_action_turtle_disabled_light.png
new file mode 100644
index 00000000..a99cd104
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_turtle_disabled_light.png differ
diff --git a/core/res/drawable-hdpi/ic_action_turtle_enabled.png b/core/res/drawable-hdpi/ic_action_turtle_enabled.png
new file mode 100644
index 00000000..1532eb39
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_turtle_enabled.png differ
diff --git a/core/res/drawable-hdpi/ic_action_website_dark.png b/core/res/drawable-hdpi/ic_action_website_dark.png
new file mode 100644
index 00000000..e154afdb
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_website_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_action_website_light.png b/core/res/drawable-hdpi/ic_action_website_light.png
new file mode 100644
index 00000000..6a2bc885
Binary files /dev/null and b/core/res/drawable-hdpi/ic_action_website_light.png differ
diff --git a/core/res/drawable-hdpi/ic_activity_torrents.png b/core/res/drawable-hdpi/ic_activity_torrents.png
new file mode 100644
index 00000000..63728265
Binary files /dev/null and b/core/res/drawable-hdpi/ic_activity_torrents.png differ
diff --git a/core/res/drawable-hdpi/ic_empty_details_dark.png b/core/res/drawable-hdpi/ic_empty_details_dark.png
new file mode 100644
index 00000000..718615e5
Binary files /dev/null and b/core/res/drawable-hdpi/ic_empty_details_dark.png differ
diff --git a/core/res/drawable-hdpi/ic_empty_details_light.png b/core/res/drawable-hdpi/ic_empty_details_light.png
new file mode 100644
index 00000000..6a1b5aa9
Binary files /dev/null and b/core/res/drawable-hdpi/ic_empty_details_light.png differ
diff --git a/core/res/drawable-hdpi/ic_launcher.png b/core/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 00000000..d6181101
Binary files /dev/null and b/core/res/drawable-hdpi/ic_launcher.png differ
diff --git a/core/res/drawable-hdpi/ic_stat_notification.png b/core/res/drawable-hdpi/ic_stat_notification.png
new file mode 100644
index 00000000..32ba0b73
Binary files /dev/null and b/core/res/drawable-hdpi/ic_stat_notification.png differ
diff --git a/core/res/drawable-hdpi/list_focused_transdroid.9.png b/core/res/drawable-hdpi/list_focused_transdroid.9.png
new file mode 100644
index 00000000..b342ccbb
Binary files /dev/null and b/core/res/drawable-hdpi/list_focused_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/list_focused_transdroid2.9.png b/core/res/drawable-hdpi/list_focused_transdroid2.9.png
new file mode 100644
index 00000000..411da949
Binary files /dev/null and b/core/res/drawable-hdpi/list_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/menu_dropdown_panel_transdroid.9.png b/core/res/drawable-hdpi/menu_dropdown_panel_transdroid.9.png
new file mode 100644
index 00000000..bab5e3ba
Binary files /dev/null and b/core/res/drawable-hdpi/menu_dropdown_panel_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/menu_dropdown_panel_transdroid2.9.png b/core/res/drawable-hdpi/menu_dropdown_panel_transdroid2.9.png
new file mode 100644
index 00000000..a1148598
Binary files /dev/null and b/core/res/drawable-hdpi/menu_dropdown_panel_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/menu_hardkey_panel_transdroid.9.png b/core/res/drawable-hdpi/menu_hardkey_panel_transdroid.9.png
new file mode 100644
index 00000000..40a8f3eb
Binary files /dev/null and b/core/res/drawable-hdpi/menu_hardkey_panel_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/progress_bg_transdroid.9.png b/core/res/drawable-hdpi/progress_bg_transdroid.9.png
new file mode 100644
index 00000000..3d5c707d
Binary files /dev/null and b/core/res/drawable-hdpi/progress_bg_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/progress_bg_transdroid2.9.png b/core/res/drawable-hdpi/progress_bg_transdroid2.9.png
new file mode 100644
index 00000000..3b183e07
Binary files /dev/null and b/core/res/drawable-hdpi/progress_bg_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/progress_primary_transdroid.9.png b/core/res/drawable-hdpi/progress_primary_transdroid.9.png
new file mode 100644
index 00000000..44a3c436
Binary files /dev/null and b/core/res/drawable-hdpi/progress_primary_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/progress_primary_transdroid2.9.png b/core/res/drawable-hdpi/progress_primary_transdroid2.9.png
new file mode 100644
index 00000000..8a6b5208
Binary files /dev/null and b/core/res/drawable-hdpi/progress_primary_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/progress_secondary_transdroid.9.png b/core/res/drawable-hdpi/progress_secondary_transdroid.9.png
new file mode 100644
index 00000000..32196b2b
Binary files /dev/null and b/core/res/drawable-hdpi/progress_secondary_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/progress_secondary_transdroid2.9.png b/core/res/drawable-hdpi/progress_secondary_transdroid2.9.png
new file mode 100644
index 00000000..34d2c6ea
Binary files /dev/null and b/core/res/drawable-hdpi/progress_secondary_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/spinner_ab_default_transdroid.9.png b/core/res/drawable-hdpi/spinner_ab_default_transdroid.9.png
new file mode 100644
index 00000000..4fd4aeba
Binary files /dev/null and b/core/res/drawable-hdpi/spinner_ab_default_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/spinner_ab_default_transdroid2.9.png b/core/res/drawable-hdpi/spinner_ab_default_transdroid2.9.png
new file mode 100644
index 00000000..e518eb7d
Binary files /dev/null and b/core/res/drawable-hdpi/spinner_ab_default_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/spinner_ab_disabled_transdroid.9.png b/core/res/drawable-hdpi/spinner_ab_disabled_transdroid.9.png
new file mode 100644
index 00000000..d42c97b8
Binary files /dev/null and b/core/res/drawable-hdpi/spinner_ab_disabled_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/spinner_ab_disabled_transdroid2.9.png b/core/res/drawable-hdpi/spinner_ab_disabled_transdroid2.9.png
new file mode 100644
index 00000000..b6febf96
Binary files /dev/null and b/core/res/drawable-hdpi/spinner_ab_disabled_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/spinner_ab_focused_transdroid.9.png b/core/res/drawable-hdpi/spinner_ab_focused_transdroid.9.png
new file mode 100644
index 00000000..4761bd26
Binary files /dev/null and b/core/res/drawable-hdpi/spinner_ab_focused_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/spinner_ab_focused_transdroid2.9.png b/core/res/drawable-hdpi/spinner_ab_focused_transdroid2.9.png
new file mode 100644
index 00000000..544ae6ae
Binary files /dev/null and b/core/res/drawable-hdpi/spinner_ab_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/spinner_ab_pressed_transdroid.9.png b/core/res/drawable-hdpi/spinner_ab_pressed_transdroid.9.png
new file mode 100644
index 00000000..bddc913e
Binary files /dev/null and b/core/res/drawable-hdpi/spinner_ab_pressed_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/spinner_ab_pressed_transdroid2.9.png b/core/res/drawable-hdpi/spinner_ab_pressed_transdroid2.9.png
new file mode 100644
index 00000000..7d207c52
Binary files /dev/null and b/core/res/drawable-hdpi/spinner_ab_pressed_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/tab_selected_focused_transdroid.9.png b/core/res/drawable-hdpi/tab_selected_focused_transdroid.9.png
new file mode 100644
index 00000000..36fec49a
Binary files /dev/null and b/core/res/drawable-hdpi/tab_selected_focused_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/tab_selected_focused_transdroid2.9.png b/core/res/drawable-hdpi/tab_selected_focused_transdroid2.9.png
new file mode 100644
index 00000000..15e12068
Binary files /dev/null and b/core/res/drawable-hdpi/tab_selected_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/tab_selected_pressed_transdroid.9.png b/core/res/drawable-hdpi/tab_selected_pressed_transdroid.9.png
new file mode 100644
index 00000000..aa07ad6f
Binary files /dev/null and b/core/res/drawable-hdpi/tab_selected_pressed_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/tab_selected_pressed_transdroid2.9.png b/core/res/drawable-hdpi/tab_selected_pressed_transdroid2.9.png
new file mode 100644
index 00000000..cca1aeb2
Binary files /dev/null and b/core/res/drawable-hdpi/tab_selected_pressed_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/tab_selected_transdroid.9.png b/core/res/drawable-hdpi/tab_selected_transdroid.9.png
new file mode 100644
index 00000000..d59bde58
Binary files /dev/null and b/core/res/drawable-hdpi/tab_selected_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/tab_selected_transdroid2.9.png b/core/res/drawable-hdpi/tab_selected_transdroid2.9.png
new file mode 100644
index 00000000..ca828066
Binary files /dev/null and b/core/res/drawable-hdpi/tab_selected_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/tab_unselected_focused_transdroid.9.png b/core/res/drawable-hdpi/tab_unselected_focused_transdroid.9.png
new file mode 100644
index 00000000..40613b47
Binary files /dev/null and b/core/res/drawable-hdpi/tab_unselected_focused_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/tab_unselected_focused_transdroid2.9.png b/core/res/drawable-hdpi/tab_unselected_focused_transdroid2.9.png
new file mode 100644
index 00000000..d8ed7709
Binary files /dev/null and b/core/res/drawable-hdpi/tab_unselected_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/tab_unselected_pressed_transdroid.9.png b/core/res/drawable-hdpi/tab_unselected_pressed_transdroid.9.png
new file mode 100644
index 00000000..ef83039c
Binary files /dev/null and b/core/res/drawable-hdpi/tab_unselected_pressed_transdroid.9.png differ
diff --git a/core/res/drawable-hdpi/tab_unselected_pressed_transdroid2.9.png b/core/res/drawable-hdpi/tab_unselected_pressed_transdroid2.9.png
new file mode 100644
index 00000000..bd9026bc
Binary files /dev/null and b/core/res/drawable-hdpi/tab_unselected_pressed_transdroid2.9.png differ
diff --git a/core/res/drawable-hdpi/tab_unselected_transdroid2.9.png b/core/res/drawable-hdpi/tab_unselected_transdroid2.9.png
new file mode 100644
index 00000000..c442c393
Binary files /dev/null and b/core/res/drawable-hdpi/tab_unselected_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi-v11/ic_stat_notification.png b/core/res/drawable-mdpi-v11/ic_stat_notification.png
new file mode 100644
index 00000000..a598474c
Binary files /dev/null and b/core/res/drawable-mdpi-v11/ic_stat_notification.png differ
diff --git a/core/res/drawable-mdpi/ab_bottom_solid_transdroid.9.png b/core/res/drawable-mdpi/ab_bottom_solid_transdroid.9.png
new file mode 100644
index 00000000..135904dc
Binary files /dev/null and b/core/res/drawable-mdpi/ab_bottom_solid_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/ab_bottom_solid_transdroid2.9.png b/core/res/drawable-mdpi/ab_bottom_solid_transdroid2.9.png
new file mode 100644
index 00000000..77ef47fe
Binary files /dev/null and b/core/res/drawable-mdpi/ab_bottom_solid_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/ab_solid_transdroid.9.png b/core/res/drawable-mdpi/ab_solid_transdroid.9.png
new file mode 100644
index 00000000..fa3d4284
Binary files /dev/null and b/core/res/drawable-mdpi/ab_solid_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/ab_solid_transdroid2.9.png b/core/res/drawable-mdpi/ab_solid_transdroid2.9.png
new file mode 100644
index 00000000..9b1e02aa
Binary files /dev/null and b/core/res/drawable-mdpi/ab_solid_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/ab_stacked_solid_transdroid.9.png b/core/res/drawable-mdpi/ab_stacked_solid_transdroid.9.png
new file mode 100644
index 00000000..fd09cb18
Binary files /dev/null and b/core/res/drawable-mdpi/ab_stacked_solid_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/ab_stacked_solid_transdroid2.9.png b/core/res/drawable-mdpi/ab_stacked_solid_transdroid2.9.png
new file mode 100644
index 00000000..3c78f52c
Binary files /dev/null and b/core/res/drawable-mdpi/ab_stacked_solid_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/ab_texture_tile_transdroid2.png b/core/res/drawable-mdpi/ab_texture_tile_transdroid2.png
new file mode 100644
index 00000000..87cc8211
Binary files /dev/null and b/core/res/drawable-mdpi/ab_texture_tile_transdroid2.png differ
diff --git a/core/res/drawable-mdpi/ab_transparent_transdroid.9.png b/core/res/drawable-mdpi/ab_transparent_transdroid.9.png
new file mode 100644
index 00000000..8c3b514f
Binary files /dev/null and b/core/res/drawable-mdpi/ab_transparent_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/ab_transparent_transdroid2.9.png b/core/res/drawable-mdpi/ab_transparent_transdroid2.9.png
new file mode 100644
index 00000000..f24375ca
Binary files /dev/null and b/core/res/drawable-mdpi/ab_transparent_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/btn_cab_done_default_transdroid2.9.png b/core/res/drawable-mdpi/btn_cab_done_default_transdroid2.9.png
new file mode 100644
index 00000000..da14cdbc
Binary files /dev/null and b/core/res/drawable-mdpi/btn_cab_done_default_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/btn_cab_done_focused_transdroid2.9.png b/core/res/drawable-mdpi/btn_cab_done_focused_transdroid2.9.png
new file mode 100644
index 00000000..89b06832
Binary files /dev/null and b/core/res/drawable-mdpi/btn_cab_done_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/btn_cab_done_pressed_transdroid2.9.png b/core/res/drawable-mdpi/btn_cab_done_pressed_transdroid2.9.png
new file mode 100644
index 00000000..767cf60e
Binary files /dev/null and b/core/res/drawable-mdpi/btn_cab_done_pressed_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/cab_background_bottom_transdroid2.9.png b/core/res/drawable-mdpi/cab_background_bottom_transdroid2.9.png
new file mode 100644
index 00000000..df024689
Binary files /dev/null and b/core/res/drawable-mdpi/cab_background_bottom_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/cab_background_top_transdroid2.9.png b/core/res/drawable-mdpi/cab_background_top_transdroid2.9.png
new file mode 100644
index 00000000..02b6f3b6
Binary files /dev/null and b/core/res/drawable-mdpi/cab_background_top_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/ic_action_discard.png b/core/res/drawable-mdpi/ic_action_discard.png
new file mode 100644
index 00000000..a8ee5f25
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_discard.png differ
diff --git a/core/res/drawable-mdpi/ic_action_discard_light.png b/core/res/drawable-mdpi/ic_action_discard_light.png
new file mode 100644
index 00000000..cedb1085
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_discard_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_labels_dark.png b/core/res/drawable-mdpi/ic_action_labels_dark.png
new file mode 100644
index 00000000..b85d7c58
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_labels_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_labels_light.png b/core/res/drawable-mdpi/ic_action_labels_light.png
new file mode 100644
index 00000000..8567d5e4
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_labels_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_new_dark.png b/core/res/drawable-mdpi/ic_action_new_dark.png
new file mode 100644
index 00000000..4d5d484b
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_new_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_new_light.png b/core/res/drawable-mdpi/ic_action_new_light.png
new file mode 100644
index 00000000..884c9d27
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_new_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_pause_dark.png b/core/res/drawable-mdpi/ic_action_pause_dark.png
new file mode 100644
index 00000000..a5aee6f2
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_pause_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_pause_light.png b/core/res/drawable-mdpi/ic_action_pause_light.png
new file mode 100644
index 00000000..01858e34
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_pause_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_priority_high_dark.png b/core/res/drawable-mdpi/ic_action_priority_high_dark.png
new file mode 100644
index 00000000..9cfeeebc
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_priority_high_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_priority_high_light.png b/core/res/drawable-mdpi/ic_action_priority_high_light.png
new file mode 100644
index 00000000..bef007c6
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_priority_high_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_priority_low_dark.png b/core/res/drawable-mdpi/ic_action_priority_low_dark.png
new file mode 100644
index 00000000..89927f63
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_priority_low_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_priority_low_light.png b/core/res/drawable-mdpi/ic_action_priority_low_light.png
new file mode 100644
index 00000000..44e768fa
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_priority_low_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_priority_normal_dark.png b/core/res/drawable-mdpi/ic_action_priority_normal_dark.png
new file mode 100644
index 00000000..7dd2c7bd
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_priority_normal_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_priority_normal_light.png b/core/res/drawable-mdpi/ic_action_priority_normal_light.png
new file mode 100644
index 00000000..4a471f36
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_priority_normal_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_priority_off_dark.png b/core/res/drawable-mdpi/ic_action_priority_off_dark.png
new file mode 100644
index 00000000..8d9c72d5
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_priority_off_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_priority_off_light.png b/core/res/drawable-mdpi/ic_action_priority_off_light.png
new file mode 100644
index 00000000..d7766bc2
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_priority_off_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_refresh_dark.png b/core/res/drawable-mdpi/ic_action_refresh_dark.png
new file mode 100644
index 00000000..bd611e8e
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_refresh_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_refresh_light.png b/core/res/drawable-mdpi/ic_action_refresh_light.png
new file mode 100644
index 00000000..63e70e17
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_refresh_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_remove_dark.png b/core/res/drawable-mdpi/ic_action_remove_dark.png
new file mode 100644
index 00000000..3336760d
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_remove_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_remove_light.png b/core/res/drawable-mdpi/ic_action_remove_light.png
new file mode 100644
index 00000000..9f4c3d6a
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_remove_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_resume_dark.png b/core/res/drawable-mdpi/ic_action_resume_dark.png
new file mode 100644
index 00000000..28e81379
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_resume_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_resume_light.png b/core/res/drawable-mdpi/ic_action_resume_light.png
new file mode 100644
index 00000000..937e0299
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_resume_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_rss_dark.png b/core/res/drawable-mdpi/ic_action_rss_dark.png
new file mode 100644
index 00000000..2de867b8
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_rss_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_rss_light.png b/core/res/drawable-mdpi/ic_action_rss_light.png
new file mode 100644
index 00000000..2c5d0933
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_rss_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_search_dark.png b/core/res/drawable-mdpi/ic_action_search_dark.png
new file mode 100644
index 00000000..587d9e0b
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_search_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_search_light.png b/core/res/drawable-mdpi/ic_action_search_light.png
new file mode 100644
index 00000000..3aa64404
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_search_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_sort_by_size_dark.png b/core/res/drawable-mdpi/ic_action_sort_by_size_dark.png
new file mode 100644
index 00000000..aa921e76
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_sort_by_size_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_sort_by_size_light.png b/core/res/drawable-mdpi/ic_action_sort_by_size_light.png
new file mode 100644
index 00000000..af004e5f
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_sort_by_size_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_start_dark.png b/core/res/drawable-mdpi/ic_action_start_dark.png
new file mode 100644
index 00000000..6a40cd5f
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_start_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_start_light.png b/core/res/drawable-mdpi/ic_action_start_light.png
new file mode 100644
index 00000000..1e3bc97a
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_start_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_stop_dark.png b/core/res/drawable-mdpi/ic_action_stop_dark.png
new file mode 100644
index 00000000..20df4158
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_stop_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_stop_light.png b/core/res/drawable-mdpi/ic_action_stop_light.png
new file mode 100644
index 00000000..0c1a0a97
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_stop_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_trackers_dark.png b/core/res/drawable-mdpi/ic_action_trackers_dark.png
new file mode 100644
index 00000000..539bad7d
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_trackers_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_trackers_light.png b/core/res/drawable-mdpi/ic_action_trackers_light.png
new file mode 100644
index 00000000..f9c51464
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_trackers_light.png differ
diff --git a/core/res/drawable-mdpi/ic_action_turtle_disabled.png b/core/res/drawable-mdpi/ic_action_turtle_disabled.png
new file mode 100644
index 00000000..70155517
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_turtle_disabled.png differ
diff --git a/core/res/drawable-mdpi/ic_action_turtle_enabled.png b/core/res/drawable-mdpi/ic_action_turtle_enabled.png
new file mode 100644
index 00000000..fedbfc9b
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_turtle_enabled.png differ
diff --git a/core/res/drawable-mdpi/ic_action_website_dark.png b/core/res/drawable-mdpi/ic_action_website_dark.png
new file mode 100644
index 00000000..41b56ec9
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_website_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_action_website_light.png b/core/res/drawable-mdpi/ic_action_website_light.png
new file mode 100644
index 00000000..f146cf99
Binary files /dev/null and b/core/res/drawable-mdpi/ic_action_website_light.png differ
diff --git a/core/res/drawable-mdpi/ic_activity_torrents.png b/core/res/drawable-mdpi/ic_activity_torrents.png
new file mode 100644
index 00000000..ce606a71
Binary files /dev/null and b/core/res/drawable-mdpi/ic_activity_torrents.png differ
diff --git a/core/res/drawable-mdpi/ic_empty_details_dark.png b/core/res/drawable-mdpi/ic_empty_details_dark.png
new file mode 100644
index 00000000..5bd58335
Binary files /dev/null and b/core/res/drawable-mdpi/ic_empty_details_dark.png differ
diff --git a/core/res/drawable-mdpi/ic_empty_details_light.png b/core/res/drawable-mdpi/ic_empty_details_light.png
new file mode 100644
index 00000000..92ed5fa0
Binary files /dev/null and b/core/res/drawable-mdpi/ic_empty_details_light.png differ
diff --git a/core/res/drawable-mdpi/ic_launcher.png b/core/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 00000000..c153fddf
Binary files /dev/null and b/core/res/drawable-mdpi/ic_launcher.png differ
diff --git a/core/res/drawable-mdpi/ic_stat_notification.png b/core/res/drawable-mdpi/ic_stat_notification.png
new file mode 100644
index 00000000..ddbf26ce
Binary files /dev/null and b/core/res/drawable-mdpi/ic_stat_notification.png differ
diff --git a/core/res/drawable-mdpi/list_focused_transdroid.9.png b/core/res/drawable-mdpi/list_focused_transdroid.9.png
new file mode 100644
index 00000000..cc6c75f9
Binary files /dev/null and b/core/res/drawable-mdpi/list_focused_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/list_focused_transdroid2.9.png b/core/res/drawable-mdpi/list_focused_transdroid2.9.png
new file mode 100644
index 00000000..469e1e0e
Binary files /dev/null and b/core/res/drawable-mdpi/list_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/menu_dropdown_panel_transdroid.9.png b/core/res/drawable-mdpi/menu_dropdown_panel_transdroid.9.png
new file mode 100644
index 00000000..2b2458ef
Binary files /dev/null and b/core/res/drawable-mdpi/menu_dropdown_panel_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/menu_dropdown_panel_transdroid2.9.png b/core/res/drawable-mdpi/menu_dropdown_panel_transdroid2.9.png
new file mode 100644
index 00000000..ea341b50
Binary files /dev/null and b/core/res/drawable-mdpi/menu_dropdown_panel_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/menu_hardkey_panel_transdroid.9.png b/core/res/drawable-mdpi/menu_hardkey_panel_transdroid.9.png
new file mode 100644
index 00000000..19b2feb5
Binary files /dev/null and b/core/res/drawable-mdpi/menu_hardkey_panel_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/progress_bg_transdroid.9.png b/core/res/drawable-mdpi/progress_bg_transdroid.9.png
new file mode 100644
index 00000000..9372a60f
Binary files /dev/null and b/core/res/drawable-mdpi/progress_bg_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/progress_bg_transdroid2.9.png b/core/res/drawable-mdpi/progress_bg_transdroid2.9.png
new file mode 100644
index 00000000..71753a4b
Binary files /dev/null and b/core/res/drawable-mdpi/progress_bg_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/progress_primary_transdroid.9.png b/core/res/drawable-mdpi/progress_primary_transdroid.9.png
new file mode 100644
index 00000000..27192ef1
Binary files /dev/null and b/core/res/drawable-mdpi/progress_primary_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/progress_primary_transdroid2.9.png b/core/res/drawable-mdpi/progress_primary_transdroid2.9.png
new file mode 100644
index 00000000..2bbbbb9a
Binary files /dev/null and b/core/res/drawable-mdpi/progress_primary_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/progress_secondary_transdroid.9.png b/core/res/drawable-mdpi/progress_secondary_transdroid.9.png
new file mode 100644
index 00000000..2b240eae
Binary files /dev/null and b/core/res/drawable-mdpi/progress_secondary_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/progress_secondary_transdroid2.9.png b/core/res/drawable-mdpi/progress_secondary_transdroid2.9.png
new file mode 100644
index 00000000..9dd0f6cb
Binary files /dev/null and b/core/res/drawable-mdpi/progress_secondary_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/spinner_ab_default_transdroid.9.png b/core/res/drawable-mdpi/spinner_ab_default_transdroid.9.png
new file mode 100644
index 00000000..9aeafee2
Binary files /dev/null and b/core/res/drawable-mdpi/spinner_ab_default_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/spinner_ab_default_transdroid2.9.png b/core/res/drawable-mdpi/spinner_ab_default_transdroid2.9.png
new file mode 100644
index 00000000..5e1dd470
Binary files /dev/null and b/core/res/drawable-mdpi/spinner_ab_default_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/spinner_ab_disabled_transdroid.9.png b/core/res/drawable-mdpi/spinner_ab_disabled_transdroid.9.png
new file mode 100644
index 00000000..88dd4415
Binary files /dev/null and b/core/res/drawable-mdpi/spinner_ab_disabled_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/spinner_ab_disabled_transdroid2.9.png b/core/res/drawable-mdpi/spinner_ab_disabled_transdroid2.9.png
new file mode 100644
index 00000000..38025ad3
Binary files /dev/null and b/core/res/drawable-mdpi/spinner_ab_disabled_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/spinner_ab_focused_transdroid.9.png b/core/res/drawable-mdpi/spinner_ab_focused_transdroid.9.png
new file mode 100644
index 00000000..63a0fda5
Binary files /dev/null and b/core/res/drawable-mdpi/spinner_ab_focused_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/spinner_ab_focused_transdroid2.9.png b/core/res/drawable-mdpi/spinner_ab_focused_transdroid2.9.png
new file mode 100644
index 00000000..e8c8b0f7
Binary files /dev/null and b/core/res/drawable-mdpi/spinner_ab_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/spinner_ab_pressed_transdroid.9.png b/core/res/drawable-mdpi/spinner_ab_pressed_transdroid.9.png
new file mode 100644
index 00000000..15652b11
Binary files /dev/null and b/core/res/drawable-mdpi/spinner_ab_pressed_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/spinner_ab_pressed_transdroid2.9.png b/core/res/drawable-mdpi/spinner_ab_pressed_transdroid2.9.png
new file mode 100644
index 00000000..c9d4e8b6
Binary files /dev/null and b/core/res/drawable-mdpi/spinner_ab_pressed_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/tab_selected_focused_transdroid.9.png b/core/res/drawable-mdpi/tab_selected_focused_transdroid.9.png
new file mode 100644
index 00000000..ac6ffee0
Binary files /dev/null and b/core/res/drawable-mdpi/tab_selected_focused_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/tab_selected_focused_transdroid2.9.png b/core/res/drawable-mdpi/tab_selected_focused_transdroid2.9.png
new file mode 100644
index 00000000..87a5b7c6
Binary files /dev/null and b/core/res/drawable-mdpi/tab_selected_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/tab_selected_pressed_transdroid.9.png b/core/res/drawable-mdpi/tab_selected_pressed_transdroid.9.png
new file mode 100644
index 00000000..690f41de
Binary files /dev/null and b/core/res/drawable-mdpi/tab_selected_pressed_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/tab_selected_pressed_transdroid2.9.png b/core/res/drawable-mdpi/tab_selected_pressed_transdroid2.9.png
new file mode 100644
index 00000000..5cfe08f0
Binary files /dev/null and b/core/res/drawable-mdpi/tab_selected_pressed_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/tab_selected_transdroid.9.png b/core/res/drawable-mdpi/tab_selected_transdroid.9.png
new file mode 100644
index 00000000..1aa38d48
Binary files /dev/null and b/core/res/drawable-mdpi/tab_selected_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/tab_selected_transdroid2.9.png b/core/res/drawable-mdpi/tab_selected_transdroid2.9.png
new file mode 100644
index 00000000..79e55713
Binary files /dev/null and b/core/res/drawable-mdpi/tab_selected_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/tab_unselected_focused_transdroid.9.png b/core/res/drawable-mdpi/tab_unselected_focused_transdroid.9.png
new file mode 100644
index 00000000..f9c9c6bd
Binary files /dev/null and b/core/res/drawable-mdpi/tab_unselected_focused_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/tab_unselected_focused_transdroid2.9.png b/core/res/drawable-mdpi/tab_unselected_focused_transdroid2.9.png
new file mode 100644
index 00000000..1a55fa4b
Binary files /dev/null and b/core/res/drawable-mdpi/tab_unselected_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/tab_unselected_pressed_transdroid.9.png b/core/res/drawable-mdpi/tab_unselected_pressed_transdroid.9.png
new file mode 100644
index 00000000..2acd0bbe
Binary files /dev/null and b/core/res/drawable-mdpi/tab_unselected_pressed_transdroid.9.png differ
diff --git a/core/res/drawable-mdpi/tab_unselected_pressed_transdroid2.9.png b/core/res/drawable-mdpi/tab_unselected_pressed_transdroid2.9.png
new file mode 100644
index 00000000..57a75c8c
Binary files /dev/null and b/core/res/drawable-mdpi/tab_unselected_pressed_transdroid2.9.png differ
diff --git a/core/res/drawable-mdpi/tab_unselected_transdroid2.9.png b/core/res/drawable-mdpi/tab_unselected_transdroid2.9.png
new file mode 100644
index 00000000..7f723870
Binary files /dev/null and b/core/res/drawable-mdpi/tab_unselected_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi-v11/ic_stat_notification.png b/core/res/drawable-xhdpi-v11/ic_stat_notification.png
new file mode 100644
index 00000000..721ed41b
Binary files /dev/null and b/core/res/drawable-xhdpi-v11/ic_stat_notification.png differ
diff --git a/core/res/drawable-xhdpi/ab_bottom_solid_transdroid.9.png b/core/res/drawable-xhdpi/ab_bottom_solid_transdroid.9.png
new file mode 100644
index 00000000..abf5694b
Binary files /dev/null and b/core/res/drawable-xhdpi/ab_bottom_solid_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/ab_bottom_solid_transdroid2.9.png b/core/res/drawable-xhdpi/ab_bottom_solid_transdroid2.9.png
new file mode 100644
index 00000000..b1a11071
Binary files /dev/null and b/core/res/drawable-xhdpi/ab_bottom_solid_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/ab_solid_transdroid.9.png b/core/res/drawable-xhdpi/ab_solid_transdroid.9.png
new file mode 100644
index 00000000..143a16f4
Binary files /dev/null and b/core/res/drawable-xhdpi/ab_solid_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/ab_solid_transdroid2.9.png b/core/res/drawable-xhdpi/ab_solid_transdroid2.9.png
new file mode 100644
index 00000000..550ceee8
Binary files /dev/null and b/core/res/drawable-xhdpi/ab_solid_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/ab_stacked_solid_transdroid.9.png b/core/res/drawable-xhdpi/ab_stacked_solid_transdroid.9.png
new file mode 100644
index 00000000..f9a1e1f2
Binary files /dev/null and b/core/res/drawable-xhdpi/ab_stacked_solid_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/ab_stacked_solid_transdroid2.9.png b/core/res/drawable-xhdpi/ab_stacked_solid_transdroid2.9.png
new file mode 100644
index 00000000..61130d91
Binary files /dev/null and b/core/res/drawable-xhdpi/ab_stacked_solid_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/ab_texture_tile_transdroid2.png b/core/res/drawable-xhdpi/ab_texture_tile_transdroid2.png
new file mode 100644
index 00000000..4cd50792
Binary files /dev/null and b/core/res/drawable-xhdpi/ab_texture_tile_transdroid2.png differ
diff --git a/core/res/drawable-xhdpi/ab_transparent_transdroid.9.png b/core/res/drawable-xhdpi/ab_transparent_transdroid.9.png
new file mode 100644
index 00000000..24020e23
Binary files /dev/null and b/core/res/drawable-xhdpi/ab_transparent_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/ab_transparent_transdroid2.9.png b/core/res/drawable-xhdpi/ab_transparent_transdroid2.9.png
new file mode 100644
index 00000000..78739156
Binary files /dev/null and b/core/res/drawable-xhdpi/ab_transparent_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/btn_cab_done_default_transdroid2.9.png b/core/res/drawable-xhdpi/btn_cab_done_default_transdroid2.9.png
new file mode 100644
index 00000000..5d9632ed
Binary files /dev/null and b/core/res/drawable-xhdpi/btn_cab_done_default_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/btn_cab_done_focused_transdroid2.9.png b/core/res/drawable-xhdpi/btn_cab_done_focused_transdroid2.9.png
new file mode 100644
index 00000000..57e1c92e
Binary files /dev/null and b/core/res/drawable-xhdpi/btn_cab_done_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/btn_cab_done_pressed_transdroid2.9.png b/core/res/drawable-xhdpi/btn_cab_done_pressed_transdroid2.9.png
new file mode 100644
index 00000000..56b13fa9
Binary files /dev/null and b/core/res/drawable-xhdpi/btn_cab_done_pressed_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/cab_background_bottom_transdroid2.9.png b/core/res/drawable-xhdpi/cab_background_bottom_transdroid2.9.png
new file mode 100644
index 00000000..9272cc29
Binary files /dev/null and b/core/res/drawable-xhdpi/cab_background_bottom_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/cab_background_top_transdroid2.9.png b/core/res/drawable-xhdpi/cab_background_top_transdroid2.9.png
new file mode 100644
index 00000000..ca1b570b
Binary files /dev/null and b/core/res/drawable-xhdpi/cab_background_top_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_discard_dark.png b/core/res/drawable-xhdpi/ic_action_discard_dark.png
new file mode 100644
index 00000000..412b3335
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_discard_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_discard_light.png b/core/res/drawable-xhdpi/ic_action_discard_light.png
new file mode 100644
index 00000000..98c73da1
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_discard_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_labels_dark.png b/core/res/drawable-xhdpi/ic_action_labels_dark.png
new file mode 100644
index 00000000..8fdcd1a2
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_labels_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_labels_light.png b/core/res/drawable-xhdpi/ic_action_labels_light.png
new file mode 100644
index 00000000..c1ec9727
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_labels_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_new_dark.png b/core/res/drawable-xhdpi/ic_action_new_dark.png
new file mode 100644
index 00000000..23b9a1c1
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_new_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_new_light.png b/core/res/drawable-xhdpi/ic_action_new_light.png
new file mode 100644
index 00000000..9b48a63d
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_new_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_pause_dark.png b/core/res/drawable-xhdpi/ic_action_pause_dark.png
new file mode 100644
index 00000000..333c1b24
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_pause_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_pause_light.png b/core/res/drawable-xhdpi/ic_action_pause_light.png
new file mode 100644
index 00000000..97d6f91a
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_pause_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_priority_high_dark.png b/core/res/drawable-xhdpi/ic_action_priority_high_dark.png
new file mode 100644
index 00000000..5932124c
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_priority_high_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_priority_high_light.png b/core/res/drawable-xhdpi/ic_action_priority_high_light.png
new file mode 100644
index 00000000..c20807d6
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_priority_high_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_priority_low_dark.png b/core/res/drawable-xhdpi/ic_action_priority_low_dark.png
new file mode 100644
index 00000000..d0b6ba5f
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_priority_low_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_priority_low_light.png b/core/res/drawable-xhdpi/ic_action_priority_low_light.png
new file mode 100644
index 00000000..abf14070
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_priority_low_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_priority_normal_dark.png b/core/res/drawable-xhdpi/ic_action_priority_normal_dark.png
new file mode 100644
index 00000000..8c1f81af
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_priority_normal_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_priority_normal_light.png b/core/res/drawable-xhdpi/ic_action_priority_normal_light.png
new file mode 100644
index 00000000..9405ed21
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_priority_normal_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_priority_off_dark.png b/core/res/drawable-xhdpi/ic_action_priority_off_dark.png
new file mode 100644
index 00000000..f6b9ff22
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_priority_off_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_priority_off_light.png b/core/res/drawable-xhdpi/ic_action_priority_off_light.png
new file mode 100644
index 00000000..4424a0c3
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_priority_off_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_refresh_dark.png b/core/res/drawable-xhdpi/ic_action_refresh_dark.png
new file mode 100644
index 00000000..a7fdc0df
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_refresh_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_refresh_light.png b/core/res/drawable-xhdpi/ic_action_refresh_light.png
new file mode 100644
index 00000000..e6212cf6
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_refresh_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_remove_dark.png b/core/res/drawable-xhdpi/ic_action_remove_dark.png
new file mode 100644
index 00000000..f391760e
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_remove_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_remove_light.png b/core/res/drawable-xhdpi/ic_action_remove_light.png
new file mode 100644
index 00000000..ca7d159f
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_remove_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_resume_dark.png b/core/res/drawable-xhdpi/ic_action_resume_dark.png
new file mode 100644
index 00000000..fe6b5588
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_resume_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_resume_light.png b/core/res/drawable-xhdpi/ic_action_resume_light.png
new file mode 100644
index 00000000..61b8d595
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_resume_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_rss_dark.png b/core/res/drawable-xhdpi/ic_action_rss_dark.png
new file mode 100644
index 00000000..dcd88e1c
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_rss_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_rss_light.png b/core/res/drawable-xhdpi/ic_action_rss_light.png
new file mode 100644
index 00000000..03365510
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_rss_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_search_dark.png b/core/res/drawable-xhdpi/ic_action_search_dark.png
new file mode 100644
index 00000000..3549f84d
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_search_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_search_light.png b/core/res/drawable-xhdpi/ic_action_search_light.png
new file mode 100644
index 00000000..804420ae
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_search_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_sort_by_size_light.png b/core/res/drawable-xhdpi/ic_action_sort_by_size_light.png
new file mode 100644
index 00000000..59da08e4
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_sort_by_size_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_start_dark.png b/core/res/drawable-xhdpi/ic_action_start_dark.png
new file mode 100644
index 00000000..51124993
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_start_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_start_light.png b/core/res/drawable-xhdpi/ic_action_start_light.png
new file mode 100644
index 00000000..2d67d31e
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_start_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_stop_dark.png b/core/res/drawable-xhdpi/ic_action_stop_dark.png
new file mode 100644
index 00000000..ee5eda25
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_stop_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_stop_light.png b/core/res/drawable-xhdpi/ic_action_stop_light.png
new file mode 100644
index 00000000..9a23e3d4
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_stop_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_trackers_dark.png b/core/res/drawable-xhdpi/ic_action_trackers_dark.png
new file mode 100644
index 00000000..2caf74c9
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_trackers_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_trackers_light.png b/core/res/drawable-xhdpi/ic_action_trackers_light.png
new file mode 100644
index 00000000..b607e604
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_trackers_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_turtle_disabled.png b/core/res/drawable-xhdpi/ic_action_turtle_disabled.png
new file mode 100644
index 00000000..12068f67
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_turtle_disabled.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_turtle_enabled.png b/core/res/drawable-xhdpi/ic_action_turtle_enabled.png
new file mode 100644
index 00000000..8bfedf1e
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_turtle_enabled.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_website_dark.png b/core/res/drawable-xhdpi/ic_action_website_dark.png
new file mode 100644
index 00000000..9b77be96
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_website_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_action_website_light.png b/core/res/drawable-xhdpi/ic_action_website_light.png
new file mode 100644
index 00000000..bd6b8682
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_action_website_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_activity_torrents.png b/core/res/drawable-xhdpi/ic_activity_torrents.png
new file mode 100644
index 00000000..25614e00
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_activity_torrents.png differ
diff --git a/core/res/drawable-xhdpi/ic_empty_details_dark.png b/core/res/drawable-xhdpi/ic_empty_details_dark.png
new file mode 100644
index 00000000..7b446989
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_empty_details_dark.png differ
diff --git a/core/res/drawable-xhdpi/ic_empty_details_light.png b/core/res/drawable-xhdpi/ic_empty_details_light.png
new file mode 100644
index 00000000..055caa04
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_empty_details_light.png differ
diff --git a/core/res/drawable-xhdpi/ic_launcher.png b/core/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..258c1fe5
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/core/res/drawable-xhdpi/ic_stat_notification.png b/core/res/drawable-xhdpi/ic_stat_notification.png
new file mode 100644
index 00000000..abf1f208
Binary files /dev/null and b/core/res/drawable-xhdpi/ic_stat_notification.png differ
diff --git a/core/res/drawable-xhdpi/list_focused_transdroid.9.png b/core/res/drawable-xhdpi/list_focused_transdroid.9.png
new file mode 100644
index 00000000..368d15f6
Binary files /dev/null and b/core/res/drawable-xhdpi/list_focused_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/list_focused_transdroid2.9.png b/core/res/drawable-xhdpi/list_focused_transdroid2.9.png
new file mode 100644
index 00000000..d41b7ecd
Binary files /dev/null and b/core/res/drawable-xhdpi/list_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/menu_dropdown_panel_transdroid.9.png b/core/res/drawable-xhdpi/menu_dropdown_panel_transdroid.9.png
new file mode 100644
index 00000000..2974663c
Binary files /dev/null and b/core/res/drawable-xhdpi/menu_dropdown_panel_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/menu_dropdown_panel_transdroid2.9.png b/core/res/drawable-xhdpi/menu_dropdown_panel_transdroid2.9.png
new file mode 100644
index 00000000..3d9f6149
Binary files /dev/null and b/core/res/drawable-xhdpi/menu_dropdown_panel_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/menu_hardkey_panel_transdroid.9.png b/core/res/drawable-xhdpi/menu_hardkey_panel_transdroid.9.png
new file mode 100644
index 00000000..97c03063
Binary files /dev/null and b/core/res/drawable-xhdpi/menu_hardkey_panel_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/progress_bg_transdroid.9.png b/core/res/drawable-xhdpi/progress_bg_transdroid.9.png
new file mode 100644
index 00000000..8b4853aa
Binary files /dev/null and b/core/res/drawable-xhdpi/progress_bg_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/progress_bg_transdroid2.9.png b/core/res/drawable-xhdpi/progress_bg_transdroid2.9.png
new file mode 100644
index 00000000..5ffc2acc
Binary files /dev/null and b/core/res/drawable-xhdpi/progress_bg_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/progress_primary_transdroid.9.png b/core/res/drawable-xhdpi/progress_primary_transdroid.9.png
new file mode 100644
index 00000000..b1c9444a
Binary files /dev/null and b/core/res/drawable-xhdpi/progress_primary_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/progress_primary_transdroid2.9.png b/core/res/drawable-xhdpi/progress_primary_transdroid2.9.png
new file mode 100644
index 00000000..e6d5511a
Binary files /dev/null and b/core/res/drawable-xhdpi/progress_primary_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/progress_secondary_transdroid.9.png b/core/res/drawable-xhdpi/progress_secondary_transdroid.9.png
new file mode 100644
index 00000000..48cc8e57
Binary files /dev/null and b/core/res/drawable-xhdpi/progress_secondary_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/progress_secondary_transdroid2.9.png b/core/res/drawable-xhdpi/progress_secondary_transdroid2.9.png
new file mode 100644
index 00000000..8676a0fd
Binary files /dev/null and b/core/res/drawable-xhdpi/progress_secondary_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/spinner_ab_default_transdroid.9.png b/core/res/drawable-xhdpi/spinner_ab_default_transdroid.9.png
new file mode 100644
index 00000000..14b1401d
Binary files /dev/null and b/core/res/drawable-xhdpi/spinner_ab_default_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/spinner_ab_default_transdroid2.9.png b/core/res/drawable-xhdpi/spinner_ab_default_transdroid2.9.png
new file mode 100644
index 00000000..f738a44c
Binary files /dev/null and b/core/res/drawable-xhdpi/spinner_ab_default_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/spinner_ab_disabled_transdroid.9.png b/core/res/drawable-xhdpi/spinner_ab_disabled_transdroid.9.png
new file mode 100644
index 00000000..c9dfbd60
Binary files /dev/null and b/core/res/drawable-xhdpi/spinner_ab_disabled_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/spinner_ab_disabled_transdroid2.9.png b/core/res/drawable-xhdpi/spinner_ab_disabled_transdroid2.9.png
new file mode 100644
index 00000000..79d24c9f
Binary files /dev/null and b/core/res/drawable-xhdpi/spinner_ab_disabled_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/spinner_ab_focused_transdroid.9.png b/core/res/drawable-xhdpi/spinner_ab_focused_transdroid.9.png
new file mode 100644
index 00000000..e4eddc17
Binary files /dev/null and b/core/res/drawable-xhdpi/spinner_ab_focused_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/spinner_ab_focused_transdroid2.9.png b/core/res/drawable-xhdpi/spinner_ab_focused_transdroid2.9.png
new file mode 100644
index 00000000..e52a8dd4
Binary files /dev/null and b/core/res/drawable-xhdpi/spinner_ab_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/spinner_ab_pressed_transdroid.9.png b/core/res/drawable-xhdpi/spinner_ab_pressed_transdroid.9.png
new file mode 100644
index 00000000..95f9b4c6
Binary files /dev/null and b/core/res/drawable-xhdpi/spinner_ab_pressed_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/spinner_ab_pressed_transdroid2.9.png b/core/res/drawable-xhdpi/spinner_ab_pressed_transdroid2.9.png
new file mode 100644
index 00000000..ec063706
Binary files /dev/null and b/core/res/drawable-xhdpi/spinner_ab_pressed_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/tab_selected_focused_transdroid.9.png b/core/res/drawable-xhdpi/tab_selected_focused_transdroid.9.png
new file mode 100644
index 00000000..1c6ccb7b
Binary files /dev/null and b/core/res/drawable-xhdpi/tab_selected_focused_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/tab_selected_focused_transdroid2.9.png b/core/res/drawable-xhdpi/tab_selected_focused_transdroid2.9.png
new file mode 100644
index 00000000..34fcda3b
Binary files /dev/null and b/core/res/drawable-xhdpi/tab_selected_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/tab_selected_pressed_transdroid.9.png b/core/res/drawable-xhdpi/tab_selected_pressed_transdroid.9.png
new file mode 100644
index 00000000..bed352bd
Binary files /dev/null and b/core/res/drawable-xhdpi/tab_selected_pressed_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/tab_selected_pressed_transdroid2.9.png b/core/res/drawable-xhdpi/tab_selected_pressed_transdroid2.9.png
new file mode 100644
index 00000000..bfb3d453
Binary files /dev/null and b/core/res/drawable-xhdpi/tab_selected_pressed_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/tab_selected_transdroid.9.png b/core/res/drawable-xhdpi/tab_selected_transdroid.9.png
new file mode 100644
index 00000000..794323de
Binary files /dev/null and b/core/res/drawable-xhdpi/tab_selected_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/tab_selected_transdroid2.9.png b/core/res/drawable-xhdpi/tab_selected_transdroid2.9.png
new file mode 100644
index 00000000..0c90c530
Binary files /dev/null and b/core/res/drawable-xhdpi/tab_selected_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/tab_unselected_focused_transdroid.9.png b/core/res/drawable-xhdpi/tab_unselected_focused_transdroid.9.png
new file mode 100644
index 00000000..564f40f5
Binary files /dev/null and b/core/res/drawable-xhdpi/tab_unselected_focused_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/tab_unselected_focused_transdroid2.9.png b/core/res/drawable-xhdpi/tab_unselected_focused_transdroid2.9.png
new file mode 100644
index 00000000..90d55085
Binary files /dev/null and b/core/res/drawable-xhdpi/tab_unselected_focused_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/tab_unselected_pressed_transdroid.9.png b/core/res/drawable-xhdpi/tab_unselected_pressed_transdroid.9.png
new file mode 100644
index 00000000..3f885d60
Binary files /dev/null and b/core/res/drawable-xhdpi/tab_unselected_pressed_transdroid.9.png differ
diff --git a/core/res/drawable-xhdpi/tab_unselected_pressed_transdroid2.9.png b/core/res/drawable-xhdpi/tab_unselected_pressed_transdroid2.9.png
new file mode 100644
index 00000000..b5d5335c
Binary files /dev/null and b/core/res/drawable-xhdpi/tab_unselected_pressed_transdroid2.9.png differ
diff --git a/core/res/drawable-xhdpi/tab_unselected_transdroid2.9.png b/core/res/drawable-xhdpi/tab_unselected_transdroid2.9.png
new file mode 100644
index 00000000..6fd9a251
Binary files /dev/null and b/core/res/drawable-xhdpi/tab_unselected_transdroid2.9.png differ
diff --git a/core/res/drawable-xxhdpi-v11/ic_stat_notification.png b/core/res/drawable-xxhdpi-v11/ic_stat_notification.png
new file mode 100644
index 00000000..d09f3bf0
Binary files /dev/null and b/core/res/drawable-xxhdpi-v11/ic_stat_notification.png differ
diff --git a/core/res/drawable-xxhdpi/ic_launcher.png b/core/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..6f6ea1ce
Binary files /dev/null and b/core/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/core/res/drawable-xxhdpi/ic_stat_notification.png b/core/res/drawable-xxhdpi/ic_stat_notification.png
new file mode 100644
index 00000000..e5afa301
Binary files /dev/null and b/core/res/drawable-xxhdpi/ic_stat_notification.png differ
diff --git a/core/res/drawable/ab_background_textured_transdroid2.xml b/core/res/drawable/ab_background_textured_transdroid2.xml
new file mode 100644
index 00000000..d08217b4
--- /dev/null
+++ b/core/res/drawable/ab_background_textured_transdroid2.xml
@@ -0,0 +1,21 @@
+
+
+
+
\ No newline at end of file
diff --git a/core/res/drawable/btn_cab_done_transdroid2.xml b/core/res/drawable/btn_cab_done_transdroid2.xml
new file mode 100644
index 00000000..ba1783fc
--- /dev/null
+++ b/core/res/drawable/btn_cab_done_transdroid2.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/core/res/drawable/details_list_background_dark.xml b/core/res/drawable/details_list_background_dark.xml
new file mode 100644
index 00000000..6da452cb
--- /dev/null
+++ b/core/res/drawable/details_list_background_dark.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/core/res/drawable/details_list_background_light.xml b/core/res/drawable/details_list_background_light.xml
new file mode 100644
index 00000000..57af89ec
--- /dev/null
+++ b/core/res/drawable/details_list_background_light.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/core/res/drawable/loading_progress_dark.xml b/core/res/drawable/loading_progress_dark.xml
new file mode 100644
index 00000000..326e7639
--- /dev/null
+++ b/core/res/drawable/loading_progress_dark.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/core/res/drawable/loading_progress_light.xml b/core/res/drawable/loading_progress_light.xml
new file mode 100644
index 00000000..ed73f056
--- /dev/null
+++ b/core/res/drawable/loading_progress_light.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/core/res/drawable/pressed_background_transdroid.xml b/core/res/drawable/pressed_background_transdroid.xml
new file mode 100644
index 00000000..d59067c2
--- /dev/null
+++ b/core/res/drawable/pressed_background_transdroid.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/core/res/drawable/pressed_background_transdroid2.xml b/core/res/drawable/pressed_background_transdroid2.xml
new file mode 100644
index 00000000..4998b8fd
--- /dev/null
+++ b/core/res/drawable/pressed_background_transdroid2.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
diff --git a/core/res/drawable/section_header.xml b/core/res/drawable/section_header.xml
new file mode 100644
index 00000000..d6094f63
--- /dev/null
+++ b/core/res/drawable/section_header.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/drawable/selectable_background_transdroid.xml b/core/res/drawable/selectable_background_transdroid.xml
new file mode 100644
index 00000000..77db9571
--- /dev/null
+++ b/core/res/drawable/selectable_background_transdroid.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/drawable/selectable_background_transdroid2.xml b/core/res/drawable/selectable_background_transdroid2.xml
new file mode 100644
index 00000000..268cdcd4
--- /dev/null
+++ b/core/res/drawable/selectable_background_transdroid2.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/drawable/spinner_background_ab_transdroid.xml b/core/res/drawable/spinner_background_ab_transdroid.xml
new file mode 100644
index 00000000..32edfe7b
--- /dev/null
+++ b/core/res/drawable/spinner_background_ab_transdroid.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
diff --git a/core/res/drawable/spinner_background_ab_transdroid2.xml b/core/res/drawable/spinner_background_ab_transdroid2.xml
new file mode 100644
index 00000000..d1adeb93
--- /dev/null
+++ b/core/res/drawable/spinner_background_ab_transdroid2.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
diff --git a/core/res/drawable/tab_indicator_ab_transdroid.xml b/core/res/drawable/tab_indicator_ab_transdroid.xml
new file mode 100644
index 00000000..5345f3e5
--- /dev/null
+++ b/core/res/drawable/tab_indicator_ab_transdroid.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/res/drawable/tab_indicator_ab_transdroid2.xml b/core/res/drawable/tab_indicator_ab_transdroid2.xml
new file mode 100644
index 00000000..c940e202
--- /dev/null
+++ b/core/res/drawable/tab_indicator_ab_transdroid2.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/res/layout-v14/actionbar_progressitem.xml b/core/res/layout-v14/actionbar_progressitem.xml
new file mode 100644
index 00000000..f6dfd64a
--- /dev/null
+++ b/core/res/layout-v14/actionbar_progressitem.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/res/layout-w600dp/activity_search.xml b/core/res/layout-w600dp/activity_search.xml
new file mode 100644
index 00000000..88035aff
--- /dev/null
+++ b/core/res/layout-w600dp/activity_search.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout-w600dp/activity_torrents.xml b/core/res/layout-w600dp/activity_torrents.xml
new file mode 100644
index 00000000..b19c1959
--- /dev/null
+++ b/core/res/layout-w600dp/activity_torrents.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout-w900dp/activity_rssfeeds.xml b/core/res/layout-w900dp/activity_rssfeeds.xml
new file mode 100644
index 00000000..8e208f55
--- /dev/null
+++ b/core/res/layout-w900dp/activity_rssfeeds.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout-w900dp/activity_torrents.xml b/core/res/layout-w900dp/activity_torrents.xml
new file mode 100644
index 00000000..8ffcf39b
--- /dev/null
+++ b/core/res/layout-w900dp/activity_torrents.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/actionbar_navigation.xml b/core/res/layout/actionbar_navigation.xml
new file mode 100644
index 00000000..203d103f
--- /dev/null
+++ b/core/res/layout/actionbar_navigation.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/actionbar_progressitem.xml b/core/res/layout/actionbar_progressitem.xml
new file mode 100644
index 00000000..5123e8df
--- /dev/null
+++ b/core/res/layout/actionbar_progressitem.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/res/layout/actionbar_searchsite.xml b/core/res/layout/actionbar_searchsite.xml
new file mode 100644
index 00000000..63244f61
--- /dev/null
+++ b/core/res/layout/actionbar_searchsite.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/actionbar_serverstatus.xml b/core/res/layout/actionbar_serverstatus.xml
new file mode 100644
index 00000000..e5370b6a
--- /dev/null
+++ b/core/res/layout/actionbar_serverstatus.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/activity_details.xml b/core/res/layout/activity_details.xml
new file mode 100644
index 00000000..f1d8b7c8
--- /dev/null
+++ b/core/res/layout/activity_details.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/activity_rssfeeds.xml b/core/res/layout/activity_rssfeeds.xml
new file mode 100644
index 00000000..a8e7aaac
--- /dev/null
+++ b/core/res/layout/activity_rssfeeds.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/activity_rssitems.xml b/core/res/layout/activity_rssitems.xml
new file mode 100644
index 00000000..e86818dd
--- /dev/null
+++ b/core/res/layout/activity_rssitems.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/activity_search.xml b/core/res/layout/activity_search.xml
new file mode 100644
index 00000000..e26348e2
--- /dev/null
+++ b/core/res/layout/activity_search.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/activity_torrents.xml b/core/res/layout/activity_torrents.xml
new file mode 100644
index 00000000..feb3cba7
--- /dev/null
+++ b/core/res/layout/activity_torrents.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/dialog_about.xml b/core/res/layout/dialog_about.xml
new file mode 100644
index 00000000..ce92e4a2
--- /dev/null
+++ b/core/res/layout/dialog_about.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/dialog_changelog.xml b/core/res/layout/dialog_changelog.xml
new file mode 100644
index 00000000..dc51afd1
--- /dev/null
+++ b/core/res/layout/dialog_changelog.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/core/res/layout/dialog_setlabel.xml b/core/res/layout/dialog_setlabel.xml
new file mode 100644
index 00000000..034d2b49
--- /dev/null
+++ b/core/res/layout/dialog_setlabel.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/dialog_trackers.xml b/core/res/layout/dialog_trackers.xml
new file mode 100644
index 00000000..691f1132
--- /dev/null
+++ b/core/res/layout/dialog_trackers.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/dialog_transferrates.xml b/core/res/layout/dialog_transferrates.xml
new file mode 100644
index 00000000..9bdf34d3
--- /dev/null
+++ b/core/res/layout/dialog_transferrates.xml
@@ -0,0 +1,293 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/fragment_details.xml b/core/res/layout/fragment_details.xml
new file mode 100644
index 00000000..e60c310b
--- /dev/null
+++ b/core/res/layout/fragment_details.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/fragment_details_header.xml b/core/res/layout/fragment_details_header.xml
new file mode 100644
index 00000000..907b71d5
--- /dev/null
+++ b/core/res/layout/fragment_details_header.xml
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/fragment_filters.xml b/core/res/layout/fragment_filters.xml
new file mode 100644
index 00000000..7ccfb746
--- /dev/null
+++ b/core/res/layout/fragment_filters.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/fragment_rssfeeds.xml b/core/res/layout/fragment_rssfeeds.xml
new file mode 100644
index 00000000..d4181251
--- /dev/null
+++ b/core/res/layout/fragment_rssfeeds.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/fragment_rssitems.xml b/core/res/layout/fragment_rssitems.xml
new file mode 100644
index 00000000..2549c479
--- /dev/null
+++ b/core/res/layout/fragment_rssitems.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/fragment_searchresults.xml b/core/res/layout/fragment_searchresults.xml
new file mode 100644
index 00000000..a3a69c8a
--- /dev/null
+++ b/core/res/layout/fragment_searchresults.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/fragment_torrents.xml b/core/res/layout/fragment_torrents.xml
new file mode 100644
index 00000000..292df0b1
--- /dev/null
+++ b/core/res/layout/fragment_torrents.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/list_item_filter.xml b/core/res/layout/list_item_filter.xml
new file mode 100644
index 00000000..8d1e4444
--- /dev/null
+++ b/core/res/layout/list_item_filter.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/list_item_rssfeed.xml b/core/res/layout/list_item_rssfeed.xml
new file mode 100644
index 00000000..00ca102a
--- /dev/null
+++ b/core/res/layout/list_item_rssfeed.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/list_item_rssitem.xml b/core/res/layout/list_item_rssitem.xml
new file mode 100644
index 00000000..c1297c62
--- /dev/null
+++ b/core/res/layout/list_item_rssitem.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/list_item_searchresult.xml b/core/res/layout/list_item_searchresult.xml
new file mode 100644
index 00000000..f9c62e3c
--- /dev/null
+++ b/core/res/layout/list_item_searchresult.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/list_item_searchsite.xml b/core/res/layout/list_item_searchsite.xml
new file mode 100644
index 00000000..92b0838b
--- /dev/null
+++ b/core/res/layout/list_item_searchsite.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/list_item_separator.xml b/core/res/layout/list_item_separator.xml
new file mode 100644
index 00000000..c844caa6
--- /dev/null
+++ b/core/res/layout/list_item_separator.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/list_item_simple.xml b/core/res/layout/list_item_simple.xml
new file mode 100644
index 00000000..48a567f0
--- /dev/null
+++ b/core/res/layout/list_item_simple.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/list_item_torrent.xml b/core/res/layout/list_item_torrent.xml
new file mode 100644
index 00000000..e2a0ffe4
--- /dev/null
+++ b/core/res/layout/list_item_torrent.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/layout/list_item_torrentfile.xml b/core/res/layout/list_item_torrentfile.xml
new file mode 100644
index 00000000..84a43658
--- /dev/null
+++ b/core/res/layout/list_item_torrentfile.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/menu/activity_deleteableprefs.xml b/core/res/menu/activity_deleteableprefs.xml
new file mode 100644
index 00000000..64e8baae
--- /dev/null
+++ b/core/res/menu/activity_deleteableprefs.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/menu/activity_details.xml b/core/res/menu/activity_details.xml
new file mode 100644
index 00000000..c1582953
--- /dev/null
+++ b/core/res/menu/activity_details.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/menu/activity_search.xml b/core/res/menu/activity_search.xml
new file mode 100644
index 00000000..3e2ad945
--- /dev/null
+++ b/core/res/menu/activity_search.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/menu/activity_torrents.xml b/core/res/menu/activity_torrents.xml
new file mode 100644
index 00000000..633e76f0
--- /dev/null
+++ b/core/res/menu/activity_torrents.xml
@@ -0,0 +1,76 @@
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/menu/dialog_about.xml b/core/res/menu/dialog_about.xml
new file mode 100644
index 00000000..cb82ee19
--- /dev/null
+++ b/core/res/menu/dialog_about.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/menu/fragment_details.xml b/core/res/menu/fragment_details.xml
new file mode 100644
index 00000000..2b4c5ac9
--- /dev/null
+++ b/core/res/menu/fragment_details.xml
@@ -0,0 +1,56 @@
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/menu/fragment_details_cab.xml b/core/res/menu/fragment_details_cab.xml
new file mode 100644
index 00000000..d0f4a8ba
--- /dev/null
+++ b/core/res/menu/fragment_details_cab.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/menu/fragment_rssfeeds.xml b/core/res/menu/fragment_rssfeeds.xml
new file mode 100644
index 00000000..8975f24b
--- /dev/null
+++ b/core/res/menu/fragment_rssfeeds.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/menu/fragment_rssitems_cab.xml b/core/res/menu/fragment_rssitems_cab.xml
new file mode 100644
index 00000000..a1478db3
--- /dev/null
+++ b/core/res/menu/fragment_rssitems_cab.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/menu/fragment_searchresults_cab.xml b/core/res/menu/fragment_searchresults_cab.xml
new file mode 100644
index 00000000..68335641
--- /dev/null
+++ b/core/res/menu/fragment_searchresults_cab.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/menu/fragment_torrents_cab.xml b/core/res/menu/fragment_torrents_cab.xml
new file mode 100644
index 00000000..afb60120
--- /dev/null
+++ b/core/res/menu/fragment_torrents_cab.xml
@@ -0,0 +1,29 @@
+
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/values-land/dimens.xml b/core/res/values-land/dimens.xml
new file mode 100644
index 00000000..28365483
--- /dev/null
+++ b/core/res/values-land/dimens.xml
@@ -0,0 +1,13 @@
+
+
+
+ 15sp
+ 12sp
+ 1dp
+ 2dp
+ 19sp
+ 12sp
+ 12sp
+ 53dp
+
+
\ No newline at end of file
diff --git a/core/res/values-sw500dp/bools.xml b/core/res/values-sw500dp/bools.xml
new file mode 100644
index 00000000..f701a217
--- /dev/null
+++ b/core/res/values-sw500dp/bools.xml
@@ -0,0 +1,5 @@
+
+
+
+ false
+
\ No newline at end of file
diff --git a/core/res/values-sw600dp/dimens.xml b/core/res/values-sw600dp/dimens.xml
new file mode 100644
index 00000000..f4926c5c
--- /dev/null
+++ b/core/res/values-sw600dp/dimens.xml
@@ -0,0 +1,27 @@
+
+
+
+ 16dp
+ 8dp
+ 16dp
+
+
+ 14sp
+ 17sp
+ 18sp
+ 27sp
+ 40sp
+ 14sp
+
+
+ 18sp
+ 15sp
+ 125dp
+ 2dp
+ 5dp
+ 25sp
+ 17sp
+ 14sp
+ 63dp
+
+
\ No newline at end of file
diff --git a/core/res/values-v11/styles_transdroid_dark.xml b/core/res/values-v11/styles_transdroid_dark.xml
new file mode 100644
index 00000000..bf417f3e
--- /dev/null
+++ b/core/res/values-v11/styles_transdroid_dark.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/values-v11/styles_transdroid_light.xml b/core/res/values-v11/styles_transdroid_light.xml
new file mode 100644
index 00000000..03a595ff
--- /dev/null
+++ b/core/res/values-v11/styles_transdroid_light.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/values-v14/styles_transdroid_dark.xml b/core/res/values-v14/styles_transdroid_dark.xml
new file mode 100644
index 00000000..62a740a1
--- /dev/null
+++ b/core/res/values-v14/styles_transdroid_dark.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/values-v14/styles_transdroid_light.xml b/core/res/values-v14/styles_transdroid_light.xml
new file mode 100644
index 00000000..242309f8
--- /dev/null
+++ b/core/res/values-v14/styles_transdroid_light.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/values-v16/styles.xml b/core/res/values-v16/styles.xml
new file mode 100644
index 00000000..fcfa28db
--- /dev/null
+++ b/core/res/values-v16/styles.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/values/attrs.xml b/core/res/values/attrs.xml
new file mode 100644
index 00000000..1fc1ff87
--- /dev/null
+++ b/core/res/values/attrs.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/values/bools.xml b/core/res/values/bools.xml
new file mode 100644
index 00000000..63d71d3e
--- /dev/null
+++ b/core/res/values/bools.xml
@@ -0,0 +1,5 @@
+
+
+
+ true
+
\ No newline at end of file
diff --git a/core/res/values/changelog.xml b/core/res/values/changelog.xml
new file mode 100644
index 00000000..7cd22624
--- /dev/null
+++ b/core/res/values/changelog.xml
@@ -0,0 +1,21 @@
+
+
+
+Transdroid 2.0.0-alpha3\n
+- Added torrent search\n
+- Setting of max. up/down speeds\n
+- Update trackers and set labels (where supported)\n
+- Background RSS and server checker services\n
+\n
+Transdroid 2.0.0-alpha2\n
+- Fixed Transmission adapter folder setting\n
+- Fixed RSS feed screens\n
+- UI tweaks\n
+\n
+Transdroid 2.0.0-alpha1\n
+- Totally reworked Holo-style interface\n
+- Provide tablet interface on smaller tablets\n
+\n
+Older changes: http://www.transdroid.org/about/changelog/
+
+
diff --git a/core/res/values/colors.xml b/core/res/values/colors.xml
new file mode 100644
index 00000000..5203dfd2
--- /dev/null
+++ b/core/res/values/colors.xml
@@ -0,0 +1,7 @@
+
+
+ #8acc12
+ #7dbb21
+ #c81113
+ #aada62
+
diff --git a/core/res/values/colors_transdroid_dark.xml b/core/res/values/colors_transdroid_dark.xml
new file mode 100644
index 00000000..30ee0f48
--- /dev/null
+++ b/core/res/values/colors_transdroid_dark.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ #CCaada62
+ #303030
+ #000
+ #fff
+ #fff
+
diff --git a/core/res/values/colors_transdroid_light.xml b/core/res/values/colors_transdroid_light.xml
new file mode 100644
index 00000000..84564063
--- /dev/null
+++ b/core/res/values/colors_transdroid_light.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ #CC8ACC12
+ #bdbdbd
+ #fff
+ #000
+ #000
+
diff --git a/core/res/values/dimens.xml b/core/res/values/dimens.xml
new file mode 100644
index 00000000..c98149c1
--- /dev/null
+++ b/core/res/values/dimens.xml
@@ -0,0 +1,27 @@
+
+
+
+ 16dp
+ 8dp
+ 16dp
+
+
+ 12sp
+ 15sp
+ 17sp
+ 24sp
+ 35sp
+ 14sp
+
+
+ 17sp
+ 14sp
+ 105dp
+ 2dp
+ 3dp
+ 21sp
+ 15sp
+ 13sp
+ 53dp
+
+
\ No newline at end of file
diff --git a/core/res/values/strings.xml b/core/res/values/strings.xml
new file mode 100644
index 00000000..a16fb63c
--- /dev/null
+++ b/core/res/values/strings.xml
@@ -0,0 +1,350 @@
+
+
+
+ Add
+ Add all
+ From file
+ From URL
+ Scan barcode
+ Search
+ Refresh
+ RSS
+ Enable turle mode
+ Disable turle mode
+ Sort list
+ Name
+ Status
+ Date done
+ Date added
+ Upload speed
+ Ratio
+ Filter list
+ Settings
+ Help
+ Start
+ Normal start
+ Force start
+ Stop
+ Resume
+ Pause
+ Remove
+ Remove torrent
+ Remove and delete data
+ Set label
+ Update trackers
+ Off
+ Low
+ Normal
+ High
+ Remote play in VLC
+ Download using (S)FTP
+ Show details
+ Remove settings
+ Visit transdroid.org
+
+ Transdroid allows you to monitor and manage the torrent client you run at home or on your seedbox. Setting things up can be a bit tricky, but we offer step-by-step guides and promise it\'ll be worth it!
+ Connected, but no torrent are active within the current filter
+ Select a torrent to view its details
+ SERVERS
+ STATUS
+ LABELS
+ All
+ Downloading
+ Uploading
+ Active
+ Inactive
+
+ - %1$d torrent selected
+ - %1$d torrents selected
+
+
+ - %1$d files selected
+ - %1$d files selected
+
+ Select all
+ Invert selection
+
+ STATUS: %1$s
+ Waiting to check…
+ Verifying local data…
+ Waiting to download %s
+ Error…
+ %1$s OF %2$s (%3$s)
+ %1$s, UPLOADED %2$s
+ SINCE %1$s
+ ~ %1$s
+ ETA %1$s
+ OF %1$s
+ UNKNOWN ETA
+ RATIO %1$s
+ %1$s OF %2$s PEERS
+ ↑ %1$s
+ ↓ %1$s
+ %1$s ↓
+ Downloading
+ Seeding
+ Paused
+ Queued
+ Stopped
+ Unknown status
+ Not downloaded
+ Low priority
+ Normal priority
+ High priority
+ TRACKERS
+ ERRORS
+ FILES
+ Maximum transfer speeds
+ MAX DOWNLOAD
+ MAX UPLOAD
+ KB/S
+ Reset
+ Update
+
+ - New torrent added
+ - %1$s new torrents added
+
+
+ - Torrent is finished
+ - %1$s torrents are finished
+
+
+ - %1$s added, %2$s finished torrent
+ - %1$s added, %2$s finished torrents
+
+ %1$s and others
+
+ All labels
+ Unlabeled
+ New label
+ Setting a label is not supported by your client
+ PICK A LABEL
+ NEW LABEL
+ Remove label
+ E.g. movies or linux
+
+ %1$s added (refreshing)
+ %1$s removed
+ %1$s removed and data deleted
+ %1$s resumed (refreshing)
+ %1$s stopped (refreshing)
+ %1$s started (refreshing)
+ %1$s paused (refreshing)
+ Torrents paused (refreshing)
+ Torrents resumed (refreshing)
+ Torrents stopped (refreshing)
+ Torrents started (refreshing)
+ Trackers updated
+ Label set to \'%1$s\'
+ Label removed
+ Torrent moved to \'%1$s\'
+ File priorities updated
+ Maximum transfer speeds set
+
+ Torrent search
+ Search for torrents
+ No results for your query
+ S: %1$s
+ L: %1$s
+ This feature requires a one-time installation of the Torrent Search module. Click download to get the install package (apk) from transdroid.org and restart your search.
+ Download module
+ Opening details for %1$s
+ The Barcode Scanner could not be found. Would you like to install it from the Play Store?
+ No compatible file manager could not be found. Would you like to install IO File Manager from the Play Store?
+
+ - %1$d result selected
+ - %1$d results selected
+
+
+ RSS feeds
+ You have not defined any RSS feeds yet to monitor. Torrent-specific RSS feeds keep you up to date with new releases and you are notified of new items.
+ Select an RSS feed to view the new items
+ The RSS feed is not available or it contains no items
+ Sorry, please wait until the RSS feed is loaded
+ Sorry, this RSS feed could not be loaded at this time
+
+ - %1$d item selected
+ - %1$d items selected
+
+
+ - New RSS feed torrent available
+ - %1$s new RSS feed torrents
+
+ New torrents for %1$s
+
+ Servers
+ Add new server
+ Search sites
+ Set default site
+ Add web search site
+ RSS feeds
+ Add RSS feed
+ Other settings
+
+ Name
+ Optional personal name
+ Direct search URL
+ %s will be replaced by the search query
+ Cookies
+ Optional; use the key1=value1;key2=value2 format
+ Feed URL
+ Requires authentication
+ Opens links in the webbrowser for user login
+
+ Server type
+ IP or host name
+ Port number
+ User name
+ Password
+ Deluge web password
+ Advanced settings
+ Local IP or host
+ When connected to the specified local network
+ Local network
+ The server\'s local network SSID
+ Folder
+ Usually empty
+ SCGI mount point
+ Optional settings
+ Finished notification
+ Notify when a torrent finishes
+ New torrent notification
+ Nofity when a torrent was added
+ Server OS
+ Download directory
+ Manually set absolute path for remote connections
+ Connection timeout
+ Number of seconds before a connection attempt is aborted
+ Base (S)FTP url
+ For example ftp://me@server/downloads/
+ (S)FTP password
+ Use SSL
+ Connect using https
+ Custom SSL thumbprint (SHA-1)
+ Permit only connections to this specific certificate
+ Accept all SSL certificates
+ Allow all connections from any thumbprint
+
+ Background notifications
+ Enable notifications
+ Enables the background service
+ Interval
+ How often to check the server
+ Sound
+ Vibrate
+ LED colour
+ If supported by your device
+ Support AWD notifications
+ Show torrent counter in ADW Launcher
+
+ System
+ Check for updates
+ Check transdroid.org for latest app version
+ Use dark UI theme
+ Requires a restart to take effect
+ Import settings
+ Transdroid will try to import server, web search, RSS and system settings from: %1$s
+ Settings successfully imported
+ Export settings
+ Transdroid will export server (including passwords), web search, RSS and system settings to the following plain text JSON file: %1$s
+ Settings successfully exported
+ Send error log
+ Get support or report a bug
+ View install guides
+ Available at transdroid.org/download
+ Recent changes
+ About Transdroid
+
+ - BitComet
+ - Bitflu 1.2+
+ - BitTorrent 6+
+ - Buffalo NAS -1.31
+ - Deluge 1.2+
+ - DLink Router BT
+ - Dummy
+ - Ktorrent
+ - qBittorrent
+ - rTorrent
+ - Torrentflux-b4rt
+ - Transmission
+ - µTorrent
+ - Vuze
+
+
+ - daemon_bitcomet
+ - daemon_bitflu
+ - daemon_bittorrent
+ - daemon_buffalonas
+ - daemon_deluge
+ - daemon_dlinkrouterbt
+ - daemon_dummy
+ - daemon_ktorrent
+ - daemon_qbittorrent
+ - daemon_rtorrent
+ - daemon_tfb4rt
+ - daemon_transmission
+ - daemon_utorrent
+ - daemon_vuze
+
+
+ - Windows
+ - Mac
+ - Linux
+
+
+ - type_windows
+ - type_mac
+ - type_linux
+
+
+ - 1 minute
+ - 10 minutes
+ - 30 minutes
+ - 1 hour
+ - 3 hours
+ - 12 hours
+ - 1 day
+
+
+ - 60
+ - 600
+ - 1800
+ - 3600
+ - 10800
+ - 43200
+ - 86400
+
+
+ Error during communication; check your connection
+ Internal error building request
+ Error parsing server response (please check your settings)
+ Web interface not connected to a running daemon
+ Access denied (please check your settings)
+ Can\'t read .torrent file
+ Error while parsing the RSS feed
+ This URL is not well-formed
+ Your web search URL is invalid:
+ Input is not a valid IP address or host name
+ Port number is always numeric
+ Directory paths end with a / or \
+ Timeout can not be empty and is a positive number
+ The RSS feed item didn\'t provide an URL enclosure or link tag pointing to the .torrent file
+ The RSS feed item does not provide a link to browse to
+ URL is not a (valid) RSS feed
+ SD card not available to read/write
+ File does not seem to contain Transdroid settings
+ The settings file could not be found
+ Can\'t write to the settings file
+ Please enter a positive number
+ Please enter a valid label or pick from the list
+
+ New Transdroid version available
+ New Transdroid search module available
+ You can now update to %1$s
+
+ Transdroid
+ \u00A9 Eric Kok, 2312 development
+ Published under GNU General Public License v3
+ Manage your torrents from your Android device
+
+
\ No newline at end of file
diff --git a/core/res/values/styles.xml b/core/res/values/styles.xml
new file mode 100644
index 00000000..7a37be80
--- /dev/null
+++ b/core/res/values/styles.xml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/values/styles_transdroid_dark.xml b/core/res/values/styles_transdroid_dark.xml
new file mode 100644
index 00000000..d3fbf97d
--- /dev/null
+++ b/core/res/values/styles_transdroid_dark.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/values/styles_transdroid_light.xml b/core/res/values/styles_transdroid_light.xml
new file mode 100644
index 00000000..ebf32fcf
--- /dev/null
+++ b/core/res/values/styles_transdroid_light.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/xml/pref_main.xml b/core/res/xml/pref_main.xml
new file mode 100644
index 00000000..09c61a50
--- /dev/null
+++ b/core/res/xml/pref_main.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/xml/pref_notifications.xml b/core/res/xml/pref_notifications.xml
new file mode 100644
index 00000000..972a477e
--- /dev/null
+++ b/core/res/xml/pref_notifications.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/xml/pref_rssfeed.xml b/core/res/xml/pref_rssfeed.xml
new file mode 100644
index 00000000..401c5aea
--- /dev/null
+++ b/core/res/xml/pref_rssfeed.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/xml/pref_server.xml b/core/res/xml/pref_server.xml
new file mode 100644
index 00000000..453c06b1
--- /dev/null
+++ b/core/res/xml/pref_server.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/xml/pref_system.xml b/core/res/xml/pref_system.xml
new file mode 100644
index 00000000..b0858671
--- /dev/null
+++ b/core/res/xml/pref_system.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/xml/pref_websearch.xml b/core/res/xml/pref_websearch.xml
new file mode 100644
index 00000000..0c760030
--- /dev/null
+++ b/core/res/xml/pref_websearch.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/xml/searchable.xml b/core/res/xml/searchable.xml
new file mode 100644
index 00000000..89ee8885
--- /dev/null
+++ b/core/res/xml/searchable.xml
@@ -0,0 +1,7 @@
+
+
diff --git a/core/src/com/actionbarsherlock/view/SherlockListView.java b/core/src/com/actionbarsherlock/view/SherlockListView.java
new file mode 100644
index 00000000..5a3481d0
--- /dev/null
+++ b/core/src/com/actionbarsherlock/view/SherlockListView.java
@@ -0,0 +1,341 @@
+package com.actionbarsherlock.view;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.SparseBooleanArray;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.Checkable;
+import android.widget.ListView;
+
+import com.actionbarsherlock.app.SherlockActivity;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.ActionMode;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+/**
+ * Provides backwards compatible multiple choice ActionMode support on Froyo+ using ActionBarSherlock.
+ */
+public class SherlockListView extends ListView {
+ // API 11+ reference, but ok because the value will be inlined.
+ public static final int CHOICE_MODE_MULTIPLE_MODAL_COMPAT = CHOICE_MODE_MULTIPLE_MODAL;
+
+ /**
+ * Wrapper to intercept delegation of long click events, and pass to {@link #doLongPress}
+ */
+ class OnItemLongClickListenerWrapper implements OnItemLongClickListener {
+ private OnItemLongClickListener wrapped;
+
+ public void setWrapped(OnItemLongClickListener listener) {
+ this.wrapped = listener;
+ }
+
+ @Override
+ public boolean onItemLongClick(AdapterView> view, View child, int position, long id) {
+ // this would be easier if AbsListView.performLongPress wasn't package
+ // protected :-(
+ boolean handled = doLongPress(child, position, id);
+ if (!handled && wrapped != null) {
+ return wrapped.onItemLongClick(view, child, position, id);
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Hijack the onLongClickListener so we can intercept delegation.
+ */
+ @Override
+ public void setOnItemLongClickListener(OnItemLongClickListener listener) {
+ if (longClickListenerWrapper == null) {
+ longClickListenerWrapper = new OnItemLongClickListenerWrapper();
+ }
+ longClickListenerWrapper.setWrapped(listener);
+ super.setOnItemLongClickListener(longClickListenerWrapper);
+ }
+
+ /**
+ * A MultiChoiceModeListener receives events for {@link AbsListView#CHOICE_MODE_MULTIPLE_MODAL}. It acts as the
+ * {@link ActionMode.Callback} for the selection mode and also receives
+ * {@link #onItemCheckedStateChanged(ActionMode, int, long, boolean)} events when the user selects and deselects
+ * list items.
+ */
+ @SuppressWarnings("javadoc")
+ public interface MultiChoiceModeListenerCompat extends ActionMode.Callback {
+ /**
+ * Called when an item is checked or unchecked during selection mode.
+ * @param mode The {@link ActionMode} providing the selection mode
+ * @param position Adapter position of the item that was checked or unchecked
+ * @param id Adapter ID of the item that was checked or unchecked
+ * @param checked true
if the item is now checked, false
if the item is now unchecked.
+ */
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked);
+ }
+
+ class MultiChoiceModeWrapper implements MultiChoiceModeListenerCompat {
+ private MultiChoiceModeListenerCompat wrapped;
+
+ public void setWrapped(MultiChoiceModeListenerCompat wrapped) {
+ this.wrapped = wrapped;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ if (wrapped == null) {
+ return false;
+ }
+ if (wrapped.onCreateActionMode(mode, menu)) {
+ // Initialize checked graphic state?
+ setLongClickable(false);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ if (wrapped == null) {
+ return false;
+ }
+ return wrapped.onPrepareActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ if (wrapped == null) {
+ return false;
+ }
+ return wrapped.onActionItemClicked(mode, item);
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ if (wrapped == null) {
+ return;
+ }
+ wrapped.onDestroyActionMode(mode);
+ actionMode = null;
+
+ // Ending selection mode means deselecting everything.
+ clearChoices();
+ checkedItemCount = 0;
+ updateOnScreenCheckedViews();
+ invalidateViews();
+ setLongClickable(true);
+ requestLayout();
+ invalidate();
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
+ if (wrapped == null) {
+ return;
+ }
+ wrapped.onItemCheckedStateChanged(mode, position, id, checked);
+
+ // If there are no items selected we no longer need the selection mode.
+ if (checkedItemCount == 0) {
+ mode.finish();
+ }
+ }
+ }
+
+ private com.actionbarsherlock.view.ActionMode actionMode;
+ private OnItemLongClickListenerWrapper longClickListenerWrapper;
+ private MultiChoiceModeWrapper choiceModeListener;
+ private int choiceMode;
+ private int checkedItemCount;
+
+ public SherlockListView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public SherlockListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public SherlockListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context);
+ }
+
+ void init(Context context) {
+ if (isInEditMode()) {
+ // Ignore when viewing in the UI designer
+ return;
+ }
+ if (!(context instanceof SherlockActivity || context instanceof SherlockFragmentActivity)) {
+ throw new IllegalStateException(
+ "This view must be hosted in a SherlockActivity or SherlockFragmentActivity");
+ }
+ setOnItemLongClickListener(null);
+ }
+
+ @Override
+ public void setChoiceMode(int mode) {
+ choiceMode = mode;
+ if (actionMode != null) {
+ actionMode.finish();
+ actionMode = null;
+ }
+ if (choiceMode != CHOICE_MODE_NONE) {
+ if (mode == CHOICE_MODE_MULTIPLE_MODAL_COMPAT) {
+ clearChoices();
+ checkedItemCount = 0;
+ setLongClickable(true);
+ updateOnScreenCheckedViews();
+ requestLayout();
+ invalidate();
+ mode = CHOICE_MODE_MULTIPLE;
+ }
+ super.setChoiceMode(mode);
+ }
+ }
+
+ @Override
+ public int getChoiceMode() {
+ return choiceMode;
+ }
+
+ public void setMultiChoiceModeListener(MultiChoiceModeListenerCompat listener) {
+ if (choiceModeListener == null) {
+ choiceModeListener = new MultiChoiceModeWrapper();
+ }
+ choiceModeListener.setWrapped(listener);
+ }
+
+ @Override
+ public boolean performItemClick(View view, int position, long id) {
+ boolean handled = false;
+ boolean dispatchItemClick = true;
+ boolean checkStateChanged = false;
+ if (choiceMode != CHOICE_MODE_NONE) {
+ handled = true;
+ if (choiceMode == CHOICE_MODE_MULTIPLE
+ || (choiceMode == CHOICE_MODE_MULTIPLE_MODAL_COMPAT && actionMode != null)) {
+ boolean newValue = !getCheckedItemPositions().get(position);
+ setItemChecked(position, newValue);
+ if (actionMode != null) {
+ choiceModeListener.onItemCheckedStateChanged(actionMode, position, id, newValue);
+ dispatchItemClick = false;
+ }
+ checkStateChanged = true;
+ return false;
+ } else if (choiceMode == CHOICE_MODE_SINGLE) {
+ boolean newValue = !getCheckedItemPositions().get(position);
+ setItemChecked(position, newValue);
+ checkStateChanged = true;
+ }
+ if (checkStateChanged) {
+ updateOnScreenCheckedViews();
+ }
+ }
+ if (dispatchItemClick) {
+ handled |= super.performItemClick(view, position, id);
+ }
+ return handled;
+ }
+
+ /**
+ * Perform a quick, in-place update of the checked or activated state on all visible item views. This should only be
+ * called when a valid choice mode is active.
+ *
+ * (Taken verbatim from AbsListView.java)
+ */
+ @TargetApi(11)
+ private void updateOnScreenCheckedViews() {
+ final int firstPos = getFirstVisiblePosition();
+ final int count = getChildCount();
+ final boolean useActivated = getContext().getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.HONEYCOMB;
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ final int position = firstPos + i;
+
+ if (child instanceof Checkable) {
+ ((Checkable) child).setChecked(getCheckedItemPositions().get(position));
+ } else if (useActivated) {
+ // TODO: Investigate the impact of this hack fix
+ // (API level < 11 devices still get to this code point when clicking teh CAB done button)
+ if (android.os.Build.VERSION.SDK_INT > 11) {
+ child.setActivated(getCheckedItemPositions().get(position));
+ }
+ }
+ }
+ }
+
+ public ActionMode startActionMode(ActionMode.Callback callback) {
+ if (actionMode != null) {
+ return actionMode;
+ }
+ Context context = getContext();
+ if (context instanceof SherlockActivity) {
+ actionMode = ((SherlockActivity) getContext()).startActionMode(callback);
+ } else if (context instanceof SherlockFragmentActivity) {
+ actionMode = ((SherlockFragmentActivity) context).startActionMode(callback);
+ } else {
+ throw new IllegalStateException(
+ "This view must be hosted in a SherlockActivity or SherlockFragmentActivity");
+ }
+ return actionMode;
+ }
+
+ boolean doLongPress(final View child, final int longPressPosition, final long longPressId) {
+ if (choiceMode == CHOICE_MODE_MULTIPLE_MODAL_COMPAT) {
+ if (actionMode == null && (actionMode = startActionMode(choiceModeListener)) != null) {
+ setItemChecked(longPressPosition, true);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Sets the checked state of the specified position. The is only valid if the choice mode has been set to
+ * {@link #CHOICE_MODE_SINGLE} or {@link #CHOICE_MODE_MULTIPLE}.
+ * @param position The item whose checked state is to be checked
+ * @param value The new checked state for the item
+ */
+ @Override
+ public void setItemChecked(int position, boolean value) {
+ if (choiceMode == CHOICE_MODE_NONE) {
+ return;
+ }
+ SparseBooleanArray checkStates = getCheckedItemPositions();
+
+ // Start selection mode if needed. We don't need to if we're unchecking
+ // something.
+ if (value && choiceMode == CHOICE_MODE_MULTIPLE_MODAL_COMPAT && actionMode == null) {
+ actionMode = startActionMode(choiceModeListener);
+ }
+
+ if (choiceMode == CHOICE_MODE_MULTIPLE || choiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
+ // boolean oldValue = checkStates.get(position);
+ checkStates.put(position, value);
+ if (value) {
+ checkedItemCount++;
+ } else {
+ checkedItemCount--;
+ }
+ if (actionMode != null) {
+ final long id = getAdapter().getItemId(position);
+ choiceModeListener.onItemCheckedStateChanged(actionMode, position, id, value);
+ }
+ } else {
+ if (value || isItemChecked(position)) {
+ checkStates.clear();
+ }
+ // this may end up selecting the value we just cleared but this way
+ // we ensure length of checkStates is 1, a fact getCheckedItemPosition
+ // relies on
+ if (value) {
+ checkStates.put(position, true);
+ }
+ }
+ requestLayout();
+ invalidate();
+ }
+}
diff --git a/core/src/fr/marvinlabs/widget/CheckableRelativeLayout.java b/core/src/fr/marvinlabs/widget/CheckableRelativeLayout.java
new file mode 100644
index 00000000..91932561
--- /dev/null
+++ b/core/src/fr/marvinlabs/widget/CheckableRelativeLayout.java
@@ -0,0 +1,100 @@
+package fr.marvinlabs.widget;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Checkable;
+import android.widget.RelativeLayout;
+
+/**
+ * Extension of a relative layout to provide a checkable behaviour
+ *
+ * @author marvinlabs
+ */
+public class CheckableRelativeLayout extends RelativeLayout implements Checkable {
+
+ private boolean isChecked;
+ private List checkableViews;
+
+ public CheckableRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initialise(attrs);
+ }
+
+ public CheckableRelativeLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialise(attrs);
+ }
+
+ public CheckableRelativeLayout(Context context) {
+ super(context);
+ initialise(null);
+ }
+
+ /*
+ * @see android.widget.Checkable#isChecked()
+ */
+ public boolean isChecked() {
+ return isChecked;
+ }
+
+ /*
+ * @see android.widget.Checkable#setChecked(boolean)
+ */
+ public void setChecked(boolean isChecked) {
+ this.isChecked = isChecked;
+ for (Checkable c : checkableViews) {
+ c.setChecked(isChecked);
+ }
+ }
+
+ /*
+ * @see android.widget.Checkable#toggle()
+ */
+ public void toggle() {
+ this.isChecked = !this.isChecked;
+ for (Checkable c : checkableViews) {
+ c.toggle();
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ final int childCount = this.getChildCount();
+ for (int i = 0; i < childCount; ++i) {
+ findCheckableChildren(this.getChildAt(i));
+ }
+ }
+
+ /**
+ * Read the custom XML attributes
+ */
+ private void initialise(AttributeSet attrs) {
+ this.isChecked = false;
+ this.checkableViews = new ArrayList(5);
+ }
+
+ /**
+ * Add to our checkable list all the children of the view that implement the
+ * interface Checkable
+ */
+ private void findCheckableChildren(View v) {
+ if (v instanceof Checkable) {
+ this.checkableViews.add((Checkable) v);
+ }
+
+ if (v instanceof ViewGroup) {
+ final ViewGroup vg = (ViewGroup) v;
+ final int childCount = vg.getChildCount();
+ for (int i = 0; i < childCount; ++i) {
+ findCheckableChildren(vg.getChildAt(i));
+ }
+ }
+ }
+}
diff --git a/core/src/fr/marvinlabs/widget/InertCheckBox.java b/core/src/fr/marvinlabs/widget/InertCheckBox.java
new file mode 100644
index 00000000..5dd3080b
--- /dev/null
+++ b/core/src/fr/marvinlabs/widget/InertCheckBox.java
@@ -0,0 +1,70 @@
+package fr.marvinlabs.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.widget.CheckBox;
+
+/**
+ * CheckBox that does not react to any user event in order to let the container handle them.
+ */
+public class InertCheckBox extends CheckBox {
+
+ // Provide the same constructors as the superclass
+ public InertCheckBox(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ // Provide the same constructors as the superclass
+ public InertCheckBox(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ // Provide the same constructors as the superclass
+ public InertCheckBox(Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // Make the checkbox not respond to any user event
+ return false;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // Make the checkbox not respond to any user event
+ return false;
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ // Make the checkbox not respond to any user event
+ return false;
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ // Make the checkbox not respond to any user event
+ return false;
+ }
+
+ @Override
+ public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+ // Make the checkbox not respond to any user event
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ // Make the checkbox not respond to any user event
+ return false;
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent event) {
+ // Make the checkbox not respond to any user event
+ return false;
+ }
+}
diff --git a/core/src/org/transdroid/core/app/search/GoogleWebSearchBarcodeResolver.java b/core/src/org/transdroid/core/app/search/GoogleWebSearchBarcodeResolver.java
new file mode 100644
index 00000000..93ad08c0
--- /dev/null
+++ b/core/src/org/transdroid/core/app/search/GoogleWebSearchBarcodeResolver.java
@@ -0,0 +1,140 @@
+/*
+ * This file is part of Transdroid
+ *
+ * Transdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Transdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Transdroid. If not, see .
+ *
+ */
+package org.transdroid.core.app.search;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.transdroid.daemon.util.HttpHelper;
+
+public class GoogleWebSearchBarcodeResolver {
+
+ public static final String apiUrl = "http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=%s";
+
+ public static String resolveBarcode(String barcode) {
+
+ try {
+ // We use the Google AJAX Search API to get a JSON-formatted list of web search results
+ String callUrl = apiUrl.replace("%s", barcode);
+ DefaultHttpClient httpclient = new DefaultHttpClient();
+ HttpGet httpget = new HttpGet(callUrl);
+ HttpResponse response = httpclient.execute(httpget);
+ InputStream instream = response.getEntity().getContent();
+ String result = HttpHelper.convertStreamToString(instream);
+ JSONArray results = new JSONObject(result).getJSONObject("responseData").getJSONArray("results");
+
+ // We will combine and filter multiple results, if there are any
+ if (results.length() < 1) {
+ return null;
+ }
+ return stripGarbage(results, barcode);
+ } catch (Exception e) {
+ return null;
+ }
+
+ }
+
+ private static String stripGarbage(JSONArray results, String barcode) throws JSONException {
+
+ String good = " abcdefghijklmnopqrstuvwxyz";
+ final int MAX_TITLE_CONSIDER = 4;
+ final int MAX_MISSING = 1;
+ final int MIN_TITLE_CONSIDER = 2;
+
+ // First gather the titles for the first MAX_TITLE_CONSIDER results
+ List titles = new ArrayList();
+ for (int i = 0; i < results.length() && i < MAX_TITLE_CONSIDER; i++) {
+
+ String title = results.getJSONObject(i).getString("titleNoFormatting");
+
+ // Make string lowercase first
+ title = title.toLowerCase(Locale.US);
+
+ // Remove the barcode number if it's there
+ title = title.replace(barcode, "");
+
+ // Remove unwanted words and HTML special chars
+ for (String rem : new String[] { "dvd", "blu-ray", "bluray", "&", """, "'", "<", ">" }) {
+ title = title.replace(rem, "");
+ }
+
+ // Remove all non-alphanumeric (and space) characters
+ String result = "";
+ for ( int j = 0; j < title.length(); j++ ) {
+ if ( good.indexOf(title.charAt(j)) >= 0 )
+ result += title.charAt(j);
+ }
+
+ // Remove double spaces
+ while (result.contains(" ")) {
+ result = result.replace(" ", " ");
+ }
+
+ titles.add(result);
+
+ }
+
+ // Only retain the words that are missing in at most one of the search result titles
+ List allWords = new ArrayList();
+ for (String title : titles) {
+ for (String word : Arrays.asList(title.split(" "))) {
+ if (!allWords.contains(word)) {
+ allWords.add(word);
+ }
+ }
+ }
+ List remainingWords = new ArrayList();
+ int allowMissing = Math.min(MAX_MISSING, Math.max(titles.size() - MIN_TITLE_CONSIDER, 0));
+ for (String word : allWords) {
+
+ int missing = 0;
+ for (String title : titles) {
+ if (!title.contains(word)) {
+ // The word is not contained in this result title
+ missing++;
+ if (missing > allowMissing) {
+ // Already misssing more than once, no need to look further
+ break;
+ }
+ }
+ }
+ if (missing <= allowMissing) {
+ // The word was only missing at most once, so we keep it
+ remainingWords.add(word);
+ }
+ }
+
+ // Now the query is the concatenation of the words remaining; with spaces in between
+ String query = "";
+ for (String word : remainingWords) {
+ query += " " + word;
+ }
+ return query.length() > 0? query.substring(1): null;
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/app/search/SearchHelper.java b/core/src/org/transdroid/core/app/search/SearchHelper.java
new file mode 100644
index 00000000..de0a40d8
--- /dev/null
+++ b/core/src/org/transdroid/core/app/search/SearchHelper.java
@@ -0,0 +1,121 @@
+package org.transdroid.core.app.search;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.EBean.Scope;
+import org.androidannotations.annotations.RootContext;
+
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+@EBean(scope = Scope.Singleton)
+public class SearchHelper {
+
+ static final int CURSOR_SEARCH_ID = 0;
+ static final int CURSOR_SEARCH_NAME = 1;
+ static final int CURSOR_SEARCH_TORRENTURL = 2;
+ static final int CURSOR_SEARCH_DETAILSURL = 3;
+ static final int CURSOR_SEARCH_SIZE = 4;
+ static final int CURSOR_SEARCH_ADDED = 5;
+ static final int CURSOR_SEARCH_SEEDERS = 6;
+ static final int CURSOR_SEARCH_LEECHERS = 7;
+
+ static final int CURSOR_SITE_ID = 0;
+ static final int CURSOR_SITE_CODE = 1;
+ static final int CURSOR_SITE_NAME = 2;
+ static final int CURSOR_SITE_RSSURL = 3;
+
+ @RootContext
+ protected Context context;
+
+ public enum SearchSortOrder {
+ Combined, BySeeders
+ }
+
+ /**
+ * Return whether the Torrent Search package is installed and available to query against
+ * @return True if the available sites can be retrieved from the content provider, false otherwise
+ */
+ public boolean isTorrentSearchInstalled() {
+ return getAvailableSites() != null;
+ }
+
+ /**
+ * Queries the Torrent Search package for all available in-app search sites. This method is synchronous.
+ * @return A list of available search sites as POJOs, or null if the Torrent Search package is not installed
+ */
+ public List getAvailableSites() {
+
+ // Try to access the TorrentSitesProvider of the Torrent Search app
+ Uri uri = Uri.parse("content://org.transdroid.search.torrentsitesprovider/sites");
+ ContentProviderClient test = context.getContentResolver().acquireContentProviderClient(uri);
+ if (test == null) {
+ // Torrent Search package is not yet installed
+ return null;
+ }
+
+ // Query the available in-app torrent search sites
+ Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
+ if (cursor.moveToFirst()) {
+ List sites = new ArrayList();
+ do {
+ // Read the cursor fields into the SearchSite object
+ sites.add(new SearchSite(cursor.getInt(CURSOR_SITE_ID), cursor.getString(CURSOR_SITE_CODE), cursor
+ .getString(CURSOR_SITE_NAME), cursor.getString(CURSOR_SITE_RSSURL)));
+ } while (cursor.moveToNext());
+ cursor.close();
+ return sites;
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Queries the Torrent Search module to search for torrents on the web. This method is synchronous and should always
+ * be called in a background thread.
+ * @param query The search query to pass to the torrent site
+ * @param site The site to search, as retrieved from the TorrentSitesProvider, or null if the Torrent Search package
+ * @param sortBy.name() The sort order to request from the torrent site, if supported
+ * @return A list of torrent search results as POJOs, or null if the Torrent Search package is not installed or
+ * there is no internet connection
+ */
+ public ArrayList search(String query, SearchSite site, SearchSortOrder sortBy) {
+
+ // Try to query the TorrentSearchProvider to search for torrents on the web
+ Uri uri = Uri.parse("content://org.transdroid.search.torrentsearchprovider/search/" + query);
+ Cursor cursor;
+ if (site == null) {
+ // If no explicit site was supplied, rely on the Torrent Search package's default
+ cursor = context.getContentResolver().query(uri, null, null, null, sortBy.name());
+ } else {
+ cursor = context.getContentResolver().query(uri, null, "SITE = ?", new String[] { site.getKey() },
+ sortBy.name());
+ }
+ if (cursor == null) {
+ // The content provider could not load any content (for example when there is no connection)
+ return null;
+ }
+ if (cursor.moveToFirst()) {
+ ArrayList results = new ArrayList();
+ do {
+ // Read the cursor fields into the SearchResult object
+ results.add(new SearchResult(cursor.getInt(CURSOR_SEARCH_ID), cursor.getString(CURSOR_SEARCH_NAME),
+ cursor.getString(CURSOR_SEARCH_TORRENTURL), cursor.getString(CURSOR_SEARCH_DETAILSURL), cursor
+ .getString(CURSOR_SEARCH_SIZE), cursor.getLong(CURSOR_SEARCH_ADDED), cursor
+ .getString(CURSOR_SEARCH_SEEDERS), cursor.getString(CURSOR_SEARCH_LEECHERS)));
+ } while (cursor.moveToNext());
+ cursor.close();
+ return results;
+ }
+
+ // Torrent Search package is not yet installed
+ return null;
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/app/search/SearchResult.java b/core/src/org/transdroid/core/app/search/SearchResult.java
new file mode 100644
index 00000000..feff27c6
--- /dev/null
+++ b/core/src/org/transdroid/core/app/search/SearchResult.java
@@ -0,0 +1,106 @@
+package org.transdroid.core.app.search;
+
+import java.util.Date;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents a search result as retrieved by querying the Torrent Search package.
+ * @author Eric Kok
+ */
+public class SearchResult implements Parcelable {
+
+ private final int id;
+ private final String name;
+ private final String torrentUrl;
+ private final String detailsUrl;
+ private final String size;
+ private final Date addedOn;
+ private final String seeders;
+ private final String leechers;
+
+ public SearchResult(int id, String name, String torrentUrl, String detailsUrl, String size, long addedOnTime,
+ String seeders, String leechers) {
+ this.id = id;
+ this.name = name;
+ this.torrentUrl = torrentUrl;
+ this.detailsUrl = detailsUrl;
+ this.size = size;
+ this.addedOn = (addedOnTime == -1L) ? null : new Date(addedOnTime);
+ this.seeders = seeders;
+ this.leechers = leechers;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getTorrentUrl() {
+ return torrentUrl;
+ }
+
+ public String getDetailsUrl() {
+ return detailsUrl;
+ }
+
+ public String getSize() {
+ return size;
+ }
+
+ public Date getAddedOn() {
+ return addedOn;
+ }
+
+ public String getSeeders() {
+ return seeders;
+ }
+
+ public String getLeechers() {
+ return leechers;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(id);
+ out.writeString(name);
+ out.writeString(torrentUrl);
+ out.writeString(detailsUrl);
+ out.writeString(size);
+ out.writeLong(addedOn == null ? -1 : addedOn.getTime());
+ out.writeString(seeders);
+ out.writeString(leechers);
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public SearchResult createFromParcel(Parcel in) {
+ return new SearchResult(in);
+ }
+
+ public SearchResult[] newArray(int size) {
+ return new SearchResult[size];
+ }
+ };
+
+ public SearchResult(Parcel in) {
+ id = in.readInt();
+ name = in.readString();
+ torrentUrl = in.readString();
+ detailsUrl = in.readString();
+ size = in.readString();
+ long addedOnIn = in.readLong();
+ addedOn = addedOnIn == -1 ? null : new Date(addedOnIn);
+ seeders = in.readString();
+ leechers = in.readString();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/app/search/SearchSite.java b/core/src/org/transdroid/core/app/search/SearchSite.java
new file mode 100644
index 00000000..7e051cff
--- /dev/null
+++ b/core/src/org/transdroid/core/app/search/SearchSite.java
@@ -0,0 +1,46 @@
+package org.transdroid.core.app.search;
+
+import org.transdroid.core.gui.lists.SimpleListItem;
+import org.transdroid.core.gui.search.SearchSetting;
+
+/**
+ * Represents an available torrent site that can be searched using the Torrent Search package.
+ * @author Eric Kok
+ */
+public class SearchSite implements SimpleListItem, SearchSetting {
+
+ private final int id;
+ private final String key;
+ private final String name;
+ private final String rssFeedUrl;
+
+ public SearchSite(int id, String key, String name, String rssFeedUrl) {
+ this.id = id;
+ this.key = key;
+ this.name = name;
+ this.rssFeedUrl = rssFeedUrl;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ public String getRssFeedUrl() {
+ return rssFeedUrl;
+ }
+
+ @Override
+ public String getBaseUrl() {
+ return rssFeedUrl;
+ }
+
+}
diff --git a/core/src/org/transdroid/core/app/settings/ApplicationSettings.java b/core/src/org/transdroid/core/app/settings/ApplicationSettings.java
new file mode 100644
index 00000000..cbaee05e
--- /dev/null
+++ b/core/src/org/transdroid/core/app/settings/ApplicationSettings.java
@@ -0,0 +1,491 @@
+package org.transdroid.core.app.settings;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.EBean.Scope;
+import org.androidannotations.annotations.RootContext;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.transdroid.core.app.search.SearchHelper;
+import org.transdroid.core.app.search.SearchSite;
+import org.transdroid.core.gui.search.SearchSetting;
+import org.transdroid.daemon.Daemon;
+import org.transdroid.daemon.OS;
+import org.transdroid.daemon.TorrentsSortBy;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceManager;
+
+/**
+ * Singleton object to access all application settings, including stored servers, web search sites and RSS feeds.
+ * @author Eric Kok
+ */
+@EBean(scope = Scope.Singleton)
+public class ApplicationSettings {
+
+ @RootContext
+ protected Context context;
+ private SharedPreferences prefs;
+ @Bean
+ protected SearchHelper searchHelper;
+
+ protected ApplicationSettings(Context context) {
+ prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ /**
+ * Returns all available user-configured servers
+ * @return A list of all stored server settings objects
+ */
+ public List getServerSettings() {
+ List servers = new ArrayList();
+ for (int i = 0; i <= getMaxServer(); i++) {
+ servers.add(getServerSetting(i));
+ }
+ return Collections.unmodifiableList(servers);
+ }
+
+ /**
+ * Returns the order number/identifying key of the last server
+ * @return The zero-based order number (index) of the last stored server settings
+ */
+ public int getMaxServer() {
+ for (int i = 0; true; i++) {
+ if (prefs.getString("server_type_" + i, null) == null)
+ return i - 1;
+ }
+ }
+
+ /**
+ * Returns the user-specified server settings for a specific server. WARNING: This method does not check if the
+ * settings actually exist and may rely on empty defaults if called not a non-existing server.
+ * @param order The order number/identifying key of the settings to retrieve
+ * @return The server settings object, loaded from shared preferences
+ */
+ public ServerSetting getServerSetting(int order) {
+ // @formatter:off
+ Daemon type = Daemon.fromCode(prefs.getString("server_type_" + order, null));
+ boolean ssl = prefs.getBoolean("server_sslenabled_" + order, false);
+ String defaultPort = Integer.toString(Daemon.getDefaultPortNumber(type, ssl));
+ return new ServerSetting(order,
+ prefs.getString("server_name_" + order, null), type,
+ prefs.getString("server_address_" + order, null),
+ prefs.getString("server_localaddress_" + order, null),
+ prefs.getString("server_localnetwork_" + order, null),
+ Integer.parseInt(prefs.getString("server_port_" + order, defaultPort)),
+ ssl,
+ prefs.getBoolean("server_ssltrustall_" + order, false),
+ prefs.getString("server_ssltrustkey_" + order, null),
+ prefs.getString("server_folder_" + order, null),
+ prefs.getBoolean("server_useauth_" + order, true),
+ prefs.getString("server_user_" + order, null),
+ prefs.getString("server_pass_" + order, null),
+ prefs.getString("server_extrapass_" + order, null),
+ OS.fromCode(prefs.getString("server_os_" + order, "type_linux")),
+ prefs.getString("server_downloaddir_" + order, null),
+ prefs.getString("server_ftpurl_" + order, null),
+ prefs.getString("server_ftppass_" + order, null),
+ Integer.parseInt(prefs.getString("server_timeout_" + order, "8")),
+ prefs.getBoolean("server_alarmfinished_" + order, true),
+ prefs.getBoolean("server_alarmnew_" + order, false),
+ false);
+ // @formatter:on
+ }
+
+ /**
+ * Removes all settings related to a configured server. Since servers are ordered, the order of the remaining
+ * servers will be updated accordingly.
+ * @param order The identifying order number/key of the settings to remove
+ */
+ public void removeServerSettings(int order) {
+ if (prefs.getString("server_type_" + order, null) == null)
+ return; // The settings that were requested to be removed do not exist
+
+ // Copy all settings higher than the supplied order number to the previous spot
+ Editor edit = prefs.edit();
+ int max = getMaxServer();
+ for (int i = order; i < max; i++) {
+ edit.putString("server_name_" + i, prefs.getString("server_name_" + (i + 1), null));
+ edit.putString("server_type_" + i, prefs.getString("server_type_" + (i + 1), null));
+ edit.putString("server_address_" + i, prefs.getString("server_address_" + (i + 1), null));
+ edit.putString("server_localaddress_" + i, prefs.getString("server_localaddress_" + (i + 1), null));
+ edit.putString("server_localnetwork_" + i, prefs.getString("server_localnetwork_" + (i + 1), null));
+ edit.putString("server_port_" + i, prefs.getString("server_port_" + (i + 1), null));
+ edit.putBoolean("server_sslenabled_" + i, prefs.getBoolean("server_sslenabled_" + (i + 1), false));
+ edit.putBoolean("server_ssltrustall_" + i, prefs.getBoolean("server_ssltrustall_" + (i + 1), false));
+ edit.putString("server_ssltrustkey_" + i, prefs.getString("server_ssltrustkey_" + (i + 1), null));
+ edit.putString("server_folder_" + i, prefs.getString("server_folder_" + (i + 1), null));
+ edit.putBoolean("server_useauth_" + i, prefs.getBoolean("server_useauth_" + (i + 1), true));
+ edit.putString("server_user_" + i, prefs.getString("server_user_" + (i + 1), null));
+ edit.putString("server_pass_" + i, prefs.getString("server_pass_" + (i + 1), null));
+ edit.putString("server_extrapass_" + i, prefs.getString("server_extrapass_" + (i + 1), null));
+ edit.putString("server_os_" + i, prefs.getString("server_os_" + (i + 1), null));
+ edit.putString("server_downloaddir_" + i, prefs.getString("server_downloaddir_" + (i + 1), null));
+ edit.putString("server_ftpurl_" + i, prefs.getString("server_ftpurl_" + (i + 1), null));
+ edit.putString("server_ftppass_" + i, prefs.getString("server_ftppass_" + (i + 1), null));
+ edit.putString("server_timeout_" + i, prefs.getString("server_timeout_" + (i + 1), null));
+ edit.putBoolean("server_alarmfinished_" + i, prefs.getBoolean("server_alarmfinished_" + (i + 1), true));
+ edit.putBoolean("server_alarmfinished_" + i, prefs.getBoolean("server_alarmfinished_" + (i + 1), false));
+ }
+
+ // Remove the last settings, of which we are now sure are no longer required
+ edit.remove("server_name_" + max);
+ edit.remove("server_type_" + max);
+ edit.remove("server_address_" + max);
+ edit.remove("server_localaddress_" + max);
+ edit.remove("server_localnetwork_" + max);
+ edit.remove("server_port_" + max);
+ edit.remove("server_sslenabled_" + max);
+ edit.remove("server_ssltrustall_" + max);
+ edit.remove("server_ssltrustkey_" + max);
+ edit.remove("server_folder_" + max);
+ edit.remove("server_useauth_" + max);
+ edit.remove("server_user_" + max);
+ edit.remove("server_pass_" + max);
+ edit.remove("server_extrapass_" + max);
+ edit.remove("server_os_" + max);
+ edit.remove("server_downloaddir_" + max);
+ edit.remove("server_ftpurl_" + max);
+ edit.remove("server_ftppass_" + max);
+ edit.remove("server_timeout_" + max);
+ edit.remove("server_alarmfinished_" + max);
+ edit.remove("server_alarmfinished_" + max);
+ edit.commit();
+
+ }
+
+ /**
+ * Returns the settings of the server that was last used by the user. As opposed to getLastUsedServerKey(int), this
+ * method checks whether a server was already registered as being last used and check whether the server still
+ * exists. It returns the first server if that fails. If no servers are configured, null is returned.
+ * @return A server settings object of the last used server (or, if not known, the first server), or null if no
+ * servers exist
+ */
+ public ServerSetting getLastUsedServer() {
+ int max = getMaxServer(); // Zero-based index, so with max == 0 there is 1 server
+ if (max < 0) {
+ // No servers configured
+ return null;
+ }
+ int last = getLastUsedServerKey();
+ if (last < 0 || last > max) {
+ // Last server was never set or no longer exists
+ return getServerSetting(0);
+ }
+ return getServerSetting(last);
+ }
+
+ /**
+ * Returns the order number/unique key of the server that the used last used; use with getServerSettings(int) or
+ * call getLastUsedServer directly. WARNING: the returned integer may no longer refer to a known server settings
+ * object: check the bounds.
+ * @return An integer indicating the order number/key or the last used server, or -1 if it was not set
+ */
+ public int getLastUsedServerKey() {
+ return prefs.getInt("system_lastusedserver", -1);
+ }
+
+ /**
+ * Registers some server as being the last used by the user
+ * @param server The settings of the server that the user last used
+ */
+ public void setLastUsedServer(ServerSetting server) {
+ setLastUsedServerKey(server.getOrder());
+ }
+
+ /**
+ * Registers the order number/unique key of some server as being last used by the user
+ * @param order The key identifying the specific server
+ */
+ public void setLastUsedServerKey(int order) {
+ prefs.edit().putInt("system_lastusedserver", order).commit();
+ }
+
+ /**
+ * Returns all available user-configured web-based (as opped to in-app) search sites
+ * @return A list of all stored web search site settings objects
+ */
+ public List getWebsearchSettings() {
+ List websearches = new ArrayList();
+ for (int i = 0; i <= getMaxWebsearch(); i++) {
+ websearches.add(getWebsearchSetting(i));
+ }
+ return Collections.unmodifiableList(websearches);
+ }
+
+ /**
+ * Returns the order number/identifying key of the last web search site
+ * @return The zero-based order number (index) of the last stored web search site
+ */
+ public int getMaxWebsearch() {
+ for (int i = 0; true; i++) {
+ if (prefs.getString("websearch_baseurl_" + i, null) == null)
+ return i - 1;
+ }
+ }
+
+ /**
+ * Returns the user-specified web-based search site setting for a specific site
+ * @param order The order number/identifying key of the settings to retrieve
+ * @return The web search site settings object, loaded from shared preferences
+ */
+ public WebsearchSetting getWebsearchSetting(int order) {
+ // @formatter:off
+ return new WebsearchSetting(order,
+ prefs.getString("websearch_name_" + order, null),
+ prefs.getString("websearch_baseurl_" + order, null),
+ prefs.getString("websearch_cookies_" + order, null));
+ // @formatter:on
+ }
+
+ /**
+ * Removes all settings related to a configured web-based search site. Since sites are ordered, the order of the
+ * remaining sites will be updated accordingly.
+ * @param order The identifying order number/key of the settings to remove
+ */
+ public void removeWebsearchSettings(int order) {
+ if (prefs.getString("websearch_baseurl_" + order, null) == null)
+ return; // The settings that were requested to be removed do not exist
+
+ // Copy all settings higher than the supplied order number to the previous spot
+ Editor edit = prefs.edit();
+ int max = getMaxWebsearch();
+ for (int i = order; i < max; i++) {
+ edit.putString("websearch_name_" + i, prefs.getString("websearch_name_" + (i + 1), null));
+ edit.putString("websearch_baseurl_" + i, prefs.getString("websearch_baseurl_" + (i + 1), null));
+ edit.putString("websearch_cookies_" + i, prefs.getString("websearch_cookies_" + (i + 1), null));
+ }
+
+ // Remove the last settings, of which we are now sure are no longer required
+ edit.remove("websearch_name_" + max);
+ edit.remove("websearch_baseurl_" + max);
+ edit.remove("websearch_cookies_" + max);
+ edit.commit();
+
+ }
+
+ /**
+ * Returns all available user-configured RSS feeds
+ * @return A list of all stored RSS feed settings objects
+ */
+ public List getRssfeedSettings() {
+ List rssfeeds = new ArrayList();
+ for (int i = 0; i <= getMaxRssfeed(); i++) {
+ rssfeeds.add(getRssfeedSetting(i));
+ }
+ return Collections.unmodifiableList(rssfeeds);
+ }
+
+ /**
+ * Returns the order number/identifying key of the last stored RSS feed
+ * @return The zero-based order number (index) of the last stored RSS feed
+ */
+ public int getMaxRssfeed() {
+ for (int i = 0; true; i++) {
+ if (prefs.getString("rssfeed_url_" + i, null) == null)
+ return i - 1;
+ }
+ }
+
+ /**
+ * Returns the user-specified RSS feed setting for a specific feed
+ * @param order The order number/identifying key of the settings to retrieve
+ * @return The RSS feed settings object, loaded from shared preferences
+ */
+ public RssfeedSetting getRssfeedSetting(int order) {
+ // @formatter:off
+ long lastViewed = prefs.getLong("rssfeed_lastviewed_" + order, -1);
+ return new RssfeedSetting(order,
+ prefs.getString("rssfeed_name_" + order, null),
+ prefs.getString("rssfeed_url_" + order, null),
+ prefs.getBoolean("rssfeed_reqauth_" + order, false),
+ lastViewed == -1L ? null: new Date(lastViewed));
+ // @formatter:on
+ }
+
+ /**
+ * Removes all settings related to a configured RSS feed. Since feeds are ordered, the order of the remaining feeds
+ * will be updated accordingly.
+ * @param order The identifying order number/key of the settings to remove
+ */
+ public void removeRssfeedSettings(int order) {
+ if (prefs.getString("rssfeed_url_" + order, null) == null)
+ return; // The settings that were requested to be removed do not exist
+
+ // Copy all settings higher than the supplied order number to the previous spot
+ Editor edit = prefs.edit();
+ int max = getMaxRssfeed();
+ for (int i = order; i < max; i++) {
+ edit.putString("rssfeed_name_" + i, prefs.getString("rssfeed_name_" + (i + 1), null));
+ edit.putString("rssfeed_url_" + i, prefs.getString("rssfeed_url_" + (i + 1), null));
+ edit.putBoolean("rssfeed_reqauth_" + i, prefs.getBoolean("rssfeed_reqauth_" + (i + 1), false));
+ edit.putLong("rssfeed_lastviewed_" + i, prefs.getLong("rssfeed_lastviewed_" + (i + 1), -1));
+ }
+
+ // Remove the last settings, of which we are now sure are no longer required
+ edit.remove("rssfeed_name_" + max);
+ edit.remove("rssfeed_url_" + max);
+ edit.remove("rssfeed_reqauth_" + max);
+ edit.remove("rssfeed_lastviewed_" + max);
+ edit.commit();
+
+ }
+
+ /**
+ * Registers for some RSS feed (as identified by its order numbe/key) the last date and time that it was viewed by
+ * the user. This is used to determine which items in an RSS feed are 'new'. Warning: any previously retrieved
+ * {@link RssfeedSetting} object is now no longer in sync, as this will not automatically be updated in the object.
+ * Use {@link #getRssfeedSetting(int)} to get fresh data.
+ * @param order The identifying order number/key of the settings of te RSS feed that was viewed
+ * @param lastViewed The date and time that the feed was last viewed; typically now
+ */
+ public void setRssfeedLastViewer(int order, Date lastViewed) {
+ if (prefs.getString("rssfeed_url_" + order, null) == null)
+ return; // The settings that were requested to be removed do not exist
+ prefs.edit().putLong("rssfeed_lastviewed_" + order, lastViewed.getTime()).commit();
+ }
+
+ /**
+ * Registers the torrents list sort order as being last used by the user
+ * @param currentSortOrder The sort order property the user selected last
+ * @param currentSortAscending The sort order direction that was last used
+ */
+ public void setLastUsedSortOrder(TorrentsSortBy currentSortOrder, boolean currentSortAscending) {
+ prefs.edit().putInt("system_lastusedsortorder", currentSortOrder.getCode()).commit();
+ prefs.edit().putBoolean("system_lastusedsortdirection", currentSortAscending).commit();
+ }
+
+ /**
+ * Returns the sort order property that the user last used. Use together with {@link #getLastUsedSortDescending()}
+ * to get the full last used sort settings.
+ * @return The last used sort order enumeration value
+ */
+ public TorrentsSortBy getLastUsedSortOrder() {
+ return TorrentsSortBy
+ .getStatus(prefs.getInt("system_lastusedsortorder", TorrentsSortBy.Alphanumeric.getCode()));
+ }
+
+ /**
+ * Returns the sort order direction that the user last used. Use together with {@link #getLastUsedSortOrder()} to
+ * get the full last used sort settings.
+ * @return True if the last used sort direction was descending, false otherwise (i.e. the default ascending
+ * direction)
+ */
+ public boolean getLastUsedSortDescending() {
+ return prefs.getBoolean("system_lastusedsortdirection", false);
+ }
+
+ /**
+ * Returns the list of all available in-app search sites as well as all web searches that the user configured.
+ * @return A list of search settings, all of which are either a {@link SearchSite} or {@link WebsearchSetting}
+ */
+ public List getSearchSettings() {
+ List all = new ArrayList();
+ all.addAll(searchHelper.getAvailableSites());
+ all.addAll(getWebsearchSettings());
+ return Collections.unmodifiableList(all);
+ }
+
+ /**
+ * Returns the settings of the search site that was last used by the user or was selected by the user as default
+ * site in the main settings. As opposed to getLastUsedSearchSiteKey(int), this method checks whether a site was
+ * already registered as being last used (or set as default) and checks whether the site still exists. It returns
+ * the first in-app search site if that fails.
+ * @return A site settings object of the last used server (or, if not known, the first server), or null if no
+ * servers exist
+ */
+ public SearchSetting getLastUsedSearchSite() {
+ String lastKey = getLastUsedSearchSiteKey();
+ List allsites = searchHelper.getAvailableSites();
+ int lastWebsearch = -1;
+ try {
+ lastWebsearch = Integer.parseInt(lastKey);
+ } catch (Exception e) {
+ // Not an in-app search site, but probably an in-app search
+ }
+
+ if (lastKey == null) {
+ // No site yet set specified; return the first in-app one, if available
+ if (allsites != null) {
+ return allsites.get(0);
+ }
+ return null;
+ }
+
+ if (lastWebsearch >= 0) {
+ // The last used site should be a user-configured web search site
+ int max = getMaxWebsearch(); // Zero-based index, so with max == 0 there is 1 server
+ if (max < 0 || lastWebsearch > max) {
+ // No web search sites configured
+ return null;
+ }
+ return getWebsearchSetting(lastWebsearch);
+ }
+
+ // Should be an in-app search key
+ if (allsites != null) {
+ for (SearchSite searchSite : allsites) {
+ if (searchSite.getKey().equals(lastKey)) {
+ return searchSite;
+ }
+ }
+ // Not found at all; probably a no longer existing web search; return the first in-app one
+ return allsites.get(0);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the unique key of the site that the used last used or selected as default form the main settings; use
+ * with getLastUsedSearchSite directly. WARNING: the returned string may no longer refer to a known web search site
+ * or in-app search settings object.
+ * @return A string indicating the key of the last used search site, or null if no site was yet used or set as
+ * default
+ */
+ private String getLastUsedSearchSiteKey() {
+ return prefs.getString("header_setsearchsite", null);
+ }
+
+ /**
+ * Registers the unique key of some web search or in-app search site as being last used by the user
+ * @param order The key identifying the specific server
+ */
+ public void setLastUsedSearchSite(SearchSite site) {
+ prefs.edit().putString("header_setsearchsite", site.getKey()).commit();
+ }
+
+ /**
+ * Returns the statistics of this server as it was last seen by the background server checker service.
+ * @param server The server for which to retrieved the statistics from the stored preferences
+ * @return A JSON array of JSON objects, each which represent a since torrent
+ */
+ public JSONArray getServerLastStats(ServerSetting server) {
+ String lastStats = prefs.getString(server.getUniqueIdentifier(), null);
+ if (lastStats == null)
+ return null;
+ try {
+ return new JSONArray(lastStats);
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Stores the now-last seen statistics of the supplied server by the background server checker service to the
+ * internal stored preferences.
+ * @param server The server to which the statistics apply to
+ * @param lastStats A JSON array of JSON objects that each represent a single seen torrent
+ */
+ public void setServerLastStats(ServerSetting server, JSONArray lastStats) {
+ prefs.edit().putString(server.getUniqueIdentifier(), lastStats.toString()).commit();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/app/settings/NotificationSettings.java b/core/src/org/transdroid/core/app/settings/NotificationSettings.java
new file mode 100644
index 00000000..44368c62
--- /dev/null
+++ b/core/src/org/transdroid/core/app/settings/NotificationSettings.java
@@ -0,0 +1,106 @@
+package org.transdroid.core.app.settings;
+
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.EBean.Scope;
+import org.androidannotations.annotations.RootContext;
+import org.transdroid.core.R;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+
+/**
+ * Allows instantiation of the settings specified in R.xml.pref_notifications.
+ * @author Eric Kok
+ */
+@EBean(scope = Scope.Singleton)
+public class NotificationSettings {
+
+ @RootContext
+ protected Context context;
+ private SharedPreferences prefs;
+
+ protected NotificationSettings(Context context) {
+ prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ /**
+ * Whether the background service is enabled, i.e. whether the user want to receive notifications
+ * @return True if the server should be checked for torrent status updates
+ */
+ public boolean isEnabled() {
+ return prefs.getBoolean("notifications_enabled", true);
+ }
+
+ private String getRawInverval() {
+ return prefs.getString("notifications_interval", "10800");
+ }
+
+ /**
+ * Returns the interval between two server checks
+ * @return The interval, in milliseconds
+ */
+ public Long getInvervalInMilliseconds() {
+ return Long.parseLong(getRawInverval()) * 1000L;
+ }
+
+ private String getRawSound() {
+ return prefs.getString("notifications_sound", null);
+ }
+
+ /**
+ * Returns the sound (ring tone) to play on a new notification, or null if it should not play any
+ * @return Either the user-specified sound, null if the user specified 'Silent' or the system default notification sound
+ */
+ public Uri getSound() {
+ String raw = getRawSound();
+ if (raw == null)
+ return null;
+ if (raw.equals(""))
+ return Settings.System.DEFAULT_NOTIFICATION_URI;
+ return Uri.parse(raw);
+ }
+
+ /**
+ * Whether the device should vibrate on a new notification
+ * @return
+ */
+ public boolean shouldVibrate() {
+ return prefs.getBoolean("notifications_vibrate", false);
+ }
+
+ /**
+ * Returns the default vibrate pattern to use if the user enabled notification vibrations; check
+ * {@link #shouldVibrate()},
+ * @return A unique pattern for vibrations in Transdroid
+ */
+ public long[] getDefaultVibratePattern() {
+ return new long[]{100, 100, 200, 300, 400, 700}; // Unique pattern?
+ }
+
+ private int getRawLedColour() {
+ return prefs.getInt("notifications_ledcolour", -1);
+ }
+
+ /**
+ * Returns the LED colour to use on a new notification
+ * @return The integer value of the user-specified or default colour
+ */
+ public int getDesiredLedColour() {
+ int raw = getRawLedColour();
+ if (raw <= 0)
+ return context.getResources().getColor(R.color.ledgreen);
+ return raw;
+ }
+
+ /**
+ * Whether the background service should report to ADW Launcher
+ * @return True if the user want Transdroid to report to ADW Launcher
+ */
+ public boolean shouldReportToAdwLauncher() {
+ return prefs.getBoolean("notifications_adwnotify", false);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/app/settings/RssfeedSetting.java b/core/src/org/transdroid/core/app/settings/RssfeedSetting.java
new file mode 100644
index 00000000..462b6156
--- /dev/null
+++ b/core/src/org/transdroid/core/app/settings/RssfeedSetting.java
@@ -0,0 +1,75 @@
+package org.transdroid.core.app.settings;
+
+import java.util.Date;
+
+import org.transdroid.core.gui.lists.SimpleListItem;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+/**
+ * Represents a user-specified RSS feed.
+ * @author Eric Kok
+ */
+public class RssfeedSetting implements SimpleListItem {
+
+ private static final String DEFAULT_NAME = "Default";
+
+ private final int order;
+ private final String name;
+ private final String url;
+ private final boolean requiresAuth;
+ private Date lastViewed;
+
+ public RssfeedSetting(int order, String name, String baseUrl, boolean needsAuth, Date lastViewed) {
+ this.order = order;
+ this.name = name;
+ this.url = baseUrl;
+ this.requiresAuth = needsAuth;
+ this.lastViewed = lastViewed;
+ }
+
+ public int getOrder() {
+ return order;
+ }
+
+ @Override
+ public String getName() {
+ if (!TextUtils.isEmpty(name))
+ return name;
+ if (!TextUtils.isEmpty(url)) {
+ String host = Uri.parse(url).getHost();
+ return host == null ? DEFAULT_NAME : host;
+ }
+ return DEFAULT_NAME;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public boolean requiresExternalAuthentication() {
+ return requiresAuth;
+ }
+
+ /**
+ * Returns the URL of the item that was the newest last time we checked this feed. Note that this is NOT updated
+ * automatically after the settings were loaded from {@link ApplicationSettings}; instead the settings have to be
+ * manually loaded again using {@link ApplicationSettings#getRssfeedSetting(int)}.
+ * @return The last new item's URL as URL-encoded string
+ */
+ public Date getLastViewed() {
+ return this.lastViewed;
+ }
+
+ /**
+ * Returns a nicely formatted identifier containing (a portion of) the feed URL
+ * @return A string to identify this feed's URL
+ */
+ public String getHumanReadableIdentifier() {
+ String host = Uri.parse(url).getHost();
+ String path = Uri.parse(url).getPath();
+ return (host == null ? null : host + (path == null ? "" : path));
+ }
+
+}
diff --git a/core/src/org/transdroid/core/app/settings/ServerSetting.java b/core/src/org/transdroid/core/app/settings/ServerSetting.java
new file mode 100644
index 00000000..fdd29b81
--- /dev/null
+++ b/core/src/org/transdroid/core/app/settings/ServerSetting.java
@@ -0,0 +1,253 @@
+package org.transdroid.core.app.settings;
+
+import org.transdroid.core.gui.lists.SimpleListItem;
+import org.transdroid.daemon.Daemon;
+import org.transdroid.daemon.DaemonSettings;
+import org.transdroid.daemon.IDaemonAdapter;
+import org.transdroid.daemon.OS;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+/**
+ * Represents a user-configured remote server.
+ * @author Eric Kok
+ */
+public class ServerSetting implements SimpleListItem {
+
+ private static final String DEFAULT_NAME = "Default";
+
+ private final int key;
+ private final String name;
+ private final Daemon type;
+ private final String address;
+ private final String localAddress;
+ private final String localNetwork;
+ private final int port;
+ private final String folder;
+ private final boolean useAuthentication;
+ private final String username;
+ private final String password;
+ private final String extraPass;
+ private final OS os;
+ private final String downloadDir;
+ private final String ftpUrl;
+ private final String ftpPassword;
+ private final int timeout;
+ private final boolean alarmOnFinishedDownload;
+ private final boolean alarmOnNewTorrent;
+ private final boolean ssl;
+ private final boolean sslTrustAll;
+ private final String sslTrustKey;
+ private final boolean isAutoGenerated;
+
+ /**
+ * Creates a daemon settings instance, providing full connection details
+ * @param name A name used to identify this server to the user
+ * @param type The server daemon type
+ * @param address The server domain name or IP address
+ * @param localAddress The server domain or IP address when connected to the server's local network
+ * @param localNetwork The server's local network SSID
+ * @param port The port on which the server daemon is running
+ * @param sslTrustKey The specific key that will be accepted.
+ * @param folder The server folder (like a virtual sub-folder or an SCGI mount point)
+ * @param useAuthentication Whether to use basic authentication
+ * @param username The user name to provide during authentication
+ * @param password The password to provide during authentication
+ * @param extraPass The Deluge web interface password
+ * @param downloadDir The default download directory (which may also be used as base directory for file paths)
+ * @param ftpUrl The partial URL to connect to when requesting FTP-style transfers
+ * @param timeout The number of seconds to wait before timing out a connection attempt
+ * @param isAutoGenerated Whether this setting was generated rather than manually inputed by the user
+ */
+ public ServerSetting(int key, String name, Daemon type, String address, String localAddress, String localNetwork,
+ int port, boolean ssl, boolean sslTrustAll, String sslTrustKey, String folder, boolean useAuthentication,
+ String username, String password, String extraPass, OS os, String downloadDir, String ftpUrl,
+ String ftpPassword, int timeout, boolean alarmOnFinishedDownload, boolean alarmOnNewTorrent,
+ boolean isAutoGenerated) {
+ this.key = key;
+ this.name = name;
+ this.type = type;
+ this.address = address;
+ this.localAddress = localAddress;
+ this.localNetwork = localNetwork;
+ this.port = port;
+ this.ssl = ssl;
+ this.sslTrustAll = sslTrustAll;
+ this.sslTrustKey = sslTrustKey;
+ this.folder = folder;
+ this.useAuthentication = useAuthentication;
+ this.username = username;
+ this.password = password;
+ this.extraPass = extraPass;
+ this.os = os;
+ this.downloadDir = downloadDir;
+ this.ftpUrl = ftpUrl;
+ this.ftpPassword = ftpPassword;
+ this.timeout = timeout;
+ this.alarmOnFinishedDownload = alarmOnFinishedDownload;
+ this.alarmOnNewTorrent = alarmOnNewTorrent;
+ this.isAutoGenerated = isAutoGenerated;
+ }
+
+ @Override
+ public String getName() {
+ if (!TextUtils.isEmpty(name))
+ return name;
+ if (!TextUtils.isEmpty(address)) {
+ String host = Uri.parse(address).getHost();
+ return host == null? DEFAULT_NAME: host;
+ }
+ return DEFAULT_NAME;
+ }
+
+ public Daemon getType() {
+ return type;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public String getLocalAddress() {
+ return localAddress;
+ }
+
+ public String getLocalNetwork() {
+ return localNetwork;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public boolean getSsl() {
+ return ssl;
+ }
+
+ public boolean getSslTrustAll() {
+ return sslTrustAll;
+ }
+
+ public String getSslTrustKey() {
+ return sslTrustKey;
+ }
+
+ public String getFolder() {
+ return folder;
+ }
+
+ public boolean shouldUseAuthentication() {
+ return useAuthentication;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public String getExtraPassword() {
+ return extraPass;
+ }
+
+ public OS getOS() {
+ return os;
+ }
+
+ public String getDownloadDir() {
+ return downloadDir;
+ }
+
+ public String getFtpUrl() {
+ return ftpUrl;
+ }
+
+ public String getFtpPassword() {
+ return ftpPassword;
+ }
+
+ public int getTimeoutInMilliseconds() {
+ return timeout * 1000;
+ }
+
+ public boolean shouldAlarmOnFinishedDownload() {
+ return alarmOnFinishedDownload;
+ }
+
+ public boolean shouldAlarmOnNewTorrent() {
+ return alarmOnNewTorrent;
+ }
+
+ public boolean isAutoGenerated() {
+ return isAutoGenerated;
+ }
+
+ public int getOrder() {
+ return this.key;
+ }
+
+ /**
+ * Returns a string that the user can use to identify the server by internal settings (rather than the name).
+ * @return A human-readable identifier in the form [https://]username@address:port/folder
+ */
+ public String getHumanReadableIdentifier() {
+ if (isAutoGenerated) {
+ // Hide the 'implementation details'; just give the username and server
+ return (this.shouldUseAuthentication() && !TextUtils.isEmpty(this.getUsername()) ? this.getUsername() + "@"
+ : "") + getAddress();
+ }
+ return (this.ssl ? "https://" : "http://")
+ + (this.shouldUseAuthentication() && !TextUtils.isEmpty(this.getUsername()) ? this.getUsername() + "@"
+ : "") + getAddress() + ":" + getPort()
+ + (Daemon.supportsCustomFolder(getType()) && getFolder() != null ? getFolder() : "");
+ }
+
+ /**
+ * Returns a string that acts as a unique identifier for this server, non-depending on the internal storage
+ * order/index. THis may be used to store additional details about this server elsewhere. It may change if the user
+ * changes server settings, but not with name or notification settings.
+ * @return A unique identifying string, based primarily on the configured address, port number, SSL settings and
+ * user name; returns null if the server is not yet fully identifiable (during configuration, for example)
+ */
+ public String getUniqueIdentifier() {
+ if (getType() == null || getAddress() == null || getAddress().equals(""))
+ return null;
+ return getType().toString() + "|" + getHumanReadableIdentifier();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof ServerSetting) {
+ // Directly compare order numbers/unique keys
+ return ((ServerSetting) o).getOrder() == this.key;
+ } else if (o instanceof DaemonSettings) {
+ // Old-style DaemonSettings objects can be equal if they were constructed from a ServerSettings object:
+ // idString should reflect the local key/order
+ return ((DaemonSettings) o).getIdString().equals(Integer.toString(this.key));
+ }
+ // Other objects are never equal to this
+ return false;
+ }
+
+ /**
+ * Returns the appropriate daemon adapter to which tasks can be executed, in accordance with this server's settings
+ * @return An IDaemonAdapter instance of the specific torrent client daemon type
+ */
+ public IDaemonAdapter createServerAdapter() {
+ return type.createAdapter(convertToDaemonSettings());
+ }
+
+ private DaemonSettings convertToDaemonSettings() {
+ // Convert local server settings into an old-style DaemonSetting object
+ // The local integer key is converted to the idString string
+ // TODO: Add localaddress and localnetwork to DaemonSettings, or solve properly rework the Connect library
+ // handling of settings
+ return new DaemonSettings(name, type, address, port, ssl, sslTrustAll, sslTrustKey,
+ folder, useAuthentication, username, password, extraPass, os, downloadDir, ftpUrl, ftpPassword,
+ timeout, alarmOnFinishedDownload, alarmOnNewTorrent, Integer.toString(key), isAutoGenerated);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/app/settings/SettingsPersistence.java b/core/src/org/transdroid/core/app/settings/SettingsPersistence.java
new file mode 100644
index 00000000..620b67d8
--- /dev/null
+++ b/core/src/org/transdroid/core/app/settings/SettingsPersistence.java
@@ -0,0 +1,269 @@
+package org.transdroid.core.app.settings;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileWriter;
+import java.io.IOException;
+
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.EBean.Scope;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.transdroid.daemon.util.HttpHelper;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.os.Environment;
+
+/**
+ * Singleton class that can persist user settings (servers, RSS feeds, etc.) to and from a plain text JSON file.
+ *
+ * @author Eric Kok
+ */
+@EBean(scope = Scope.Singleton)
+public class SettingsPersistence {
+
+ @Bean
+ protected ApplicationSettings applicationSettings;
+ @Bean
+ protected SystemSettings systemSettings;
+
+ public static final String DEFAULT_SETTINGS_DIR = Environment.getExternalStorageDirectory().toString()
+ + "/Transdroid";
+ public static final String DEFAULT_SETTINGS_FILENAME = "/settings.json";
+ public static final File DEFAULT_SETTINGS_FILE = new File(DEFAULT_SETTINGS_DIR + DEFAULT_SETTINGS_FILENAME);
+
+ /**
+ * Synchronously reads the server, web searches, RSS feed, background service and system settings from a file in
+ * JSON format.
+ * @param settingsFile The local file to write the settings to
+ * @throws FileNotFoundException Thrown when the settings file doesn't exist or couln't be read
+ * @throws JSONException Thrown when the file did not contain valid JSON content
+ */
+ public void importSettings(SharedPreferences prefs, File settingsFile) throws FileNotFoundException,
+ JSONException {
+
+ Editor editor = prefs.edit();
+
+ // Read the settings file
+ String raw = HttpHelper.convertStreamToString(new FileInputStream(settingsFile));
+ JSONObject json = new JSONObject(raw);
+
+ // Import servers
+ if (json.has("servers")) {
+ JSONArray servers = json.getJSONArray("servers");
+ for (int i = 0; i < servers.length(); i++) {
+ JSONObject server = servers.getJSONObject(i);
+ String postfix = Integer.toString(applicationSettings.getMaxServer() + 1 + i);
+
+ if (server.has("name"))
+ editor.putString("server_name_" + postfix, server.getString("name"));
+ if (server.has("type"))
+ editor.putString("server_type_" + postfix, server.getString("type"));
+ if (server.has("host"))
+ editor.putString("server_address_" + postfix, server.getString("host"));
+ if (server.has("local_network"))
+ editor.putString("server_localnetwork_" + postfix, server.getString("local_network"));
+ if (server.has("local_host"))
+ editor.putString("server_localaddress_" + postfix, server.getString("local_host"));
+ if (server.has("port"))
+ editor.putString("server_port_" + postfix, server.getString("port"));
+ if (server.has("ssl"))
+ editor.putBoolean("server_sslenabled_" + postfix, server.getBoolean("ssl"));
+ if (server.has("ssl_accept_all"))
+ editor.putBoolean("server_ssltrustall_" + postfix, server.getBoolean("ssl_accept_all"));
+ if (server.has("ssl_trust_key"))
+ editor.putString("server_ssltrustkey_" + postfix, server.getString("ssl_trust_key"));
+ if (server.has("folder"))
+ editor.putString("server_folder_" + postfix, server.getString("folder"));
+ if (server.has("use_auth"))
+ editor.putBoolean("server_useauth_" + postfix, server.getBoolean("use_auth"));
+ if (server.has("username"))
+ editor.putString("server_user_" + postfix, server.getString("username"));
+ if (server.has("password"))
+ editor.putString("server_pass_" + postfix, server.getString("password"));
+ if (server.has("extra_password"))
+ editor.putString("server_extrapass_" + postfix, server.getString("extra_password"));
+ if (server.has("os_type"))
+ editor.putString("server_os_" + postfix, server.getString("os_type"));
+ if (server.has("downloads_dir"))
+ editor.putString("server_downloaddir_" + postfix, server.getString("downloads_dir"));
+ if (server.has("base_ftp_url"))
+ editor.putString("server_ftpurl_" + postfix, server.getString("base_ftp_url"));
+ if (server.has("ftp_password"))
+ editor.putString("server_ftppass_" + postfix, server.getString("ftp_password"));
+ if (server.has("server_timeout"))
+ editor.putString("server_timeout_" + postfix, server.getString("server_timeout"));
+ if (server.has("download_alarm"))
+ editor.putBoolean("server_alarmfinished_" + postfix, server.getBoolean("download_alarm"));
+ if (server.has("new_torrent_alarm"))
+ editor.putBoolean("server_alarmnew_" + postfix, server.getBoolean("new_torrent_alarm"));
+
+ }
+ }
+
+ // Import web search sites
+ if (json.has("websites")) {
+ JSONArray sites = json.getJSONArray("websites");
+ for (int i = 0; i < sites.length(); i++) {
+ JSONObject site = sites.getJSONObject(i);
+ String postfix = Integer.toString(applicationSettings.getMaxWebsearch() + 1 + i);
+
+ if (site.has("name"))
+ editor.putString("websearch_name_" + postfix, site.getString("name"));
+ if (site.has("url"))
+ editor.putString("websearch_baseurl_" + postfix, site.getString("url"));
+
+ }
+ }
+
+ // Import RSS feeds
+ if (json.has("rssfeeds")) {
+ JSONArray feeds = json.getJSONArray("rssfeeds");
+ for (int i = 0; i < feeds.length(); i++) {
+ JSONObject feed = feeds.getJSONObject(i);
+ String postfix = Integer.toString(applicationSettings.getMaxRssfeed() + 1 + i);
+
+ if (feed.has("name"))
+ editor.putString("rssfeed_name_" + postfix, feed.getString("name"));
+ if (feed.has("url"))
+ editor.putString("rssfeed_url_" + postfix, feed.getString("url"));
+ if (feed.has("needs_auth"))
+ editor.putBoolean("rssfeed_reqauth_" + postfix, feed.getBoolean("needs_auth"));
+ if (feed.has("last_seen"))
+ editor.putString("rssfeed_lastnew_" + postfix, feed.getString("last_seen"));
+
+ }
+ }
+
+ // Import background service and system settings
+ if (json.has("alarm_enabled"))
+ editor.putBoolean("notifications_enabled", json.getBoolean("alarm_enabled"));
+ if (json.has("alarm_interval"))
+ editor.putString("notifications_interval", json.getString("alarm_interval"));
+ if (json.has("alarm_sound_uri"))
+ editor.putString("notifications_sound", json.getString("alarm_sound_uri"));
+ if (json.has("alarm_vibrate"))
+ editor.putBoolean("notifications_vibrate", json.getBoolean("alarm_vibrate"));
+ if (json.has("alarm_ledcolour"))
+ editor.putInt("notifications_ledcolour", json.getInt("alarm_ledcolour"));
+ if (json.has("alarm_adwnotifications"))
+ editor.putBoolean("notifications_adwnotify", json.getBoolean("alarm_adwnotifications"));
+ if (json.has("system_checkupdates"))
+ editor.putBoolean("system_checkupdates", json.getBoolean("system_checkupdates"));
+ if (json.has("system_usedarktheme"))
+ editor.putBoolean("system_usedarktheme", json.getBoolean("system_usedarktheme"));
+
+ editor.commit();
+
+ }
+
+ /**
+ * Synchronously writes the server, web searches, RSS feed, background service and system settings to a file in JSON
+ * format.
+ * @param prefs The application-global preferences object to write settings to
+ * @param settingsFile The local file to read the settings from
+ * @throws JSONException Thrown when the JSON content could not be constructed properly
+ * @throws IOException Thrown when the settings file could not be created or written to
+ */
+ public void exportSettings(SharedPreferences prefs, File settingsFile) throws JSONException, IOException {
+
+ // Create a single JSON object that will contain all settings
+ JSONObject json = new JSONObject();
+
+ // Convert server settings into JSON
+ JSONArray servers = new JSONArray();
+ int i = 0;
+ String postfixi = "0";
+ while (prefs.contains("server_type_" + postfixi)) {
+
+ JSONObject server = new JSONObject();
+ server.put("name", prefs.getString("server_name_" + postfixi, null));
+ server.put("type", prefs.getString("server_type_" + postfixi, null));
+ server.put("host", prefs.getString("server_address_" + postfixi, null));
+ server.put("local_network", prefs.getString("server_localnetwork_" + postfixi, null));
+ server.put("local_host", prefs.getString("server_localaddress_" + postfixi, null));
+ server.put("port", prefs.getString("server_port_" + postfixi, null));
+ server.put("ssl", prefs.getBoolean("server_sslenabled_" + postfixi, false));
+ server.put("ssl_accept_all", prefs.getBoolean("server_ssltrustall_" + postfixi, false));
+ server.put("ssl_trust_key", prefs.getString("server_ssltrustkey_" + postfixi, null));
+ server.put("folder", prefs.getString("server_folder_" + postfixi, null));
+ server.put("use_auth", prefs.getBoolean("server_useauth_" + postfixi, true));
+ server.put("username", prefs.getString("server_user_" + postfixi, null));
+ server.put("password", prefs.getString("server_pass_" + postfixi, null));
+ server.put("extra_password", prefs.getString("server_extrapass_" + postfixi, null));
+ server.put("os_type", prefs.getString("server_os_" + postfixi, null));
+ server.put("downloads_dir", prefs.getString("server_downloaddir_" + postfixi, null));
+ server.put("base_ftp_url", prefs.getString("server_ftpurl_" + postfixi, null));
+ server.put("ftp_password", prefs.getString("server_ftppass_" + postfixi, null));
+ server.put("server_timeout", prefs.getString("server_timeout_" + postfixi, null));
+ server.put("download_alarm", prefs.getBoolean("server_alarmfinished_" + postfixi, false));
+ server.put("new_torrent_alarm", prefs.getBoolean("server_alarmnew_" + postfixi, false));
+
+ servers.put(server);
+ i++;
+ postfixi = Integer.toString(i);
+ }
+ json.put("servers", servers);
+
+ // Convert web search settings into JSON
+ JSONArray sites = new JSONArray();
+ int j = 0;
+ String postfixj = "0";
+ while (prefs.contains("websearch_baseurl_" + postfixj)) {
+
+ JSONObject site = new JSONObject();
+ site.put("name", prefs.getString("websearch_name_" + postfixj, null));
+ site.put("url", prefs.getString("websearch_baseurl_" + postfixj, null));
+
+ sites.put(site);
+ j++;
+ postfixj = Integer.toString(j);
+ }
+ json.put("websites", sites);
+
+ // Convert RSS feed settings into JSON
+ JSONArray feeds = new JSONArray();
+ int k = 0;
+ String postfixk = "0";
+ while (prefs.contains("rssfeed_url_" + postfixk)) {
+
+ JSONObject feed = new JSONObject();
+ feed.put("name", prefs.getString("rssfeed_name_" + postfixk, null));
+ feed.put("url", prefs.getString("rssfeed_url_" + postfixk, null));
+ feed.put("needs_auth", prefs.getBoolean("rssfeed_reqauth_" + postfixk, false));
+ feed.put("last_seen", prefs.getString("rssfeed_lastnew_" + postfixk, null));
+
+ feeds.put(feed);
+ k++;
+ postfixk = Integer.toString(k);
+ }
+ json.put("rssfeeds", feeds);
+
+ // Convert background service and system settings into JSON
+ json.put("alarm_enabled", prefs.getBoolean("notifications_enabled", true));
+ json.put("alarm_interval", prefs.getString("notifications_interval", null));
+ json.put("alarm_sound_uri", prefs.getString("notifications_sound", null));
+ json.put("alarm_vibrate", prefs.getBoolean("notifications_vibrate", false));
+ json.put("alarm_ledcolour", prefs.getInt("notifications_ledcolour", -1));
+ json.put("alarm_adwnotifications", prefs.getBoolean("notifications_adwnotify", false));
+ json.put("system_checkupdates", prefs.getBoolean("system_checkupdates", true));
+ json.put("system_usedarktheme", prefs.getBoolean("system_usedarktheme", false));
+
+ // Serialise the JSON object to a file
+ if (settingsFile.exists()) {
+ settingsFile.delete();
+ }
+ settingsFile.getParentFile().mkdirs();
+ settingsFile.createNewFile();
+ FileWriter writer = new FileWriter(settingsFile);
+ writer.write(json.toString(2));
+ writer.flush();
+ writer.close();
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/app/settings/SystemSettings.java b/core/src/org/transdroid/core/app/settings/SystemSettings.java
new file mode 100644
index 00000000..ccb38918
--- /dev/null
+++ b/core/src/org/transdroid/core/app/settings/SystemSettings.java
@@ -0,0 +1,34 @@
+package org.transdroid.core.app.settings;
+
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.RootContext;
+import org.androidannotations.annotations.EBean.Scope;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+/**
+ * Allows instantiation of the settings specified in R.xml.pref_system.
+ * @author Eric Kok
+ */
+@EBean(scope = Scope.Singleton)
+public class SystemSettings {
+
+ @RootContext
+ protected Context context;
+ private SharedPreferences prefs;
+
+ protected SystemSettings(Context context) {
+ prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ public boolean checkForUpdates() {
+ return prefs.getBoolean("system_checkupdates", true);
+ }
+
+ public boolean useDarkTheme() {
+ return prefs.getBoolean("system_usedarktheme", false);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/app/settings/WebsearchSetting.java b/core/src/org/transdroid/core/app/settings/WebsearchSetting.java
new file mode 100644
index 00000000..9d9a612f
--- /dev/null
+++ b/core/src/org/transdroid/core/app/settings/WebsearchSetting.java
@@ -0,0 +1,65 @@
+package org.transdroid.core.app.settings;
+
+import org.transdroid.core.gui.lists.SimpleListItem;
+import org.transdroid.core.gui.search.SearchSetting;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+/**
+ * Represents a user-specified website that can be searched (by starting the browser, rather than in-app)
+ * @author Eric Kok
+ */
+public class WebsearchSetting implements SimpleListItem, SearchSetting {
+
+ private static final String DEFAULT_NAME = "Default";
+ private static final String KEY_PREFIX = "websearch_";
+
+ private final int order;
+ private final String name;
+ private final String baseUrl;
+ private final String cookies;
+
+ public WebsearchSetting(int order, String name, String baseUrl, String cookies) {
+ this.order = order;
+ this.name = name;
+ this.baseUrl = baseUrl;
+ this.cookies = cookies;
+ }
+
+ public int getOrder() {
+ return order;
+ }
+
+ @Override
+ public String getName() {
+ if (!TextUtils.isEmpty(name))
+ return name;
+ if (!TextUtils.isEmpty(baseUrl)) {
+ String host = Uri.parse(baseUrl).getHost();
+ return host == null? DEFAULT_NAME: host;
+ }
+ return DEFAULT_NAME;
+ }
+
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ public String getCookies() {
+ return cookies;
+ }
+
+ public String getKey() {
+ return KEY_PREFIX + getOrder();
+ }
+
+ /**
+ * Returns a nicely formatted identifier containing (a portion of) the search base URL
+ * @return A string to identify this site's search URL
+ */
+ public String getHumanReadableIdentifier() {
+ return Uri.parse(baseUrl).getHost();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/DetailsActivity.java b/core/src/org/transdroid/core/gui/DetailsActivity.java
new file mode 100644
index 00000000..c7b7581e
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/DetailsActivity.java
@@ -0,0 +1,325 @@
+package org.transdroid.core.gui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.androidannotations.annotations.AfterViews;
+import org.androidannotations.annotations.Background;
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.Extra;
+import org.androidannotations.annotations.FragmentById;
+import org.androidannotations.annotations.InstanceState;
+import org.androidannotations.annotations.OptionsItem;
+import org.androidannotations.annotations.OptionsMenu;
+import org.androidannotations.annotations.UiThread;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.*;
+import org.transdroid.core.gui.lists.LocalTorrent;
+import org.transdroid.core.gui.log.Log;
+import org.transdroid.core.gui.navigation.Label;
+import org.transdroid.core.gui.navigation.NavigationHelper;
+import org.transdroid.daemon.Daemon;
+import org.transdroid.daemon.IDaemonAdapter;
+import org.transdroid.daemon.Priority;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.TorrentDetails;
+import org.transdroid.daemon.TorrentFile;
+import org.transdroid.daemon.task.DaemonTaskFailureResult;
+import org.transdroid.daemon.task.DaemonTaskResult;
+import org.transdroid.daemon.task.DaemonTaskSuccessResult;
+import org.transdroid.daemon.task.GetFileListTask;
+import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
+import org.transdroid.daemon.task.GetTorrentDetailsTask;
+import org.transdroid.daemon.task.GetTorrentDetailsTaskSuccessResult;
+import org.transdroid.daemon.task.PauseTask;
+import org.transdroid.daemon.task.RemoveTask;
+import org.transdroid.daemon.task.ResumeTask;
+import org.transdroid.daemon.task.RetrieveTask;
+import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
+import org.transdroid.daemon.task.SetDownloadLocationTask;
+import org.transdroid.daemon.task.SetFilePriorityTask;
+import org.transdroid.daemon.task.SetLabelTask;
+import org.transdroid.daemon.task.SetTrackersTask;
+import org.transdroid.daemon.task.StartTask;
+import org.transdroid.daemon.task.StopTask;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+
+import de.keyboardsurfer.android.widget.crouton.Crouton;
+
+/**
+ * An activity that holds a single torrents details fragment. It is used on devices (i.e. phones) where there is no room
+ * to show details in the {@link TorrentsActivity} directly. Task execution, such as loading of more details and
+ * updating file priorities, is performed in this activity via background methods.
+ * @author Eric Kok
+ */
+@EActivity(resName = "activity_details")
+@OptionsMenu(resName = "activity_details")
+public class DetailsActivity extends SherlockFragmentActivity implements TorrentTasksExecutor {
+
+ @Extra
+ @InstanceState
+ protected Torrent torrent;
+ @Extra
+ @InstanceState
+ protected ArrayList currentLabels;
+
+ // Settings
+ @Bean
+ protected NavigationHelper navigationHelper;
+ @Bean
+ protected ApplicationSettings applicationSettings;
+ private IDaemonAdapter currentConnection = null;
+
+ // Details view components
+ @FragmentById(resName = "torrent_details")
+ protected DetailsFragment fragmentDetails;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ // Set the theme according to the user preference
+ if (SystemSettings_.getInstance_(this).useDarkTheme()) {
+ setTheme(R.style.TransdroidTheme_Dark);
+ getSupportActionBar().setIcon(R.drawable.ic_activity_torrents);
+ }
+ super.onCreate(savedInstanceState);
+ }
+
+ @AfterViews
+ protected void init() {
+
+ // We require a torrent to be specified; otherwise close the activity
+ if (torrent == null || currentLabels == null) {
+ finish();
+ return;
+ }
+
+ // Simple action bar with up, torrent name as title and refresh button
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(torrent.getName()));
+
+ // Connect to the last used server
+ ServerSetting lastUsed = applicationSettings.getLastUsedServer();
+ currentConnection = lastUsed.createServerAdapter();
+
+ // Show details and load fine stats and torrent files
+ fragmentDetails.updateTorrent(torrent);
+ fragmentDetails.updateLabels(currentLabels);
+
+ }
+
+ @Override
+ protected void onDestroy() {
+ Crouton.cancelAllCroutons();
+ super.onDestroy();
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @OptionsItem(android.R.id.home)
+ protected void navigateUp() {
+ TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
+ }
+
+ @OptionsItem(resName = "action_refresh")
+ protected void refreshScreen() {
+ fragmentDetails.updateIsLoading(true);
+ refreshTorrent();
+ refreshTorrentDetails(torrent);
+ refreshTorrentFiles(torrent);
+ }
+
+ @Background
+ protected void refreshTorrent() {
+ DaemonTaskResult result = RetrieveTask.create(currentConnection).execute();
+ if (result instanceof RetrieveTaskSuccessResult) {
+ onTorrentsRetrieved(((RetrieveTaskSuccessResult) result).getTorrents(),
+ ((RetrieveTaskSuccessResult) result).getLabels());
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result);
+ }
+ }
+
+ @Background
+ public void refreshTorrentDetails(Torrent torrent) {
+ if (!Daemon.supportsFineDetails(torrent.getDaemon()))
+ return;
+ DaemonTaskResult result = GetTorrentDetailsTask.create(currentConnection, torrent).execute();
+ if (result instanceof GetTorrentDetailsTaskSuccessResult) {
+ onTorrentDetailsRetrieved(torrent, ((GetTorrentDetailsTaskSuccessResult) result).getTorrentDetails());
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result);
+ }
+ }
+
+ @Background
+ public void refreshTorrentFiles(Torrent torrent) {
+ if (!Daemon.supportsFileListing(torrent.getDaemon()))
+ return;
+ DaemonTaskResult result = GetFileListTask.create(currentConnection, torrent).execute();
+ if (result instanceof GetFileListTaskSuccessResult) {
+ onTorrentFilesRetrieved(torrent, ((GetFileListTaskSuccessResult) result).getFiles());
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result);
+ }
+ }
+
+ @Background
+ @Override
+ public void resumeTorrent(Torrent torrent) {
+ torrent.mimicResume();
+ DaemonTaskResult result = ResumeTask.create(currentConnection, torrent).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_resumed, torrent.getName()));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result);
+ }
+ }
+
+ @Background
+ @Override
+ public void pauseTorrent(Torrent torrent) {
+ torrent.mimicPause();
+ DaemonTaskResult result = PauseTask.create(currentConnection, torrent).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_paused, torrent.getName()));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result);
+ }
+ }
+
+ @Background
+ @Override
+ public void startTorrent(Torrent torrent, boolean forced) {
+ torrent.mimicStart();
+ DaemonTaskResult result = StartTask.create(currentConnection, torrent, forced).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_started, torrent.getName()));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result);
+ }
+ }
+
+ @Background
+ @Override
+ public void stopTorrent(Torrent torrent) {
+ torrent.mimicStop();
+ DaemonTaskResult result = StopTask.create(currentConnection, torrent).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_stopped, torrent.getName()));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result);
+ }
+ }
+
+ @Background
+ @Override
+ public void removeTorrent(Torrent torrent, boolean withData) {
+ DaemonTaskResult result = RemoveTask.create(currentConnection, torrent, withData).execute();
+ if (result instanceof DaemonTaskResult) {
+ // Close the details activity (as the torrent is now removed)
+ closeActivity(getString(withData ? R.string.result_removed_with_data : R.string.result_removed,
+ torrent.getName()));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result);
+ }
+ }
+
+ @UiThread
+ protected void closeActivity(String closeText) {
+ finish();
+ if (closeText != null)
+ Toast.makeText(this, closeText, Toast.LENGTH_LONG).show();
+ }
+
+ @Background
+ @Override
+ public void updateLabel(Torrent torrent, String newLabel) {
+ torrent.mimicNewLabel(newLabel);
+ DaemonTaskResult result = SetLabelTask.create(currentConnection, torrent, newLabel == null ? "" : newLabel)
+ .execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_labelset, newLabel));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result);
+ }
+ }
+
+ @Background
+ @Override
+ public void updateTrackers(Torrent torrent, List newTrackers) {
+ DaemonTaskResult result = SetTrackersTask.create(currentConnection, torrent, newTrackers).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_trackersupdated));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result);
+ }
+ }
+
+ @Background
+ @Override
+ public void updateLocation(Torrent torrent, String newLocation) {
+ DaemonTaskResult result = SetDownloadLocationTask.create(currentConnection, torrent, newLocation).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_locationset, newLocation));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result);
+ }
+ }
+
+ @Background
+ @Override
+ public void updatePriority(Torrent torrent, List files, Priority priority) {
+ DaemonTaskResult result = SetFilePriorityTask.create(currentConnection, torrent, priority,
+ new ArrayList(files)).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_priotitiesset));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result);
+ }
+ }
+
+ @UiThread
+ protected void onTaskSucceeded(DaemonTaskSuccessResult result, String successMessage) {
+ // Refresh the screen as well
+ refreshTorrent();
+ refreshTorrentDetails(torrent);
+ Crouton.showText(this, successMessage, NavigationHelper.CROUTON_INFO_STYLE);
+ }
+
+ @UiThread
+ protected void onTorrentDetailsRetrieved(Torrent torrent, TorrentDetails torrentDetails) {
+ // Update the details fragment with the new fine details for the shown torrent
+ fragmentDetails.updateTorrentDetails(torrent, torrentDetails);
+ }
+
+ @UiThread
+ protected void onTorrentFilesRetrieved(Torrent torrent, List torrentFiles) {
+ // Update the details fragment with the newly retrieved list of files
+ fragmentDetails.updateTorrentFiles(torrent, new ArrayList(torrentFiles));
+ }
+
+ @UiThread
+ protected void onCommunicationError(DaemonTaskFailureResult result) {
+ Log.i(this, result.getException().toString());
+ fragmentDetails.updateIsLoading(false);
+ Crouton.showText(this, getString(LocalTorrent.getResourceForDaemonException(result.getException())),
+ NavigationHelper.CROUTON_ERROR_STYLE);
+ }
+
+ @UiThread
+ protected void onTorrentsRetrieved(List torrents, List labels) {
+ // Update the details fragment accordingly
+ fragmentDetails.updateIsLoading(false);
+ fragmentDetails.perhapsUpdateTorrent(torrents);
+ fragmentDetails.updateLabels(Label.convertToNavigationLabels(labels,
+ getResources().getString(R.string.labels_unlabeled)));
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/DetailsFragment.java b/core/src/org/transdroid/core/gui/DetailsFragment.java
new file mode 100644
index 00000000..6aa035dd
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/DetailsFragment.java
@@ -0,0 +1,363 @@
+package org.transdroid.core.gui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.androidannotations.annotations.AfterViews;
+import org.androidannotations.annotations.EFragment;
+import org.androidannotations.annotations.InstanceState;
+import org.androidannotations.annotations.OptionsItem;
+import org.androidannotations.annotations.OptionsMenu;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.SystemSettings_;
+import org.transdroid.core.gui.lists.DetailsAdapter;
+import org.transdroid.core.gui.lists.SimpleListItemAdapter;
+import org.transdroid.core.gui.navigation.Label;
+import org.transdroid.core.gui.navigation.NavigationHelper;
+import org.transdroid.core.gui.navigation.NavigationHelper_;
+import org.transdroid.core.gui.navigation.SelectionManagerMode;
+import org.transdroid.core.gui.navigation.SetLabelDialog;
+import org.transdroid.core.gui.navigation.SetLabelDialog.OnLabelPickedListener;
+import org.transdroid.core.gui.navigation.SetTrackersDialog;
+import org.transdroid.core.gui.navigation.SetTrackersDialog.OnTrackersUpdatedListener;
+import org.transdroid.daemon.Daemon;
+import org.transdroid.daemon.Priority;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.TorrentDetails;
+import org.transdroid.daemon.TorrentFile;
+
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.actionbarsherlock.view.ActionMode;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.SherlockListView;
+import com.actionbarsherlock.view.SherlockListView.MultiChoiceModeListenerCompat;
+
+import de.keyboardsurfer.android.widget.crouton.Crouton;
+
+/**
+ * Fragment that shows detailed statistics about some torrent. These come from some already fetched {@link Torrent}
+ * object, but it also retrieves further detailed statistics. The actual execution of tasks is performed by the activity
+ * that contains this fragment, as per the {@link TorrentTasksExecutor} interface.
+ * @author Eric Kok
+ */
+@EFragment(resName = "fragment_details")
+@OptionsMenu(resName = "fragment_details")
+public class DetailsFragment extends SherlockFragment implements OnTrackersUpdatedListener, OnLabelPickedListener {
+
+ // Local data
+ @InstanceState
+ protected Torrent torrent = null;
+ @InstanceState
+ protected TorrentDetails torrentDetails = null;
+ @InstanceState
+ protected ArrayList torrentFiles = null;
+ @InstanceState
+ protected ArrayList currentLabels = null;
+ @InstanceState
+ protected boolean isLoadingTorrent = false;
+
+ // Views
+ @ViewById(resName = "details_list")
+ protected SherlockListView detailsList;
+ @ViewById
+ protected TextView emptyText;
+ @ViewById
+ protected ProgressBar loadingProgress;
+
+ @AfterViews
+ protected void init() {
+
+ // On large screens where this fragment is shown next to the torrents list, we show a continues grey vertical
+ // line to separate the lists visually
+ if (!NavigationHelper_.getInstance_(getActivity()).isSmallScreen()) {
+ if (SystemSettings_.getInstance_(getActivity()).useDarkTheme()) {
+ detailsList.setBackgroundResource(R.drawable.details_list_background_dark);
+ } else {
+ detailsList.setBackgroundResource(R.drawable.details_list_background_light);
+ }
+ }
+
+ // Set up details adapter (itself containing the actual lists to show), which allows multi-select and fast
+ // scrolling
+ detailsList.setAdapter(new DetailsAdapter(getActivity()));
+ detailsList.setMultiChoiceModeListener(onDetailsSelected);
+ detailsList.setFastScrollEnabled(true);
+ if (torrent != null)
+ updateTorrent(torrent);
+ if (torrentDetails != null)
+ updateTorrentDetails(torrent, torrentDetails);
+ if (torrentFiles != null)
+ updateTorrentFiles(torrent, torrentFiles);
+
+ }
+
+ /**
+ * Updates the details adapter header to show the new torrent data.
+ * @param newTorrent The new torrent object
+ */
+ public void updateTorrent(Torrent newTorrent) {
+ clear();
+ this.torrent = newTorrent;
+ ((DetailsAdapter) detailsList.getAdapter()).updateTorrent(newTorrent);
+ // Make the list (with details header) visible
+ detailsList.setVisibility(View.VISIBLE);
+ emptyText.setVisibility(View.GONE);
+ loadingProgress.setVisibility(View.GONE);
+ // Also update the available actions in the action bar
+ getActivity().supportInvalidateOptionsMenu();
+ // Refresh the detailed statistics (errors) and list of files
+ getTasksExecutor().refreshTorrentDetails(torrent);
+ getTasksExecutor().refreshTorrentFiles(torrent);
+ }
+
+ /**
+ * Updates the details adapter to show the list of trackers and tracker errors.
+ * @param checkTorrent The torrent for which the details were retrieved
+ * @param newTorrentDetails The new fine details object of some torrent
+ */
+ public void updateTorrentDetails(Torrent checkTorrent, TorrentDetails newTorrentDetails) {
+ // Check if these are actually the details of the torrent we are now showing
+ if (!torrent.getUniqueID().equals(checkTorrent.getUniqueID()))
+ return;
+ this.torrentDetails = newTorrentDetails;
+ ((DetailsAdapter) detailsList.getAdapter()).updateTrackers(SimpleListItemAdapter.SimpleStringItem
+ .wrapStringsList(newTorrentDetails.getTrackers()));
+ ((DetailsAdapter) detailsList.getAdapter()).updateErrors(SimpleListItemAdapter.SimpleStringItem
+ .wrapStringsList(newTorrentDetails.getErrors()));
+ }
+
+ /**
+ * Updates the list adapter to show a new list of torrent files, replacing the old files list.
+ * @param checkTorrent The torrent for which the details were retrieved
+ * @param newTorrents The new, updated list of torrent file objects
+ */
+ public void updateTorrentFiles(Torrent checkTorrent, ArrayList newTorrentFiles) {
+ // Check if these are actually the details of the torrent we are now showing
+ if (!torrent.getUniqueID().equals(checkTorrent.getUniqueID()))
+ return;
+ Collections.sort(newTorrentFiles);
+ this.torrentFiles = newTorrentFiles;
+ ((DetailsAdapter) detailsList.getAdapter()).updateTorrentFiles(newTorrentFiles);
+ }
+
+ /**
+ * Can be called if some outside activity returned new torrents, so we can perhaps piggyback on this by update our
+ * data as well.
+ * @param torrents The last of retrieved torrents
+ */
+ public void perhapsUpdateTorrent(List torrents) {
+ // Only try to update if we actually were showing a torrent
+ if (this.torrent == null || torrents == null)
+ return;
+ for (Torrent newTorrent : torrents) {
+ if (newTorrent.getUniqueID().equals(this.torrent.getUniqueID())) {
+ // Found, so we can update our data as well
+ updateTorrent(newTorrent);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Updates the locally maintained list of labels that are active on the server. Used in the label picking dialog and
+ * should be updated every time after the list of torrents was retrieved to keep it updated.
+ * @param currentLabels The list of known server labels
+ */
+ public void updateLabels(ArrayList currentLabels) {
+ this.currentLabels = new ArrayList(currentLabels);
+ }
+
+ /**
+ * Clear the screen by fully clearing the internal merge list (with header and other lists)
+ */
+ public void clear() {
+ detailsList.setAdapter(new DetailsAdapter(getActivity()));
+ detailsList.setVisibility(View.GONE);
+ emptyText.setVisibility(!isLoadingTorrent ? View.VISIBLE : View.GONE);
+ loadingProgress.setVisibility(isLoadingTorrent ? View.VISIBLE : View.GONE);
+ // Note: this.torrent is not cleared as we need to know later what the fragment was originally bound to
+ torrentDetails = null;
+ torrentFiles = null;
+ }
+
+ /**
+ * Updates the shown screen depending on whether the torrent is loading
+ * @param isLoading True if the torrent is (re)loading, false otherwise
+ */
+ public void updateIsLoading(boolean isLoading) {
+ this.isLoadingTorrent = isLoading;
+ if (isLoadingTorrent)
+ clear();
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ if (torrent == null) {
+ menu.findItem(R.id.action_resume).setVisible(false);
+ menu.findItem(R.id.action_pause).setVisible(false);
+ menu.findItem(R.id.action_start).setVisible(false);
+ menu.findItem(R.id.action_stop).setVisible(false);
+ menu.findItem(R.id.action_remove).setVisible(false);
+ menu.findItem(R.id.action_remove_withdata).setVisible(false);
+ menu.findItem(R.id.action_setlabel).setVisible(false);
+ menu.findItem(R.id.action_updatetrackers).setVisible(false);
+ return;
+ }
+ // Update action availability
+ boolean startStop = Daemon.supportsStoppingStarting(torrent.getDaemon());
+ menu.findItem(R.id.action_resume).setVisible(torrent.canResume());
+ menu.findItem(R.id.action_pause).setVisible(torrent.canPause());
+ menu.findItem(R.id.action_start).setVisible(startStop && torrent.canStart());
+ menu.findItem(R.id.action_stop).setVisible(startStop && torrent.canStop());
+ menu.findItem(R.id.action_remove).setVisible(true);
+ boolean removeWithData = Daemon.supportsRemoveWithData(torrent.getDaemon());
+ menu.findItem(R.id.action_remove_withdata).setVisible(removeWithData);
+ boolean setLabel = Daemon.supportsSetLabel(torrent.getDaemon());
+ menu.findItem(R.id.action_setlabel).setVisible(setLabel);
+ boolean setTrackers = Daemon.supportsSetTrackers(torrent.getDaemon());
+ menu.findItem(R.id.action_updatetrackers).setVisible(setTrackers);
+
+ }
+
+ @OptionsItem(resName = "action_resume")
+ protected void resumeTorrent() {
+ getTasksExecutor().resumeTorrent(torrent);
+ }
+
+ @OptionsItem(resName = "action_pause")
+ protected void pauseTorrent() {
+ getTasksExecutor().pauseTorrent(torrent);
+ }
+
+ @OptionsItem(resName = "action_start_default")
+ protected void startTorrentDefault() {
+ getTasksExecutor().startTorrent(torrent, false);
+ }
+
+ @OptionsItem(resName = "action_start_forced")
+ protected void startTorrentForced() {
+ getTasksExecutor().startTorrent(torrent, true);
+ }
+
+ @OptionsItem(resName = "action_stop")
+ protected void stopTorrent() {
+ getTasksExecutor().stopTorrent(torrent);
+ }
+
+ @OptionsItem(resName = "action_remove_default")
+ protected void removeTorrentDefault() {
+ getTasksExecutor().removeTorrent(torrent, false);
+ }
+
+ @OptionsItem(resName = "action_remove_withdata")
+ protected void removeTorrentWithData() {
+ getTasksExecutor().removeTorrent(torrent, true);
+ }
+
+ @OptionsItem(resName = "action_setlabel")
+ protected void setLabel() {
+ new SetLabelDialog().setOnLabelPickedListener(this).setCurrentLabels(currentLabels)
+ .show(getFragmentManager(), "SetLabelDialog");
+ }
+
+ @OptionsItem(resName = "action_updatetrackers")
+ protected void updateTrackers() {
+ new SetTrackersDialog().setOnTrackersUpdated(this).setCurrentTrackers(torrentDetails.getTrackersText())
+ .show(getFragmentManager(), "SetTrackersDialog");
+ }
+
+ @Override
+ public void onLabelPicked(String newLabel) {
+ getTasksExecutor().updateLabel(torrent, newLabel);
+ }
+
+ @Override
+ public void onTrackersUpdated(List updatedTrackers) {
+ getTasksExecutor().updateTrackers(torrent, updatedTrackers);
+ }
+
+ private MultiChoiceModeListenerCompat onDetailsSelected = new MultiChoiceModeListenerCompat() {
+
+ SelectionManagerMode selectionManagerMode;
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ // Show contextual action bar to start/stop/remove/etc. torrents in batch mode
+ mode.getMenuInflater().inflate(R.menu.fragment_details_cab, menu);
+ selectionManagerMode = new SelectionManagerMode(detailsList, R.plurals.navigation_filesselected);
+ selectionManagerMode.setOnlyCheckClass(TorrentFile.class);
+ selectionManagerMode.onCreateActionMode(mode, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return selectionManagerMode.onPrepareActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+
+ // Get checked torrents
+ List checked = new ArrayList();
+ for (int i = 0; i < detailsList.getCheckedItemPositions().size(); i++) {
+ if (detailsList.getCheckedItemPositions().valueAt(i)
+ && detailsList.getAdapter().getItem(detailsList.getCheckedItemPositions().keyAt(i)) instanceof TorrentFile)
+ checked.add((TorrentFile) detailsList.getAdapter().getItem(
+ detailsList.getCheckedItemPositions().keyAt(i)));
+ }
+
+ int itemId = item.getItemId();
+ if (itemId == R.id.action_download) {
+ // TODO: Start FTP download command for the selected torrents
+ Crouton.showText(getActivity(), "TODO: Start FTP download command for the selected torrents",
+ NavigationHelper.CROUTON_INFO_STYLE);
+ // for (TorrentFile file : checked) {
+ // }
+ mode.finish();
+ return true;
+ } else {
+ Priority priority = Priority.Off;
+ if (itemId == R.id.action_priority_low)
+ priority = Priority.Low;
+ if (itemId == R.id.action_priority_normal)
+ priority = Priority.Normal;
+ if (itemId == R.id.action_priority_high)
+ priority = Priority.High;
+ getTasksExecutor().updatePriority(torrent, checked, priority);
+ mode.finish();
+ return true;
+ }
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
+ selectionManagerMode.onItemCheckedStateChanged(mode, position, id, checked);
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ selectionManagerMode.onDestroyActionMode(mode);
+ }
+
+ };
+
+ /**
+ * Returns the object responsible for executing torrent tasks against a connected server
+ * @return The executor for tasks on some torrent
+ */
+ private TorrentTasksExecutor getTasksExecutor() {
+ // NOTE: Assumes the activity implements all the required torrent tasks
+ return (TorrentTasksExecutor) getActivity();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/FilterEntryDialog.java b/core/src/org/transdroid/core/gui/FilterEntryDialog.java
new file mode 100644
index 00000000..7d97759a
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/FilterEntryDialog.java
@@ -0,0 +1,39 @@
+package org.transdroid.core.gui;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.support.v4.app.DialogFragment;
+import android.text.InputType;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+public class FilterEntryDialog {
+
+ /**
+ * Opens a dialog that allows entry of a filter string, which (on confirmation) will be used to filter the list of
+ * torrents.
+ * @param activity The activity that opens (and owns) this dialog
+ */
+ public static void startFilterEntry(final TorrentsActivity activity) {
+ new DialogFragment() {
+ public android.app.Dialog onCreateDialog(android.os.Bundle savedInstanceState) {
+ final EditText filterInput = new EditText(activity);
+ filterInput.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
+ ((InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE)).toggleSoftInput(
+ InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
+ return new AlertDialog.Builder(activity).setView(filterInput)
+ .setPositiveButton(android.R.string.ok, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String filterText = filterInput.getText().toString();
+ if (activity != null)
+ activity.filterTorrents(filterText);
+ }
+ }).setNegativeButton(android.R.string.cancel, null).create();
+ };
+ }.show(activity.getSupportFragmentManager(), "filterentry");
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/ServerStatusView.java b/core/src/org/transdroid/core/gui/ServerStatusView.java
new file mode 100644
index 00000000..35c10525
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/ServerStatusView.java
@@ -0,0 +1,110 @@
+package org.transdroid.core.gui;
+
+import java.util.List;
+
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.core.R;
+import org.transdroid.core.gui.navigation.NavigationHelper;
+import org.transdroid.core.gui.navigation.SetTransferRatesDialog;
+import org.transdroid.core.gui.navigation.SetTransferRatesDialog.OnRatesPickedListener;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.TorrentStatus;
+import org.transdroid.daemon.util.FileSizeConverter;
+
+import de.keyboardsurfer.android.widget.crouton.Crouton;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+@EViewGroup(resName = "actionbar_serverstatus")
+public class ServerStatusView extends RelativeLayout implements OnRatesPickedListener {
+
+ @ViewById
+ protected TextView downcountText, upcountText, downcountSign, upcountSign, downspeedText, upspeedText;
+ private TorrentsActivity activity;
+
+ public ServerStatusView(Context context) {
+ super(context);
+ }
+
+ public ServerStatusView(TorrentsActivity activity) {
+ super(activity);
+ this.activity = activity;
+ }
+
+ /**
+ * Updates the statistics as shown in the action bar through this server status view.
+ * @param torrents The most recently received list of torrents
+ */
+ public void update(List torrents) {
+
+ if (torrents == null) {
+ downcountText.setText(null);
+ upcountText.setText(null);
+ downspeedText.setText(null);
+ upspeedText.setText(null);
+ downcountSign.setVisibility(View.INVISIBLE);
+ upcountSign.setVisibility(View.INVISIBLE);
+ setClickListener(null);
+ }
+
+ int downcount = 0, upcount = 0, downspeed = 0, upspeed = 0;
+ for (Torrent torrent : torrents) {
+
+ // Downloading torrents count towards downloads and uploads, seeding torrents towards uploads
+ if (torrent.getStatusCode() == TorrentStatus.Downloading) {
+ downcount++;
+ upcount++;
+ } else if (torrent.getStatusCode() == TorrentStatus.Seeding) {
+ upcount++;
+ }
+ downspeed += torrent.getRateDownload();
+ upspeed += torrent.getRateUpload();
+
+ }
+
+ downcountText.setText(Integer.toString(downcount));
+ upcountText.setText(Integer.toString(upcount));
+ downspeedText.setText(FileSizeConverter.getSize(downspeed));
+ upspeedText.setText(FileSizeConverter.getSize(upspeed));
+ downcountSign.setVisibility(View.VISIBLE);
+ upcountSign.setVisibility(View.VISIBLE);
+ setClickListener(onStartDownPickerClicked);
+
+ }
+
+ private void setClickListener(OnClickListener onClick) {
+ downcountText.setOnClickListener(onClick);
+ upcountText.setOnClickListener(onClick);
+ downspeedText.setOnClickListener(onClick);
+ upspeedText.setOnClickListener(onClick);
+ downcountSign.setOnClickListener(onClick);
+ upcountSign.setOnClickListener(onClick);
+ }
+
+ private OnClickListener onStartDownPickerClicked = new OnClickListener() {
+ public void onClick(View v) {
+ new SetTransferRatesDialog().setOnRatesPickedListener(ServerStatusView.this).show(
+ activity.getSupportFragmentManager(), "SetTransferRatesDialog");
+ }
+ };
+
+ @Override
+ public void onRatesPicked(int maxDownloadSpeed, int maxUploadSpeed) {
+ activity.updateMaxSpeeds(maxDownloadSpeed, maxUploadSpeed);
+ }
+
+ @Override
+ public void resetRates() {
+ activity.updateMaxSpeeds(null, null);
+ }
+
+ @Override
+ public void onInvalidNumber() {
+ Crouton.showText(activity, R.string.error_notanumber, NavigationHelper.CROUTON_ERROR_STYLE);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/TorrentTasksExecutor.java b/core/src/org/transdroid/core/gui/TorrentTasksExecutor.java
new file mode 100644
index 00000000..420d7e10
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/TorrentTasksExecutor.java
@@ -0,0 +1,21 @@
+package org.transdroid.core.gui;
+
+import java.util.List;
+
+import org.transdroid.daemon.Priority;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.TorrentFile;
+
+public interface TorrentTasksExecutor {
+ void resumeTorrent(Torrent torrent);
+ void pauseTorrent(Torrent torrent);
+ void startTorrent(Torrent torrent, boolean forced);
+ void stopTorrent(Torrent torrent);
+ void removeTorrent(Torrent torrent, boolean withData);
+ void updateLabel(Torrent torrent, String newLabel);
+ void updateTrackers(Torrent torrent, List newTrackers);
+ void updateLocation(Torrent torrent, String newLocation);
+ void refreshTorrentDetails(Torrent torrent);
+ void refreshTorrentFiles(Torrent torrent);
+ void updatePriority(Torrent torrent, List files, Priority priority);
+}
\ No newline at end of file
diff --git a/core/src/org/transdroid/core/gui/TorrentsActivity.java b/core/src/org/transdroid/core/gui/TorrentsActivity.java
new file mode 100644
index 00000000..b87eedca
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/TorrentsActivity.java
@@ -0,0 +1,956 @@
+package org.transdroid.core.gui;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.androidannotations.annotations.AfterViews;
+import org.androidannotations.annotations.Background;
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.FragmentById;
+import org.androidannotations.annotations.InstanceState;
+import org.androidannotations.annotations.OnActivityResult;
+import org.androidannotations.annotations.OptionsItem;
+import org.androidannotations.annotations.OptionsMenu;
+import org.androidannotations.annotations.SystemService;
+import org.androidannotations.annotations.UiThread;
+import org.androidannotations.annotations.ViewById;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.cookie.BasicClientCookie;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.*;
+import org.transdroid.core.gui.lists.LocalTorrent;
+import org.transdroid.core.gui.lists.SimpleListItem;
+import org.transdroid.core.gui.log.*;
+import org.transdroid.core.gui.navigation.*;
+import org.transdroid.core.gui.rss.*;
+import org.transdroid.core.gui.search.BarcodeHelper;
+import org.transdroid.core.gui.search.FilePickerHelper;
+import org.transdroid.core.gui.search.UrlEntryDialog;
+import org.transdroid.core.gui.settings.*;
+import org.transdroid.core.service.BootReceiver;
+import org.transdroid.daemon.Daemon;
+import org.transdroid.daemon.IDaemonAdapter;
+import org.transdroid.daemon.Priority;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.TorrentDetails;
+import org.transdroid.daemon.TorrentFile;
+import org.transdroid.daemon.TorrentsSortBy;
+import org.transdroid.daemon.task.AddByFileTask;
+import org.transdroid.daemon.task.AddByMagnetUrlTask;
+import org.transdroid.daemon.task.AddByUrlTask;
+import org.transdroid.daemon.task.DaemonTaskFailureResult;
+import org.transdroid.daemon.task.DaemonTaskResult;
+import org.transdroid.daemon.task.DaemonTaskSuccessResult;
+import org.transdroid.daemon.task.GetFileListTask;
+import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
+import org.transdroid.daemon.task.GetStatsTask;
+import org.transdroid.daemon.task.GetStatsTaskSuccessResult;
+import org.transdroid.daemon.task.GetTorrentDetailsTask;
+import org.transdroid.daemon.task.GetTorrentDetailsTaskSuccessResult;
+import org.transdroid.daemon.task.PauseTask;
+import org.transdroid.daemon.task.RemoveTask;
+import org.transdroid.daemon.task.ResumeTask;
+import org.transdroid.daemon.task.RetrieveTask;
+import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
+import org.transdroid.daemon.task.SetAlternativeModeTask;
+import org.transdroid.daemon.task.SetDownloadLocationTask;
+import org.transdroid.daemon.task.SetFilePriorityTask;
+import org.transdroid.daemon.task.SetLabelTask;
+import org.transdroid.daemon.task.SetTrackersTask;
+import org.transdroid.daemon.task.SetTransferRatesTask;
+import org.transdroid.daemon.task.StartTask;
+import org.transdroid.daemon.task.StopTask;
+import org.transdroid.daemon.util.DLog;
+import org.transdroid.daemon.util.HttpHelper;
+
+import android.annotation.TargetApi;
+import android.app.SearchManager;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.ActionBar.OnNavigationListener;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.SherlockListView;
+import com.actionbarsherlock.widget.SearchView;
+
+import de.keyboardsurfer.android.widget.crouton.Crouton;
+
+/**
+ * Main activity that holds the fragment that shows the torrents list, presents a way to filter the list (via an action
+ * bar spinner or list side list) and potentially shows a torrent details fragment too, if there is room. Task execution
+ * such as loading of and adding torrents is performs in this activity, using background methods. Finally, the activity
+ * offers navigation elements such as access to settings and showing connection issues.
+ * @author Eric Kok
+ */
+@EActivity(resName = "activity_torrents")
+@OptionsMenu(resName = "activity_torrents")
+public class TorrentsActivity extends SherlockFragmentActivity implements OnNavigationListener, TorrentTasksExecutor {
+
+ // Navigation components
+ @Bean
+ protected NavigationHelper navigationHelper;
+ @ViewById
+ protected SherlockListView filtersList;
+ protected FilterListAdapter navigationListAdapter = null;
+ protected FilterListDropDownAdapter navigationSpinnerAdapter = null;
+ protected ServerStatusView serverStatusView;
+ @SystemService
+ protected SearchManager searchManager;
+
+ // Settings
+ @Bean
+ protected ApplicationSettings applicationSettings;
+ @InstanceState
+ boolean firstStart = true;
+ boolean skipNextOnNavigationItemSelectedCall = false;
+ private IDaemonAdapter currentConnection = null;
+ @InstanceState
+ protected NavigationFilter currentFilter = null;
+ @InstanceState
+ protected boolean turleModeEnabled = false;
+ @InstanceState
+ protected ArrayList lastNavigationLabels;
+
+ // Contained torrent and details fragments
+ @FragmentById(resName = "torrent_list")
+ protected TorrentsFragment fragmentTorrents;
+ @FragmentById(resName = "torrent_details")
+ protected DetailsFragment fragmentDetails;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ // Set the theme according to the user preference
+ if (SystemSettings_.getInstance_(this).useDarkTheme()) {
+ setTheme(R.style.TransdroidTheme_Dark);
+ getSupportActionBar().setIcon(R.drawable.ic_activity_torrents);
+ }
+ super.onCreate(savedInstanceState);
+ }
+
+ @AfterViews
+ protected void init() {
+
+ // Set up navigation, with an action bar spinner, server status indicator and possibly (if room) with a filter
+ // list
+ serverStatusView = ServerStatusView_.build(this);
+ getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+ getSupportActionBar().setHomeButtonEnabled(false);
+ getSupportActionBar().setDisplayShowTitleEnabled(false);
+ getSupportActionBar().setDisplayShowCustomEnabled(true);
+ getSupportActionBar().setCustomView(serverStatusView);
+ navigationSpinnerAdapter = FilterListDropDownAdapter_.getInstance_(this);
+ // Servers are always added to the action bar spinner
+ navigationSpinnerAdapter.updateServers(applicationSettings.getServerSettings());
+
+ // Check if there was room for a dedicated filter list (i.e. on tablets)
+ if (filtersList != null) {
+ // The action bar spinner doesn't have to show the 'servers' label, as it will only contain servers
+ navigationSpinnerAdapter.hideServersLabel();
+ // Create dedicated side list adapter and add the status types
+ navigationListAdapter = FilterListAdapter_.getInstance_(this);
+ navigationListAdapter.updateStatusTypes(StatusType.getAllStatusTypes(this));
+ // Add an empty labels list (which will be updated later, but the adapter needs to be created now)
+ navigationListAdapter.updateLabels(new ArrayList());
+ filtersList.setAdapter(navigationListAdapter);
+ filtersList.setOnItemClickListener(onFilterListItemClicked);
+ } else {
+ // Add status types directly to the action bar spinner
+ navigationSpinnerAdapter.updateStatusTypes(StatusType.getAllStatusTypes(this));
+ // Add an empty labels list (which will be updated later, but the adapter needs to be created now)
+ navigationSpinnerAdapter.updateLabels(new ArrayList());
+ }
+ // Now that all items (or at least their adapters) have been added
+ currentFilter = StatusType.getShowAllType(this);
+ getSupportActionBar().setListNavigationCallbacks(navigationSpinnerAdapter, this);
+
+ // Log messages from the server daemons using our singleton logger
+ DLog.setLogger(Log_.getInstance_(this));
+
+ // Connect to the last used server or a server that was supplied in the starting intent
+ ServerSetting lastUsed = applicationSettings.getLastUsedServer();
+ if (lastUsed == null) {
+ // No server settings yet;
+ return;
+ }
+ if (getIntent().getExtras() == null && getIntent().hasExtra("org.transdroid.START_SERVER")) {
+ lastUsed = applicationSettings.getServerSetting(getIntent().getExtras().getInt(
+ "org.transdroid.START_SERVER"));
+ }
+
+ // Set this as selection in the action bar spinner; we can use the server setting key since we have stable ids
+ getSupportActionBar().setSelectedNavigationItem(lastUsed.getOrder() + 1);
+ skipNextOnNavigationItemSelectedCall = true;
+
+ // Handle any start up intents
+ if (firstStart && getIntent() != null) {
+ currentConnection = lastUsed.createServerAdapter();
+ handleStartIntent();
+ }
+
+ // Start the alarms for the background services, if needed
+ BootReceiver.startBackgroundServices(getApplicationContext(), false);
+ BootReceiver.startAppUpdatesService(getApplicationContext());
+
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ // Refresh server settings
+ navigationSpinnerAdapter.updateServers(applicationSettings.getServerSettings());
+ ServerSetting lastUsed = applicationSettings.getLastUsedServer();
+ if (lastUsed == null) {
+ // Still no settings
+ updateFragmentVisibility(false);
+ return;
+ }
+ // There is a server know (now): forcefully select it to establish a connection
+ filterSelected(lastUsed, true);
+ }
+
+ @Override
+ protected void onDestroy() {
+ Crouton.cancelAllCroutons();
+ super.onDestroy();
+ }
+
+ @TargetApi(Build.VERSION_CODES.FROYO)
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ if (navigationHelper.enableSearchUi()) {
+ // For Android 2.1+, add an expandable SearchView to the action bar
+ MenuItem item = menu.findItem(R.id.action_search);
+ if (android.os.Build.VERSION.SDK_INT >= 8) {
+ final SearchView searchView = new SearchView(this);
+ searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
+ searchView.setQueryRefinementEnabled(true);
+ item.setActionView(searchView);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ // No connection yet; hide all menu options except settings
+ if (currentConnection == null) {
+ menu.findItem(R.id.action_add).setVisible(false);
+ menu.findItem(R.id.action_search).setVisible(false);
+ menu.findItem(R.id.action_rss).setVisible(false);
+ menu.findItem(R.id.action_enableturtle).setVisible(false);
+ menu.findItem(R.id.action_disableturtle).setVisible(false);
+ menu.findItem(R.id.action_refresh).setVisible(false);
+ menu.findItem(R.id.action_sort).setVisible(false);
+ menu.findItem(R.id.action_filter).setVisible(false);
+ menu.findItem(R.id.action_settings).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ menu.findItem(R.id.action_help).setVisible(true);
+ if (fragmentTorrents != null)
+ fragmentTorrents.updateConnectionStatus(false);
+ getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ return true;
+ }
+
+ // There is a connection (read: settings to some server known)
+ menu.findItem(R.id.action_add).setVisible(true);
+ menu.findItem(R.id.action_search).setVisible(navigationHelper.enableSearchUi());
+ menu.findItem(R.id.action_rss).setVisible(navigationHelper.enableRssUi());
+ boolean hasAltMode = Daemon.supportsSetAlternativeMode(currentConnection.getType());
+ menu.findItem(R.id.action_enableturtle).setVisible(hasAltMode && !turleModeEnabled);
+ menu.findItem(R.id.action_disableturtle).setVisible(hasAltMode && turleModeEnabled);
+ menu.findItem(R.id.action_refresh).setVisible(true);
+ menu.findItem(R.id.action_sort).setVisible(true);
+ menu.findItem(R.id.action_filter).setVisible(true);
+ menu.findItem(R.id.action_settings).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ menu.findItem(R.id.action_help).setVisible(false);
+ if (fragmentTorrents != null)
+ fragmentTorrents.updateConnectionStatus(true);
+ getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+
+ return true;
+ }
+
+ /**
+ * Called when an item in the action bar navigation spinner was selected
+ */
+ @Override
+ public boolean onNavigationItemSelected(int itemPosition, long itemId) {
+ if (skipNextOnNavigationItemSelectedCall) {
+ skipNextOnNavigationItemSelectedCall = false;
+ return false;
+ }
+ Object item = navigationSpinnerAdapter.getItem(itemPosition);
+ if (item instanceof SimpleListItem) {
+ // A filter item was selected form the navigation spinner
+ filterSelected((SimpleListItem) item, false);
+ return true;
+ }
+ // A header was selected; no action
+ return false;
+ }
+
+ // Handles item selections on the dedicated list of filter items
+ private OnItemClickListener onFilterListItemClicked = new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ filtersList.setItemChecked(position, true);
+ filterSelected((SimpleListItem) filtersList.getAdapter().getItem(position), false);
+ }
+ };
+
+ /**
+ * A new filter was selected; update the view over the current data
+ * @param item The touched filter item
+ * @param forceNewConnection Whether a new connection should be initialised regardless of the old server selection
+ */
+ protected void filterSelected(SimpleListItem item, boolean forceNewConnection) {
+
+ // Server selection
+ if (item instanceof ServerSetting) {
+ ServerSetting server = (ServerSetting) item;
+
+ if (!forceNewConnection && currentConnection != null && server.equals(currentConnection.getSettings())) {
+ // Already connected to this server; just ask for a refresh instead
+ fragmentTorrents.updateIsLoading(true);
+ refreshTorrents();
+ return;
+ }
+
+ // Update connection to the newly selected server and refresh
+ currentConnection = server.createServerAdapter();
+ applicationSettings.setLastUsedServer(server);
+ navigationSpinnerAdapter.updateCurrentServer(currentConnection);
+ if (forceNewConnection)
+ navigationSpinnerAdapter.updateCurrentFilter(currentFilter);
+
+ // Clear the currently shown list of torrents and perhaps the details
+ fragmentTorrents.clear(true, true);
+ if (fragmentDetails != null && fragmentDetails.getActivity() != null) {
+ fragmentDetails.clear();
+ }
+ updateFragmentVisibility(true);
+ refreshScreen();
+ return;
+
+ }
+
+ // Status type or label selection - both of which are navigation filters
+ if (item instanceof NavigationFilter) {
+ currentFilter = (NavigationFilter) item;
+ fragmentTorrents.applyNavigationFilter(currentFilter);
+ navigationSpinnerAdapter.updateCurrentFilter(currentFilter);
+ // Clear the details view
+ if (fragmentDetails != null) {
+ fragmentDetails.clear();
+ }
+ }
+
+ }
+
+ /**
+ * Hides the filter list and details fragment's full view if there is no configured connection
+ * @param hasServerSettings Whether there are server settings available, so we can continue to connect
+ */
+ private void updateFragmentVisibility(boolean hasServerSettings) {
+ if (filtersList != null)
+ filtersList.setVisibility(hasServerSettings ? View.VISIBLE : View.GONE);
+ if (fragmentDetails != null) {
+ if (hasServerSettings)
+ getSupportFragmentManager().beginTransaction().show(fragmentDetails).commit();
+ else
+ getSupportFragmentManager().beginTransaction().hide(fragmentDetails).commit();
+ }
+ supportInvalidateOptionsMenu();
+ }
+
+ /**
+ * If required, add torrents, switch to a specific server, etc.
+ */
+ protected void handleStartIntent() {
+
+ Intent intent = getIntent();
+ Uri dataUri = intent.getData();
+ String data = intent.getDataString();
+ String action = intent.getAction();
+
+ // Adding multiple torrents at the same time (as found in the Intent extras Bundle)
+
+ if (action != null && action.equals("org.transdroid.ADD_MULTIPLE")) {
+ // Intent should have some extras pointing to possibly multiple torrents
+ String[] urls = intent.getStringArrayExtra("TORRENT_URLS");
+ String[] titles = intent.getStringArrayExtra("TORRENT_TITLES");
+ if (urls != null) {
+ for (int i = 0; i < urls.length; i++) {
+ addTorrentByUrl(urls[i], (titles != null && titles.length >= i ? titles[i] : "Torrent"));
+ }
+ }
+ return;
+ }
+
+ // Add a torrent from a local or remote data URI?
+ if (dataUri == null)
+ return;
+
+ // Adding a torrent from the Android downloads manager
+ if (dataUri.getScheme() != null && dataUri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
+ addTorrentFromDownloads(dataUri);
+ return;
+ }
+
+ // Adding a torrent from http or https URL
+ if (dataUri.getScheme().equals("http") || dataUri.getScheme().equals("https")) {
+
+ // Check if the target URL is also defined as a web search in the user's settings
+ List websearches = applicationSettings.getWebsearchSettings();
+ WebsearchSetting match = null;
+ for (WebsearchSetting setting : websearches) {
+ Uri uri = Uri.parse(setting.getBaseUrl());
+ if (uri.getHost() != null && uri.getHost().equals(dataUri.getHost())) {
+ match = setting;
+ break;
+ }
+ }
+
+ // If the URL is also a web search and it defines cookies, use the cookies by downloading the targeted
+ // torrent file (while supplies the cookies to the HTTP request) instead of sending the URL directly to the
+ // torrent client
+ if (match != null && match.getCookies() != null) {
+ addTorrentFromWeb(data, match);
+ } else {
+ // Normally send the URL to the torrent client; the title we show is just the 'file name'
+ // TODO: Make a better effort in determining the title (check query string, clean special chars)
+ String title = data.substring(data.lastIndexOf("/") + 1);
+ if (intent.hasExtra("TORRENT_TITLE")) {
+ title = intent.getStringExtra("TORRENT_TITLE");
+ }
+ addTorrentByUrl(data, title);
+ }
+ return;
+ }
+
+ // Adding a torrent from magnet URL
+ if (dataUri.getScheme().equals("magnet")) {
+ addTorrentByMagnetUrl(data);
+ return;
+ }
+
+ // Adding a local .torrent file; the title we show is just the file name
+ if (dataUri.getScheme().equals("file")) {
+ String title = data.substring(data.lastIndexOf("/") + 1);
+ addTorrentByFile(data, title);
+ return;
+ }
+
+ }
+
+ @OptionsItem(resName = "action_add_fromurl")
+ protected void startUrlEntryDialog() {
+ UrlEntryDialog.startUrlEntry(this);
+ }
+
+ @OptionsItem(resName = "action_add_fromfile")
+ protected void startFilePicker() {
+ FilePickerHelper.startFilePicker(this);
+ }
+
+ @Background
+ @OnActivityResult(FilePickerHelper.ACTIVITY_FILEPICKER)
+ public void onFilePicked(int resultCode, Intent data) {
+ // We should have received an Intent with a local torrent's Uri as data from the file picker
+ if (data != null && data.getData() != null && !data.getData().equals("")) {
+ String url = data.getData().getPath();
+ addTorrentByFile(data.getData().toString(), url.substring(url.lastIndexOf("/")));
+ }
+ }
+
+ @OptionsItem(resName = "action_add_frombarcode")
+ protected void startBarcodeScanner() {
+ BarcodeHelper.startBarcodeScanner(this);
+ }
+
+ @Background
+ @OnActivityResult(BarcodeHelper.ACTIVITY_BARCODE)
+ public void onBarcodeScanned(int resultCode, Intent data) {
+ // We receive from the helper either a URL (as string) or a query we can start a search for
+ String query = BarcodeHelper.handleScanResult(resultCode, data);
+ if (query.startsWith("http"))
+ addTorrentByUrl(query, "QR code result"); // No torrent title known
+ else
+ startSearch(query, false, null, false);
+ }
+
+ @OptionsItem(resName = "action_refresh")
+ protected void refreshScreen() {
+ fragmentTorrents.updateIsLoading(true);
+ refreshTorrents();
+ if (Daemon.supportsStats(currentConnection.getType()))
+ getAdditionalStats();
+ }
+
+ @OptionsItem(resName = "action_enableturtle")
+ protected void enableTurtleMode() {
+ updateTurtleMode(true);
+ }
+
+ @OptionsItem(resName = "action_disableturtle")
+ protected void disableTurtleMode() {
+ updateTurtleMode(false);
+ }
+
+ @OptionsItem(resName = "action_rss")
+ protected void openRss() {
+ RssfeedsActivity_.intent(this).start();
+ }
+
+ @OptionsItem(resName = "action_settings")
+ protected void openSettings() {
+ MainSettingsActivity_.intent(this).start();
+ }
+
+ @OptionsItem(resName = "action_help")
+ protected void openHelp() {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.transdroid.org/download/")));
+ }
+
+ @OptionsItem(resName = "action_sort_byname")
+ protected void sortByName() {
+ fragmentTorrents.sortBy(TorrentsSortBy.Alphanumeric);
+ }
+
+ @OptionsItem(resName = "action_sort_status")
+ protected void sortByStatus() {
+ fragmentTorrents.sortBy(TorrentsSortBy.Status);
+ }
+
+ @OptionsItem(resName = "action_sort_done")
+ protected void sortByDateDone() {
+ fragmentTorrents.sortBy(TorrentsSortBy.DateDone);
+ }
+
+ @OptionsItem(resName = "action_sort_added")
+ protected void sortByDateAdded() {
+ fragmentTorrents.sortBy(TorrentsSortBy.DateAdded);
+ }
+
+ @OptionsItem(resName = "action_sort_upspeed")
+ protected void sortByUpspeed() {
+ fragmentTorrents.sortBy(TorrentsSortBy.UploadSpeed);
+ }
+
+ @OptionsItem(resName = "action_sort_ratio")
+ protected void sortByRatio() {
+ fragmentTorrents.sortBy(TorrentsSortBy.Ratio);
+ }
+
+ @OptionsItem(resName = "action_filter")
+ protected void startFilterEntryDialog() {
+ FilterEntryDialog.startFilterEntry(this);
+ }
+
+ /**
+ * Redirect the newly entered list filter to the torrents fragment.
+ * @param newFilterText The newly entered filter (or empty to clear the current filter).
+ */
+ public void filterTorrents(String newFilterText) {
+ fragmentTorrents.applyTextFilter(newFilterText);
+ }
+
+ /**
+ * Shows the a details fragment for the given torrent, either in the dedicated details fragment pane, in the same
+ * pane as the torrent list was displayed or by starting a details activity.
+ * @param torrent The torrent to show detailed statistics for
+ */
+ public void openDetails(Torrent torrent) {
+ if (fragmentDetails != null) {
+ fragmentDetails.updateTorrent(torrent);
+ } else {
+ DetailsActivity_.intent(this).torrent(torrent).currentLabels(lastNavigationLabels).start();
+ }
+ }
+
+ @Background
+ protected void refreshTorrents() {
+ DaemonTaskResult result = RetrieveTask.create(currentConnection).execute();
+ if (result instanceof RetrieveTaskSuccessResult) {
+ onTorrentsRetrieved(((RetrieveTaskSuccessResult) result).getTorrents(),
+ ((RetrieveTaskSuccessResult) result).getLabels());
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, true);
+ }
+ }
+
+ @Background
+ public void refreshTorrentDetails(Torrent torrent) {
+ if (!Daemon.supportsFineDetails(currentConnection.getType()))
+ return;
+ DaemonTaskResult result = GetTorrentDetailsTask.create(currentConnection, torrent).execute();
+ if (result instanceof GetTorrentDetailsTaskSuccessResult) {
+ onTorrentDetailsRetrieved(torrent, ((GetTorrentDetailsTaskSuccessResult) result).getTorrentDetails());
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ public void refreshTorrentFiles(Torrent torrent) {
+ if (!Daemon.supportsFileListing(currentConnection.getType()))
+ return;
+ DaemonTaskResult result = GetFileListTask.create(currentConnection, torrent).execute();
+ if (result instanceof GetFileListTaskSuccessResult) {
+ onTorrentFilesRetrieved(torrent, ((GetFileListTaskSuccessResult) result).getFiles());
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ protected void getAdditionalStats() {
+ DaemonTaskResult result = GetStatsTask.create(currentConnection).execute();
+ if (result instanceof GetStatsTaskSuccessResult) {
+ onTurtleModeRetrieved(((GetStatsTaskSuccessResult) result).isAlternativeModeEnabled());
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ protected void updateTurtleMode(boolean enable) {
+ DaemonTaskResult result = SetAlternativeModeTask.create(currentConnection, enable).execute();
+ if (result instanceof DaemonTaskSuccessResult) {
+ // Success; no need to retrieve it again - just update the visual indicator
+ onTurtleModeRetrieved(enable);
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ public void addTorrentByUrl(String url, String title) {
+ DaemonTaskResult result = AddByUrlTask.create(currentConnection, url, title).execute();
+ if (result instanceof DaemonTaskSuccessResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_added, title));
+ refreshTorrents();
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ protected void addTorrentByMagnetUrl(String url) {
+ DaemonTaskResult result = AddByMagnetUrlTask.create(currentConnection, url).execute();
+ if (result instanceof DaemonTaskSuccessResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_added, "Torrent"));
+ refreshTorrents();
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ protected void addTorrentByFile(String localFile, String title) {
+ DaemonTaskResult result = AddByFileTask.create(currentConnection, localFile).execute();
+ if (result instanceof DaemonTaskSuccessResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_added, title));
+ refreshTorrents();
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ private void addTorrentFromDownloads(Uri contentUri) {
+
+ try {
+ // Open the content uri as input stream and this via a local temporary file
+ addTorrentFromStream(getContentResolver().openInputStream(contentUri));
+ } catch (SecurityException e) {
+ // No longer access to this file
+ Log.e(this, "No access given to " + contentUri.toString() + ": " + e.toString());
+ Crouton.showText(this, R.string.error_torrentfile, NavigationHelper.CROUTON_ERROR_STYLE);
+ } catch (FileNotFoundException e) {
+ Log.e(this, contentUri.toString() + " does not exist: " + e.toString());
+ Crouton.showText(this, R.string.error_torrentfile, NavigationHelper.CROUTON_ERROR_STYLE);
+ }
+ }
+
+ @Background
+ protected void addTorrentFromWeb(String url, WebsearchSetting websearchSetting) {
+
+ try {
+ // Cookies are taken from the websearchSetting that we already matched against this target URL
+ DefaultHttpClient httpclient = HttpHelper.createStandardHttpClient(false, null, null, true, null, 10000,
+ null, -1);
+ Map cookies = HttpHelper.parseCookiePairs(websearchSetting.getCookies());
+ String domain = Uri.parse(url).getHost();
+ for (Entry pair : cookies.entrySet()) {
+ BasicClientCookie cookie = new BasicClientCookie(pair.getKey(), pair.getValue());
+ cookie.setPath("/");
+ cookie.setDomain(domain);
+ httpclient.getCookieStore().addCookie(cookie);
+ }
+
+ // Download the torrent at the specified URL (which will first be written to a temporary file)
+ // If we get an HTTP 401, 403 or 404 response, show an error to the user
+ HttpResponse response = httpclient.execute(new HttpGet(url));
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED
+ || response.getStatusLine().getStatusCode() == HttpStatus.SC_FORBIDDEN
+ || response.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_FOUND) {
+ Log.e(this, "Can't retrieve web torrent " + url + ": Unexpected HTTP response status code "
+ + response.getStatusLine().toString());
+ Crouton.showText(this, R.string.error_401, NavigationHelper.CROUTON_ERROR_STYLE);
+ return;
+ }
+ InputStream input = response.getEntity().getContent();
+ addTorrentFromStream(input);
+ } catch (Exception e) {
+ Log.e(this, "Can't retrieve web torrent " + url + ": " + e.toString());
+ Crouton.showText(this, R.string.error_torrentfile, NavigationHelper.CROUTON_ERROR_STYLE);
+ }
+ }
+
+ @Background
+ protected void addTorrentFromStream(InputStream input) {
+
+ File tempFile = new File("/not/yet/set");
+ try {
+ // Write a temporary file with the torrent contents
+ tempFile = File.createTempFile("transdroid_", ".torrent", getCacheDir());
+ FileOutputStream output = new FileOutputStream(tempFile);
+ try {
+ final byte[] buffer = new byte[1024];
+ int read;
+ while ((read = input.read(buffer)) != -1)
+ output.write(buffer, 0, read);
+ output.flush();
+ String fileName = Uri.fromFile(tempFile).toString();
+ addTorrentByFile(fileName, fileName.substring(fileName.lastIndexOf("/")));
+ } finally {
+ output.close();
+ }
+ } catch (IOException e) {
+ Log.e(this, "Can't write input stream to " + tempFile.toString() + ": " + e.toString());
+ Crouton.showText(this, R.string.error_torrentfile, NavigationHelper.CROUTON_ERROR_STYLE);
+ } finally {
+ try {
+ if (input != null)
+ input.close();
+ } catch (IOException e) {
+ Log.e(this, "Error closing the input stream " + tempFile.toString() + ": " + e.toString());
+ Crouton.showText(this, R.string.error_torrentfile, NavigationHelper.CROUTON_ERROR_STYLE);
+ }
+ }
+ }
+
+ @Background
+ @Override
+ public void resumeTorrent(Torrent torrent) {
+ torrent.mimicResume();
+ DaemonTaskResult result = ResumeTask.create(currentConnection, torrent).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_resumed, torrent.getName()));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ @Override
+ public void pauseTorrent(Torrent torrent) {
+ torrent.mimicPause();
+ DaemonTaskResult result = PauseTask.create(currentConnection, torrent).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_paused, torrent.getName()));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ @Override
+ public void startTorrent(Torrent torrent, boolean forced) {
+ torrent.mimicStart();
+ DaemonTaskResult result = StartTask.create(currentConnection, torrent, forced).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_started));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ @Override
+ public void stopTorrent(Torrent torrent) {
+ torrent.mimicStop();
+ DaemonTaskResult result = StopTask.create(currentConnection, torrent).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_stopped));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ @Override
+ public void removeTorrent(Torrent torrent, boolean withData) {
+ DaemonTaskResult result = RemoveTask.create(currentConnection, torrent, withData).execute();
+ if (result instanceof DaemonTaskSuccessResult) {
+ onTaskSucceeded(
+ (DaemonTaskSuccessResult) result,
+ getString(withData ? R.string.result_removed_with_data : R.string.result_removed, torrent.getName()));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ @Override
+ public void updateLabel(Torrent torrent, String newLabel) {
+ torrent.mimicNewLabel(newLabel);
+ DaemonTaskResult result = SetLabelTask.create(currentConnection, torrent, newLabel == null ? "" : newLabel)
+ .execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded(
+ (DaemonTaskSuccessResult) result,
+ newLabel == null ? getString(R.string.result_labelremoved) : getString(R.string.result_labelset,
+ newLabel));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ @Override
+ public void updateTrackers(Torrent torrent, List newTrackers) {
+ DaemonTaskResult result = SetTrackersTask.create(currentConnection, torrent, newTrackers).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_trackersupdated));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ @Override
+ public void updateLocation(Torrent torrent, String newLocation) {
+ DaemonTaskResult result = SetDownloadLocationTask.create(currentConnection, torrent, newLocation).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_locationset, newLocation));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ @Override
+ public void updatePriority(Torrent torrent, List files, Priority priority) {
+ DaemonTaskResult result = SetFilePriorityTask.create(currentConnection, torrent, priority,
+ new ArrayList(files)).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_priotitiesset));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @Background
+ public void updateMaxSpeeds(Integer maxDownloadSpeed, Integer maxUploadSpeed) {
+ DaemonTaskResult result = SetTransferRatesTask.create(currentConnection, maxUploadSpeed, maxDownloadSpeed)
+ .execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_maxspeedsset));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ @UiThread
+ protected void onTaskSucceeded(DaemonTaskSuccessResult result, String successMessage) {
+ // Refresh the screen as well
+ refreshScreen();
+ Crouton.showText(this, successMessage, NavigationHelper.CROUTON_INFO_STYLE);
+ }
+
+ @UiThread
+ protected void onCommunicationError(DaemonTaskFailureResult result, boolean isCritical) {
+ Log.i(this, result.getException().toString());
+ String error = getString(LocalTorrent.getResourceForDaemonException(result.getException()));
+ Crouton.showText(this, error, NavigationHelper.CROUTON_ERROR_STYLE);
+ fragmentTorrents.updateIsLoading(false);
+ if (isCritical)
+ fragmentTorrents.updateError(error);
+ }
+
+ @UiThread
+ protected void onTorrentsRetrieved(List torrents, List labels) {
+
+ lastNavigationLabels = Label.convertToNavigationLabels(labels,
+ getResources().getString(R.string.labels_unlabeled));
+
+ // Report the newly retrieved list of torrents to the torrents fragment
+ fragmentTorrents.updateIsLoading(false);
+ fragmentTorrents.updateTorrents(new ArrayList(torrents), lastNavigationLabels);
+
+ // Update the details fragment if the currently shown torrent is in the newly retrieved list
+ if (fragmentDetails != null) {
+ fragmentDetails.perhapsUpdateTorrent(torrents);
+ }
+
+ // Update local list of labels in the navigation
+ if (navigationListAdapter != null) {
+ // Labels are shown in the dedicated side navigation
+ navigationListAdapter.updateLabels(lastNavigationLabels);
+ } else {
+ // Labels are shown in the action bar spinner
+ navigationSpinnerAdapter.updateLabels(lastNavigationLabels);
+ }
+ if (fragmentDetails != null)
+ fragmentDetails.updateLabels(lastNavigationLabels);
+
+ // Update the server status (counts and speeds) in the action bar
+ serverStatusView.update(torrents);
+
+ }
+
+ @UiThread
+ protected void onTorrentDetailsRetrieved(Torrent torrent, TorrentDetails torrentDetails) {
+ // Update the details fragment with the new fine details for the shown torrent
+ if (fragmentDetails != null)
+ fragmentDetails.updateTorrentDetails(torrent, torrentDetails);
+ }
+
+ @UiThread
+ protected void onTorrentFilesRetrieved(Torrent torrent, List torrentFiles) {
+ // Update the details fragment with the newly retrieved list of files
+ if (fragmentDetails != null)
+ fragmentDetails.updateTorrentFiles(torrent, new ArrayList(torrentFiles));
+ }
+
+ @UiThread
+ protected void onTurtleModeRetrieved(boolean turtleModeEnabled) {
+ turleModeEnabled = turtleModeEnabled;
+ supportInvalidateOptionsMenu();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/TorrentsFragment.java b/core/src/org/transdroid/core/gui/TorrentsFragment.java
new file mode 100644
index 00000000..3e408d62
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/TorrentsFragment.java
@@ -0,0 +1,353 @@
+package org.transdroid.core.gui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Locale;
+
+import org.androidannotations.annotations.AfterViews;
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EFragment;
+import org.androidannotations.annotations.InstanceState;
+import org.androidannotations.annotations.ItemClick;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.ApplicationSettings;
+import org.transdroid.core.gui.lists.TorrentsAdapter;
+import org.transdroid.core.gui.lists.TorrentsAdapter_;
+import org.transdroid.core.gui.navigation.Label;
+import org.transdroid.core.gui.navigation.NavigationFilter;
+import org.transdroid.core.gui.navigation.SelectionManagerMode;
+import org.transdroid.core.gui.navigation.SetLabelDialog;
+import org.transdroid.core.gui.navigation.SetLabelDialog.OnLabelPickedListener;
+import org.transdroid.daemon.Daemon;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.TorrentsComparator;
+import org.transdroid.daemon.TorrentsSortBy;
+
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.actionbarsherlock.view.ActionMode;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.SherlockListView;
+import com.actionbarsherlock.view.SherlockListView.MultiChoiceModeListenerCompat;
+
+/**
+ * Fragment that shows a list of torrents that are active on the server. It supports sorting and filtering and can show
+ * connection progress and issues. However, actual task starting and execution and overall navigation elements are part
+ * of the containing activity, not this fragment.
+ * @author Eric Kok
+ */
+@EFragment(resName = "fragment_torrents")
+public class TorrentsFragment extends SherlockFragment implements OnLabelPickedListener {
+
+ // Local data
+ @Bean
+ protected ApplicationSettings applicationSettings;
+ @InstanceState
+ protected ArrayList torrents = null;
+ @InstanceState
+ protected ArrayList lastMultiSelectedTorrents;
+ @InstanceState
+ protected ArrayList currentLabels;
+ @InstanceState
+ protected NavigationFilter currentNavigationFilter = null;
+ @InstanceState
+ protected TorrentsSortBy currentSortOrder = TorrentsSortBy.Alphanumeric;
+ @InstanceState
+ protected boolean currentSortDescending = false;
+ @InstanceState
+ protected String currentTextFilter = null;
+ @InstanceState
+ protected boolean hasAConnection = false;
+ @InstanceState
+ protected boolean isLoading = true;
+ @InstanceState
+ protected String connectionErrorMessage = null;
+
+ // Views
+ @ViewById(resName = "torrent_list")
+ protected SherlockListView torrentsList;
+ @ViewById
+ protected TextView emptyText;
+ @ViewById
+ protected TextView nosettingsText;
+ @ViewById
+ protected TextView errorText;
+ @ViewById
+ protected ProgressBar loadingProgress;
+
+ @AfterViews
+ protected void init() {
+
+ // Load the requested sort order from the user settings
+ this.currentSortOrder = applicationSettings.getLastUsedSortOrder();
+ this.currentSortDescending = applicationSettings.getLastUsedSortDescending();
+
+ // Set up the list adapter, which allows multi-select and fast scrolling
+ torrentsList.setAdapter(TorrentsAdapter_.getInstance_(getActivity()));
+ torrentsList.setMultiChoiceModeListener(onTorrentsSelected);
+ torrentsList.setFastScrollEnabled(true);
+ if (torrents != null)
+ updateTorrents(torrents, currentLabels);
+
+ }
+
+ /**
+ * Updates the list adapter to show a new list of torrent objects, replacing the old torrents completely
+ * @param newTorrents The new, updated list of torrents
+ */
+ public void updateTorrents(ArrayList newTorrents, ArrayList currentLabels) {
+ this.torrents = newTorrents;
+ this.currentLabels = currentLabels;
+ applyAllFilters();
+ }
+
+ /**
+ * Clears the currently visible list of torrents.
+ * @param b
+ */
+ public void clear(boolean clearError, boolean clearFilter) {
+ this.torrents = null;
+ if (clearError)
+ this.connectionErrorMessage = null;
+ if (clearFilter) {
+ this.currentTextFilter = null;
+ this.currentNavigationFilter = null;
+ }
+ applyAllFilters();
+ }
+
+ /**
+ * Stores the new sort order (for future refreshes) and sorts the current visible list. If the given new sort
+ * property equals the existing property, the list sort order is reversed instead.
+ * @param newSortOrder The sort order that the user selected.
+ */
+ public void sortBy(TorrentsSortBy newSortOrder) {
+ // Update the sort order property and direction and store this last used setting
+ if (this.currentSortOrder == newSortOrder) {
+ this.currentSortDescending = !this.currentSortDescending;
+ } else {
+ this.currentSortOrder = newSortOrder;
+ this.currentSortDescending = false;
+ }
+ applicationSettings.setLastUsedSortOrder(this.currentSortOrder, this.currentSortDescending);
+ applyAllFilters();
+ }
+
+ public void applyTextFilter(String newTextFilter) {
+ this.currentTextFilter = newTextFilter;
+ // Show the new filtered list
+ applyAllFilters();
+ }
+
+ /**
+ * Apply a filter on the current list of all torrents, showing the appropriate sublist of torrents only
+ * @param newFilter The new filter to apply to the local list of torrents
+ */
+ public void applyNavigationFilter(NavigationFilter newFilter) {
+ this.currentNavigationFilter = newFilter;
+ applyAllFilters();
+ }
+
+ private void applyAllFilters() {
+
+ // No torrents? Directly update views accordingly
+ if (torrents == null) {
+ updateViewVisibility();
+ return;
+ }
+
+ // Get the server daemon type directly form the local list of torrents, if it's not empty
+ Daemon serverType = (this.torrents.size() > 0 ? this.torrents.get(0).getDaemon() : Daemon.Transmission);
+
+ // Filter the list of torrents to show according to navigation and text filters
+ ArrayList filteredTorrents = new ArrayList(torrents);
+ if (filteredTorrents != null && currentNavigationFilter != null) {
+ // Remove torrents that do not match the selected navigation filter
+ for (Iterator torrentIter = filteredTorrents.iterator(); torrentIter.hasNext();) {
+ if (!currentNavigationFilter.matches(torrentIter.next()))
+ torrentIter.remove();
+ }
+ }
+ if (filteredTorrents != null && currentTextFilter != null) {
+ // Remove torrent that do not contain the text filter string
+ for (Iterator torrentIter = filteredTorrents.iterator(); torrentIter.hasNext();) {
+ if (!torrentIter.next().getName().toLowerCase(Locale.getDefault())
+ .contains(currentTextFilter.toLowerCase(Locale.getDefault())))
+ torrentIter.remove();
+ }
+ }
+
+ // Sort the list of filtered torrents
+ Collections.sort(filteredTorrents, new TorrentsComparator(serverType, this.currentSortOrder,
+ this.currentSortDescending));
+
+ ((TorrentsAdapter) torrentsList.getAdapter()).update(filteredTorrents);
+ updateViewVisibility();
+ }
+
+ private MultiChoiceModeListenerCompat onTorrentsSelected = new MultiChoiceModeListenerCompat() {
+
+ SelectionManagerMode selectionManagerMode;
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ // Show contextual action bar to start/stop/remove/etc. torrents in batch mode
+ mode.getMenuInflater().inflate(R.menu.fragment_torrents_cab, menu);
+ selectionManagerMode = new SelectionManagerMode(torrentsList, R.plurals.navigation_torrentsselected);
+ selectionManagerMode.onCreateActionMode(mode, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return selectionManagerMode.onPrepareActionMode(mode, menu);
+ }
+
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+
+ // Get checked torrents
+ ArrayList checked = new ArrayList();
+ for (int i = 0; i < torrentsList.getCheckedItemPositions().size(); i++) {
+ if (torrentsList.getCheckedItemPositions().valueAt(i))
+ checked.add((Torrent) torrentsList.getAdapter().getItem(
+ torrentsList.getCheckedItemPositions().keyAt(i)));
+ }
+
+ int itemId = item.getItemId();
+ if (itemId == R.id.action_resume) {
+ for (Torrent torrent : checked) {
+ getTasksExecutor().resumeTorrent(torrent);
+ }
+ mode.finish();
+ return true;
+ } else if (itemId == R.id.action_pause) {
+ for (Torrent torrent : checked) {
+ getTasksExecutor().pauseTorrent(torrent);
+ }
+ mode.finish();
+ return true;
+ } else if (itemId == R.id.action_remove_default) {
+ for (Torrent torrent : checked) {
+ getTasksExecutor().removeTorrent(torrent, false);
+ }
+ mode.finish();
+ return true;
+ } else if (itemId == R.id.action_remove_withdata) {
+ for (Torrent torrent : checked) {
+ getTasksExecutor().removeTorrent(torrent, true);
+ }
+ mode.finish();
+ return true;
+ } else if (itemId == R.id.action_setlabel) {
+ lastMultiSelectedTorrents = checked;
+ new SetLabelDialog().setOnLabelPickedListener(TorrentsFragment.this).setCurrentLabels(currentLabels)
+ .show(getFragmentManager(), "SetLabelDialog");
+ mode.finish();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
+ selectionManagerMode.onItemCheckedStateChanged(mode, position, id, checked);
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ selectionManagerMode.onDestroyActionMode(mode);
+ }
+
+ };
+
+ @ItemClick(resName = "torrent_list")
+ protected void torrentsListClicked(Torrent torrent) {
+ ((TorrentsActivity) getActivity()).openDetails(torrent);
+ }
+
+ @Override
+ public void onLabelPicked(String newLabel) {
+ for (Torrent torrent : lastMultiSelectedTorrents) {
+ getTasksExecutor().updateLabel(torrent, newLabel);
+ }
+ }
+
+ /**
+ * Updates the shown screen depending on whether we have a connection (so torrents can be shown) or not (in case we
+ * need to show a message suggesting help). This should only ever be called on the UI thread.
+ * @param hasAConnection True if the user has servers configured and therefore has a connection that can be used
+ */
+ public void updateConnectionStatus(boolean hasAConnection) {
+ this.hasAConnection = hasAConnection;
+ if (!hasAConnection) {
+ clear(true, true); // Indirectly also calls updateViewVisibility()
+ } else {
+ updateViewVisibility();
+ }
+ }
+
+ /**
+ * Updates the shown screen depending on whether the torrents are loading. This should only ever be called on the UI
+ * thread.
+ * @param isLoading True if the list of torrents is (re)loading, false otherwise
+ */
+ public void updateIsLoading(boolean isLoading) {
+ this.isLoading = isLoading;
+ if (isLoading) {
+ clear(true, false); // Indirectly also calls updateViewVisibility()
+ } else {
+ updateViewVisibility();
+ }
+ }
+
+ /**
+ * Updates the shown screen depending on whether a connection error occurred. This should only ever be called on the
+ * UI thread.
+ * @param connectionErrorMessage The error message from the last failed connection attempt, or null to clear the
+ * visible error text
+ */
+ public void updateError(String connectionErrorMessage) {
+ this.connectionErrorMessage = connectionErrorMessage;
+ errorText.setText(connectionErrorMessage);
+ if (connectionErrorMessage != null) {
+ clear(false, false); // Indirectly also calls updateViewVisibility()
+ } else {
+ updateViewVisibility();
+ }
+ }
+
+ private void updateViewVisibility() {
+ if (!hasAConnection) {
+ torrentsList.setVisibility(View.GONE);
+ emptyText.setVisibility(View.GONE);
+ loadingProgress.setVisibility(View.GONE);
+ errorText.setVisibility(View.GONE);
+ nosettingsText.setVisibility(View.VISIBLE);
+ return;
+ }
+ boolean isEmpty = torrents == null || torrentsList.getAdapter().isEmpty();
+ boolean hasError = connectionErrorMessage != null;
+ nosettingsText.setVisibility(View.GONE);
+ errorText.setVisibility(hasError ? View.VISIBLE : View.GONE);
+ torrentsList.setVisibility(!hasError && !isLoading && !isEmpty ? View.VISIBLE : View.GONE);
+ loadingProgress.setVisibility(!hasError && isLoading ? View.VISIBLE : View.GONE);
+ emptyText.setVisibility(!hasError && !isLoading && isEmpty ? View.VISIBLE : View.GONE);
+ }
+
+ /**
+ * Returns the object responsible for executing torrent tasks against a connected server
+ * @return The executor for tasks on some torrent
+ */
+ private TorrentTasksExecutor getTasksExecutor() {
+ // NOTE: Assumes the activity implements all the required torrent tasks
+ return (TorrentTasksExecutor) getActivity();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/lists/DetailsAdapter.java b/core/src/org/transdroid/core/gui/lists/DetailsAdapter.java
new file mode 100644
index 00000000..fcb8d41d
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/DetailsAdapter.java
@@ -0,0 +1,182 @@
+package org.transdroid.core.gui.lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.transdroid.core.R;
+import org.transdroid.core.gui.navigation.FilterSeparatorView_;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.TorrentFile;
+
+import android.content.Context;
+import android.text.util.Linkify;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * List adapter that holds a header view showing torrent details and show the list list contained by the torrent.
+ * @author Eric Kok
+ */
+public class DetailsAdapter extends MergeAdapter {
+
+ private ViewHolderAdapter torrentDetailsViewAdapter = null;
+ private TorrentDetailsView torrentDetailsView = null;
+ private ViewHolderAdapter trackersSeparatorAdapter = null;
+ private SimpleListItemAdapter trackersAdapter = null;
+ private ViewHolderAdapter errorsSeparatorAdapter = null;
+ private SimpleListItemAdapter errorsAdapter = null;
+ private ViewHolderAdapter torrentFilesSeparatorAdapter = null;
+ private TorrentFilesAdapter torrentFilesAdapter = null;
+
+ public DetailsAdapter(Context context) {
+ // Immediately bind the adapters, or the MergeAdapter will not be able to determine the view types and instead
+ // display nothing at all
+
+ // Torrent details header
+ torrentDetailsView = TorrentDetailsView_.build(context);
+ torrentDetailsViewAdapter = new ViewHolderAdapter(torrentDetailsView);
+ torrentDetailsViewAdapter.setViewEnabled(false);
+ torrentDetailsViewAdapter.setViewVisibility(View.GONE);
+ addAdapter(torrentDetailsViewAdapter);
+
+ // Tracker errors
+ errorsSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
+ context.getString(R.string.status_errors)));
+ errorsSeparatorAdapter.setViewEnabled(false);
+ errorsSeparatorAdapter.setViewVisibility(View.GONE);
+ addAdapter(errorsSeparatorAdapter);
+ this.errorsAdapter = new SimpleListItemAdapter(context, new ArrayList());
+ this.errorsAdapter.setAutoLinkMask(Linkify.WEB_URLS);
+ addAdapter(errorsAdapter);
+
+ // Trackers
+ trackersSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
+ context.getString(R.string.status_trackers)));
+ trackersSeparatorAdapter.setViewEnabled(false);
+ trackersSeparatorAdapter.setViewVisibility(View.GONE);
+ addAdapter(trackersSeparatorAdapter);
+ this.trackersAdapter = new SimpleListItemAdapter(context, new ArrayList());
+ addAdapter(trackersAdapter);
+
+ // Torrent files
+ torrentFilesSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
+ context.getString(R.string.status_files)));
+ torrentFilesSeparatorAdapter.setViewEnabled(false);
+ torrentFilesSeparatorAdapter.setViewVisibility(View.GONE);
+ addAdapter(torrentFilesSeparatorAdapter);
+ this.torrentFilesAdapter = new TorrentFilesAdapter(context, new ArrayList());
+ addAdapter(torrentFilesAdapter);
+
+ }
+
+ /**
+ * Update the torrent data in the details header of this merge adapter
+ * @param torrent The torrent for which detailed data is shown
+ */
+ public void updateTorrent(Torrent torrent) {
+ torrentDetailsView.update(torrent);
+ torrentDetailsViewAdapter.setViewVisibility(torrent == null ? View.GONE : View.VISIBLE);
+ }
+
+ /**
+ * Update the list of files contained in this torrent
+ * @param torrentFiles The new list of files, or null if the list and header should be hidden
+ */
+ public void updateTorrentFiles(List torrentFiles) {
+ if (torrentFiles == null) {
+ torrentFilesAdapter.update(new ArrayList());
+ torrentFilesSeparatorAdapter.setViewVisibility(View.GONE);
+ } else {
+ torrentFilesAdapter.update(torrentFiles);
+ torrentFilesSeparatorAdapter.setViewVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Update the list of trackers
+ * @param trackers The new list of trackers known for this torrent, or null if the list and header should be hidden
+ */
+ public void updateTrackers(List extends SimpleListItem> trackers) {
+ if (trackers == null || trackers.isEmpty()) {
+ trackersAdapter.update(new ArrayList());
+ trackersSeparatorAdapter.setViewVisibility(View.GONE);
+ } else {
+ trackersAdapter.update(trackers);
+ trackersSeparatorAdapter.setViewVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Update the list of errors
+ * @param errors The new list of errors known for this torrent, or null if the list and header should be hidden
+ */
+ public void updateErrors(List extends SimpleListItem> errors) {
+ if (errors == null || errors.isEmpty()) {
+ errorsAdapter.update(new ArrayList());
+ errorsSeparatorAdapter.setViewVisibility(View.GONE);
+ } else {
+ errorsAdapter.update(errors);
+ errorsSeparatorAdapter.setViewVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Clear currently visible torrent, including header and shown lists
+ */
+ public void clear() {
+ updateTorrent(null);
+ updateTorrentFiles(null);
+ updateErrors(null);
+ updateTrackers(null);
+ }
+
+ protected static class TorrentFilesAdapter extends BaseAdapter {
+
+ private final Context context;
+ private List items;
+
+ public TorrentFilesAdapter(Context context, List items) {
+ this.context = context;
+ this.items = items;
+ }
+
+ /**
+ * Allows updating of the full data list underlying this adapter, replacing all items
+ * @param newItems The new list of files to display
+ */
+ public void update(List newItems) {
+ this.items = newItems;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return items.size();
+ }
+
+ @Override
+ public TorrentFile getItem(int position) {
+ return items.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TorrentFileView torrentFileView;
+ if (convertView == null) {
+ torrentFileView = TorrentFileView_.build(context);
+ } else {
+ torrentFileView = (TorrentFileView) convertView;
+ }
+ torrentFileView.bind(getItem(position));
+ return torrentFileView;
+ }
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/lists/LocalTorrent.java b/core/src/org/transdroid/core/gui/lists/LocalTorrent.java
new file mode 100644
index 00000000..76722530
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/LocalTorrent.java
@@ -0,0 +1,233 @@
+package org.transdroid.core.gui.lists;
+
+import java.util.Locale;
+
+import org.transdroid.core.R;
+import org.transdroid.daemon.DaemonException;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.TorrentStatus;
+import org.transdroid.daemon.util.FileSizeConverter;
+import org.transdroid.daemon.util.TimespanConverter;
+
+import android.content.res.Resources;
+
+/**
+ * Wrapper around Torrent to provide some addition getters that give translatable or otherwise formatted Strings of
+ * torrent statistics.
+ * @author Eric Kok
+ */
+public class LocalTorrent {
+
+ /**
+ * Creates the LocalTorrent object so that the translatable/formattable version of a Torrent can be used.
+ * @param torrent The Torrent object
+ * @return The torrent wrapped as LocalTorrent object
+ */
+ public static LocalTorrent fromTorrent(Torrent torrent) {
+ return new LocalTorrent(torrent);
+ }
+
+ private final Torrent t;
+
+ private LocalTorrent(Torrent torrent) {
+ this.t = torrent;
+ }
+
+ private static final String DECIMAL_FORMATTER = "%.1f";
+
+ /**
+ * Builds a string showing the upload/download seed ratio. If not downloading, it will base the ratio on the total
+ * size; so if you created the torrent yourself you will have downloaded 0 bytes, but the ratio will pretend you
+ * have 100%.
+ * @return A nicely formatted string containing the upload/download seed ratio
+ */
+ public String getRatioString() {
+ long baseSize = t.getTotalSize();
+ if (t.getStatusCode() == TorrentStatus.Downloading) {
+ baseSize = t.getDownloadedEver();
+ }
+ if (baseSize <= 0) {
+ return String.format(Locale.getDefault(), DECIMAL_FORMATTER, 0d);
+ } else if (t.getRatio() == Double.POSITIVE_INFINITY) {
+ return "\u221E";
+ } else {
+ return String.format(Locale.getDefault(), DECIMAL_FORMATTER, t.getRatio());
+ }
+ }
+
+ /**
+ * Returns a formatted string indicating the current progress in terms of transferred bytes
+ * @param r The context resources, to access translations
+ * @param withAvailability Whether to show file availability in-line
+ * @return A nicely formatted string indicating torrent status and, if applicable, progress in bytes
+ */
+ public String getProgressSizeText(Resources r, boolean withAvailability) {
+
+ switch (t.getStatusCode()) {
+ case Waiting:
+ case Checking:
+ case Error:
+ // Not downloading yet
+ return r.getString(R.string.status_waitingtodl, FileSizeConverter.getSize(t.getTotalSize()));
+ case Downloading:
+ // Downloading
+ return r.getString(
+ R.string.status_size1,
+ FileSizeConverter.getSize(t.getDownloadedEver()),
+ FileSizeConverter.getSize(t.getTotalSize()),
+ String.format(DECIMAL_FORMATTER, t.getDownloadedPercentage() * 100)
+ + "%"
+ + (!withAvailability ? "" : "/"
+ + String.format(DECIMAL_FORMATTER, t.getAvailability() * 100) + "%"));
+ case Seeding:
+ case Paused:
+ case Queued:
+ // Seeding or paused
+ return r.getString(R.string.status_size2, FileSizeConverter.getSize(t.getTotalSize()),
+ FileSizeConverter.getSize(t.getUploadedEver()));
+ default:
+ return "";
+ }
+
+ }
+
+ /**
+ * Returns a formatted string indicating either the expected time to download (ETA) or, when seeding, the ratio
+ * @param r The context resources, to access translations
+ * @return A string like '~ 34 seconds', or 'RATIO 8.2' or an empty string
+ */
+ public String getProgressEtaRatioText(Resources r) {
+ switch (t.getStatusCode()) {
+ case Downloading:
+ // Downloading
+ return getRemainingTimeString(r, true, false);
+ case Seeding:
+ case Paused:
+ case Queued:
+ // Seeding or paused
+ return r.getString(R.string.status_ratio, getRatioString());
+ case Waiting:
+ case Checking:
+ case Error:
+ default:
+ return "";
+ }
+ }
+
+ /**
+ * Returns a formatted string indicating the torrent status and connected peers
+ * @param r The context resources, to access translations
+ * @return A string like 'Queued' or, when seeding or leeching, '2 OF 28 PEERS'
+ */
+ public String getProgressConnectionText(Resources r) {
+
+ switch (t.getStatusCode()) {
+ case Waiting:
+ return r.getString(R.string.status_waiting);
+ case Checking:
+ return r.getString(R.string.status_checking);
+ case Downloading:
+ return r.getString(R.string.status_peers, t.getPeersSendingToUs(), t.getPeersConnected());
+ case Seeding:
+ return r.getString(R.string.status_peers, t.getPeersGettingFromUs(), t.getPeersConnected());
+ case Paused:
+ return r.getString(R.string.status_paused);
+ case Queued:
+ return r.getString(R.string.status_stopped);
+ case Error:
+ return r.getString(R.string.status_error);
+ default:
+ return r.getString(R.string.status_unknown);
+ }
+
+ }
+
+ /**
+ * Returns a formatted string indicating current transfer speeds for the torrent
+ * @param r The context resources, to access translations
+ * @return A string like '↓ 28KB/s ↑ 1.8MB/s', or an empty string when not transferrring
+ */
+ public String getProgressSpeedText(Resources r) {
+
+ switch (t.getStatusCode()) {
+ case Waiting:
+ case Checking:
+ case Paused:
+ case Queued:
+ return "";
+ case Downloading:
+ return r.getString(R.string.status_speed_down, FileSizeConverter.getSize(t.getRateDownload()) + "/s") + " "
+ + r.getString(R.string.status_speed_up, FileSizeConverter.getSize(t.getRateUpload()) + "/s");
+ case Seeding:
+ return r.getString(R.string.status_speed_up, FileSizeConverter.getSize(t.getRateUpload()) + "/s");
+ default:
+ return "";
+ }
+
+ }
+
+ public String getProgressStatusEta(Resources r) {
+ switch (t.getStatusCode()) {
+ case Waiting:
+ return r.getString(R.string.status_waiting).toUpperCase(Locale.getDefault());
+ case Checking:
+ return r.getString(R.string.status_checking).toUpperCase(Locale.getDefault());
+ case Error:
+ return r.getString(R.string.status_error).toUpperCase(Locale.getDefault());
+ case Downloading:
+ // Downloading
+ return r.getString(R.string.status_downloading).toUpperCase(Locale.getDefault()) + " ("
+ + String.format(DECIMAL_FORMATTER, t.getDownloadedPercentage() * 100) + "%), "
+ + getRemainingTimeString(r, false, true);
+ case Seeding:
+ return r.getString(R.string.status_seeding).toUpperCase(Locale.getDefault());
+ case Paused:
+ return r.getString(R.string.status_paused).toUpperCase(Locale.getDefault());
+ case Queued:
+ return r.getString(R.string.status_queued).toUpperCase(Locale.getDefault());
+ default:
+ return r.getString(R.string.status_unknown).toUpperCase(Locale.getDefault());
+ }
+ }
+
+ /**
+ * Returns a formatted string indicating the remaining download time
+ * @param r The context resources, to access translations
+ * @param inDays Whether to show days or use hours for > 24 hours left instead
+ * @return A string like '4d 8h 34m 5s' or '2m 3s'
+ */
+ public String getRemainingTimeString(Resources r, boolean abbreviate, boolean inDays) {
+ if (t.getEta() == -1 || t.getEta() == -2) {
+ return r.getString(R.string.status_unknowneta);
+ }
+ return r.getString(abbreviate ? R.string.status_eta : R.string.status_etalong,
+ TimespanConverter.getTime(t.getEta(), inDays));
+ }
+
+ /**
+ * Convert a DaemonException to a translatable human-readable error message
+ * @param e The exception that was thrown by the server
+ * @return A string resource ID to show to the user
+ */
+ public static int getResourceForDaemonException(DaemonException e) {
+ switch (e.getType()) {
+ case MethodUnsupported:
+ return R.string.error_jsonrequesterror;
+ case ConnectionError:
+ return R.string.error_httperror;
+ case UnexpectedResponse:
+ return R.string.error_jsonresponseerror;
+ case ParsingFailed:
+ return R.string.error_jsonrequesterror;
+ case NotConnected:
+ return R.string.error_daemonnotconnected;
+ case AuthenticationFailure:
+ return R.string.error_401;
+ case FileAccessError:
+ return R.string.error_torrentfile;
+ default:
+ return R.string.error_httperror;
+ }
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/lists/MergeAdapter.java b/core/src/org/transdroid/core/gui/lists/MergeAdapter.java
new file mode 100644
index 00000000..bf4db11a
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/MergeAdapter.java
@@ -0,0 +1,298 @@
+package org.transdroid.core.gui.lists;
+
+import java.util.ArrayList;
+
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+
+/**
+ * An adapter that can contain many other adapters and shows them in sequence. Taken from
+ * http://stackoverflow.com/questions/7964259/android-attaching-multiple-adapters-to-one-adapter and based on the Apache
+ * 2-licensed CommonsWare MergeAdapter.
+ * @author Eric Kok
+ * @author Alex Amiryan
+ * @author Mark Murphy
+ */
+public class MergeAdapter extends BaseAdapter implements SectionIndexer {
+
+ protected ArrayList pieces = new ArrayList();
+ protected String noItemsText;
+
+ /**
+ * Stock constructor, simply chaining to the superclass.
+ */
+ public MergeAdapter() {
+ super();
+ }
+
+ /**
+ * Adds a new adapter to the roster of things to appear in the aggregate list.
+ * @param adapter Source for row views for this section
+ */
+ public void addAdapter(ListAdapter adapter) {
+ pieces.add(adapter);
+ adapter.registerDataSetObserver(new CascadeDataSetObserver());
+ }
+
+ /**
+ * Get the data item associated with the specified position in the data set.
+ * @param position Position of the item whose data we want
+ */
+ public Object getItem(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.getItem(position));
+ }
+
+ position -= size;
+ }
+
+ return (null);
+ }
+
+ public void setNoItemsText(String text) {
+ noItemsText = text;
+ }
+
+ /**
+ * Get the adapter associated with the specified position in the data set.
+ * @param position Position of the item whose adapter we want
+ */
+ public ListAdapter getAdapter(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece);
+ }
+
+ position -= size;
+ }
+
+ return (null);
+ }
+
+ /**
+ * How many items are in the data set represented by this {@link Adapter}.
+ */
+ public int getCount() {
+ int total = 0;
+
+ for (ListAdapter piece : pieces) {
+ total += piece.getCount();
+ }
+
+ if (total == 0 && noItemsText != null) {
+ total = 1;
+ }
+
+ return (total);
+ }
+
+ /**
+ * Returns the number of types of {@link View}s that will be created by {@link #getView(int, View, ViewGroup)}.
+ */
+ @Override
+ public int getViewTypeCount() {
+ int total = 0;
+
+ for (ListAdapter piece : pieces) {
+ total += piece.getViewTypeCount();
+ }
+
+ return (Math.max(total, 1)); // needed for setListAdapter() before
+ // content add'
+ }
+
+ /**
+ * Get the type of {@link View} that will be created by {@link #getView(int, View, ViewGroup)} for the specified item.
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public int getItemViewType(int position) {
+ int typeOffset = 0;
+ int result = -1;
+
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ result = typeOffset + piece.getItemViewType(position);
+ break;
+ }
+
+ position -= size;
+ typeOffset += piece.getViewTypeCount();
+ }
+
+ return (result);
+ }
+
+ /**
+ * Are all items in this {@link ListAdapter} enabled? If yes it means all items are selectable and clickable.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (false);
+ }
+
+ /**
+ * Returns true if the item at the specified position is not a separator.
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.isEnabled(position));
+ }
+
+ position -= size;
+ }
+
+ return (false);
+ }
+
+ /**
+ * Get a {@link View} that displays the data at the specified position in the data set.
+ * @param position Position of the item whose data we want
+ * @param convertView View to recycle, if not null
+ * @param parent ViewGroup containing the returned View
+ */
+ public View getView(int position, View convertView, ViewGroup parent) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+
+ return (piece.getView(position, convertView, parent));
+ }
+
+ position -= size;
+ }
+
+ if (noItemsText != null) {
+ TextView text = new TextView(parent.getContext());
+ text.setText(noItemsText);
+ return text;
+ }
+
+ return (null);
+ }
+
+ /**
+ * Get the row id associated with the specified position in the list.
+ * @param position Position of the item whose data we want
+ */
+ public long getItemId(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.getItemId(position));
+ }
+
+ position -= size;
+ }
+
+ return (-1);
+ }
+
+ public final int getPositionForSection(int section) {
+ int position = 0;
+
+ for (ListAdapter piece : pieces) {
+ if (piece instanceof SectionIndexer) {
+ Object[] sections = ((SectionIndexer) piece).getSections();
+ int numSections = 0;
+
+ if (sections != null) {
+ numSections = sections.length;
+ }
+
+ if (section < numSections) {
+ return (position + ((SectionIndexer) piece).getPositionForSection(section));
+ } else if (sections != null) {
+ section -= numSections;
+ }
+ }
+
+ position += piece.getCount();
+ }
+
+ return (0);
+ }
+
+ public final int getSectionForPosition(int position) {
+ int section = 0;
+
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ if (piece instanceof SectionIndexer) {
+ return (section + ((SectionIndexer) piece).getSectionForPosition(position));
+ }
+
+ return (0);
+ } else {
+ if (piece instanceof SectionIndexer) {
+ Object[] sections = ((SectionIndexer) piece).getSections();
+
+ if (sections != null) {
+ section += sections.length;
+ }
+ }
+ }
+
+ position -= size;
+ }
+
+ return (0);
+ }
+
+ public final Object[] getSections() {
+ ArrayList sections = new ArrayList();
+
+ for (ListAdapter piece : pieces) {
+ if (piece instanceof SectionIndexer) {
+ Object[] curSections = ((SectionIndexer) piece).getSections();
+
+ if (curSections != null) {
+ for (Object section : curSections) {
+ sections.add(section);
+ }
+ }
+ }
+ }
+
+ if (sections.size() == 0) {
+ return (null);
+ }
+
+ return (sections.toArray(new Object[0]));
+ }
+
+ private class CascadeDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ notifyDataSetInvalidated();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/org/transdroid/core/gui/lists/SimpleListItem.java b/core/src/org/transdroid/core/gui/lists/SimpleListItem.java
new file mode 100644
index 00000000..cd4627c6
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/SimpleListItem.java
@@ -0,0 +1,12 @@
+package org.transdroid.core.gui.lists;
+
+
+/**
+ * Represents a filter item as shown in the navigation list or spinner.
+ * @author Eric Kok
+ */
+public interface SimpleListItem {
+
+ public String getName();
+
+}
diff --git a/core/src/org/transdroid/core/gui/lists/SimpleListItemAdapter.java b/core/src/org/transdroid/core/gui/lists/SimpleListItemAdapter.java
new file mode 100644
index 00000000..66843a73
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/SimpleListItemAdapter.java
@@ -0,0 +1,98 @@
+package org.transdroid.core.gui.lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+public class SimpleListItemAdapter extends BaseAdapter {
+
+ private final Context context;
+ private List extends SimpleListItem> items;
+ private int autoLinkMask = 0;
+
+ public SimpleListItemAdapter(Context context, List extends SimpleListItem> items) {
+ this.context = context;
+ this.items = items;
+ }
+
+ /**
+ * Allows updating of the full data list underlying this adapter, replacing all items
+ * @param newItems The new list of simple list items to display
+ */
+ public void update(List extends SimpleListItem> newItems) {
+ this.items = newItems;
+ notifyDataSetChanged();
+ }
+
+ public void setAutoLinkMask(int autoLinkMask) {
+ this.autoLinkMask = autoLinkMask;
+ }
+
+ @Override
+ public int getCount() {
+ return items.size();
+ }
+
+ @Override
+ public SimpleListItem getItem(int position) {
+ return items.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ SimpleListItemView filterItemView;
+ if (convertView == null || !(convertView instanceof SimpleListItemView)) {
+ filterItemView = SimpleListItemView_.build(context);
+ } else {
+ filterItemView = (SimpleListItemView) convertView;
+ }
+ filterItemView.bind(getItem(position), autoLinkMask);
+ return filterItemView;
+ }
+
+ /**
+ * Represents a very simple list item that only contains a single string to show in the list. Use wrapStringsList to
+ * wrap an existing list of strings into a list of {@link SimpleListItem}s.
+ * @author Eric Kok
+ */
+ public static class SimpleStringItem implements SimpleListItem {
+
+ /**
+ * Wraps a simple string of strings into a list of SimpleStringItem to add as data to a
+ * {@link SimpleListItemAdapter}
+ * @param strings A list of string
+ * @return A list of SimpleStringItem objects representing the input strings
+ */
+ public static List wrapStringsList(List strings) {
+ ArrayList errors = new ArrayList();
+ if (strings != null) {
+ for (String string : strings) {
+ errors.add(new SimpleStringItem(string));
+ }
+ }
+ return errors;
+ }
+
+ private final String string;
+
+ public SimpleStringItem(String string) {
+ this.string = string;
+ }
+
+ @Override
+ public String getName() {
+ return this.string;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/org/transdroid/core/gui/lists/SimpleListItemView.java b/core/src/org/transdroid/core/gui/lists/SimpleListItemView.java
new file mode 100644
index 00000000..74601c31
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/SimpleListItemView.java
@@ -0,0 +1,30 @@
+package org.transdroid.core.gui.lists;
+
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+/**
+ * View that represents some {@link SimpleListItem} object and simple prints out the text (in proper style)
+ * @author Eric Kok
+ */
+@EViewGroup(resName="list_item_simple")
+public class SimpleListItemView extends FrameLayout {
+
+ @ViewById
+ protected TextView itemText;
+
+ public SimpleListItemView(Context context) {
+ super(context);
+ }
+
+ public void bind(SimpleListItem filterItem, int autoLinkMask) {
+ itemText.setText(filterItem.getName());
+ if (autoLinkMask > 0)
+ itemText.setAutoLinkMask(autoLinkMask);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/lists/TorrentDetailsView.java b/core/src/org/transdroid/core/gui/lists/TorrentDetailsView.java
new file mode 100644
index 00000000..e187c029
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/TorrentDetailsView.java
@@ -0,0 +1,94 @@
+package org.transdroid.core.gui.lists;
+
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.core.R;
+import org.transdroid.daemon.Daemon;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.util.FileSizeConverter;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+/**
+ * Represents a group of views that show torrent status, sizes, speeds and other details.
+ * @author Eric Kok
+ */
+@EViewGroup(resName="fragment_details_header")
+public class TorrentDetailsView extends RelativeLayout {
+
+ @ViewById
+ protected TextView labelText, dateaddedText, uploadedText, uploadedunitText, ratioText, upspeedText, seedersText,
+ downloadedunitText, downloadedText, totalsizeText, downspeedText, leechersText, statusText;
+ @ViewById
+ protected TorrentStatusLayout statusLayout;
+
+ public TorrentDetailsView(Context context) {
+ super(context);
+ }
+
+ /**
+ * Update the text fields with new/updated torrent details
+ * @param torrent The torrent for which to show details
+ */
+ public void update(Torrent torrent) {
+
+ if (torrent == null) {
+ return;
+ }
+
+ LocalTorrent local = LocalTorrent.fromTorrent(torrent);
+
+ // Set label text
+ if (Daemon.supportsLabels(torrent.getDaemon())) {
+ if (TextUtils.isEmpty(torrent.getLabelName())) {
+ labelText.setText(getResources().getString(R.string.labels_unlabeled));
+ } else {
+ labelText.setText(torrent.getLabelName());
+ }
+ labelText.setVisibility(View.VISIBLE);
+ } else {
+ labelText.setVisibility(View.INVISIBLE);
+ }
+
+ // Set status texts
+ if (torrent.getDateAdded() != null) {
+ dateaddedText.setText(getResources().getString(
+ R.string.status_sincedate,
+ DateUtils.getRelativeDateTimeString(getContext(), torrent.getDateAdded().getTime(),
+ DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_ABBREV_MONTH)));
+ dateaddedText.setVisibility(View.VISIBLE);
+ } else {
+ dateaddedText.setVisibility(View.INVISIBLE);
+ }
+
+ statusLayout.setStatus(torrent.getStatusCode());
+ statusText.setText(getResources().getString(R.string.status_status, local.getProgressStatusEta(getResources())));
+ ratioText.setText(getResources().getString(R.string.status_ratio, local.getRatioString()));
+ // TODO: Implement separate numbers of seeders and leechers
+ seedersText.setText(getResources().getString(R.string.status_peers, torrent.getPeersSendingToUs(),
+ torrent.getPeersConnected()));
+ leechersText.setText(getResources().getString(R.string.status_peers, torrent.getPeersSendingToUs(),
+ torrent.getPeersConnected()));
+ // TODO: Add field that displays torrent errors (as opposed to tracker errors)
+ // TODO: Add field that displays availability
+
+ // Sizes and speeds texts
+ totalsizeText.setText(getResources().getString(R.string.status_ofsize,
+ FileSizeConverter.getSize(torrent.getTotalSize())));
+ downloadedText.setText(FileSizeConverter.getSize(torrent.getDownloadedEver(), false));
+ downloadedunitText.setText(FileSizeConverter.getSizeUnit(torrent.getDownloadedEver()).toString());
+ uploadedText.setText(FileSizeConverter.getSize(torrent.getUploadedEver(), false));
+ uploadedunitText.setText(FileSizeConverter.getSizeUnit(torrent.getUploadedEver()).toString());
+ downspeedText.setText(getResources().getString(R.string.status_speed_down_details,
+ FileSizeConverter.getSize(torrent.getRateDownload()) + "/s"));
+ upspeedText.setText(getResources().getString(R.string.status_speed_up,
+ FileSizeConverter.getSize(torrent.getRateUpload()) + "/s"));
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/lists/TorrentFilePriorityLayout.java b/core/src/org/transdroid/core/gui/lists/TorrentFilePriorityLayout.java
new file mode 100644
index 00000000..1cd2b6f3
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/TorrentFilePriorityLayout.java
@@ -0,0 +1,83 @@
+package org.transdroid.core.gui.lists;
+
+import org.transdroid.daemon.Priority;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import fr.marvinlabs.widget.CheckableRelativeLayout;
+
+/**
+ * A relative layout that that is checkable (to be used in a contextual action bar) and shows a coloured bar in the far
+ * left indicating the priority of the represented file. The darker the green, the higher the priority, while grey means
+ * the file isn't downloaded at all.
+ * @author Eric Kok
+ */
+public class TorrentFilePriorityLayout extends CheckableRelativeLayout {
+
+ private final float scale = getContext().getResources().getDisplayMetrics().density;
+ private final int WIDTH = (int) (6 * scale + 0.5f);
+
+ private Priority priority = null;
+ private final Paint offPaint = new Paint();
+ private final Paint lowPaint = new Paint();
+ private final Paint highPaint = new Paint();
+ private final Paint normalPaint = new Paint();
+ private final RectF fullRect = new RectF();
+
+ public TorrentFilePriorityLayout(Context context) {
+ super(context);
+ initPaints();
+ setWillNotDraw(false);
+ }
+
+ public TorrentFilePriorityLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initPaints();
+ setWillNotDraw(false);
+ }
+
+ private void initPaints() {
+ offPaint.setColor(0xFF9E9E9E); // Grey
+ lowPaint.setColor(0xFFC8E88E); // Light green
+ normalPaint.setColor(0xFF8ACC12); // Normal green
+ highPaint.setColor(0xFF4B6617); // Dark green
+ }
+
+ public void setPriority(Priority priority) {
+ this.priority = priority;
+ this.invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ int height = getHeight();
+ int width = WIDTH;
+ fullRect.set(0, 0, width, height);
+
+ if (priority == null) {
+ return;
+ }
+
+ switch (priority) {
+ case Low:
+ canvas.drawRect(fullRect, lowPaint);
+ break;
+ case Normal:
+ canvas.drawRect(fullRect, normalPaint);
+ break;
+ case High:
+ canvas.drawRect(fullRect, highPaint);
+ break;
+ default: // Off
+ canvas.drawRect(fullRect, offPaint);
+ break;
+ }
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/lists/TorrentFileView.java b/core/src/org/transdroid/core/gui/lists/TorrentFileView.java
new file mode 100644
index 00000000..f7926c62
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/TorrentFileView.java
@@ -0,0 +1,31 @@
+package org.transdroid.core.gui.lists;
+
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.daemon.TorrentFile;
+
+import android.content.Context;
+import android.widget.TextView;
+
+/**
+ * View that represents some {@link TorrentFile} object and show the file's name, status and priority
+ * @author Eric Kok
+ */
+@EViewGroup(resName="list_item_torrentfile")
+public class TorrentFileView extends TorrentFilePriorityLayout {
+
+ @ViewById
+ protected TextView nameText, progressText, sizesText;
+
+ public TorrentFileView(Context context) {
+ super(context, null);
+ }
+
+ public void bind(TorrentFile torrentFile) {
+ nameText.setText(torrentFile.getName());
+ sizesText.setText(torrentFile.getDownloadedAndTotalSizeText());
+ progressText.setText(torrentFile.getProgressText());
+ setPriority(torrentFile.getPriority());
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/lists/TorrentProgressBar.java b/core/src/org/transdroid/core/gui/lists/TorrentProgressBar.java
new file mode 100644
index 00000000..c63103ec
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/TorrentProgressBar.java
@@ -0,0 +1,109 @@
+package org.transdroid.core.gui.lists;
+
+import org.transdroid.core.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * Draws a progress bar indicating the download progress as well as the torrent status.
+ *
+ * @author Eric Kok
+ */
+public class TorrentProgressBar extends View {
+
+ private final float scale = getContext().getResources().getDisplayMetrics().density;
+ private final int MINIMUM_HEIGHT = (int) (3 * scale + 0.5f);
+
+ private int progress;
+ private boolean isActive;
+ private boolean isError;
+ private final Paint notdonePaint = new Paint();
+ private final Paint inactiveDonePaint = new Paint();
+ private final Paint inactivePaint = new Paint();
+ private final Paint progressPaint = new Paint();
+ private final Paint donePaint = new Paint();
+ private final Paint errorPaint = new Paint();
+ private final RectF fullRect = new RectF();
+ private final RectF progressRect = new RectF();
+
+ public void setProgress(int progress) {
+ this.progress = progress;
+ this.invalidate();
+ }
+
+ public void setActive(boolean isActive) {
+ this.isActive = isActive;
+ this.invalidate();
+ }
+
+ public void setError(boolean isError) {
+ this.isError = isError;
+ this.invalidate();
+ }
+
+ public TorrentProgressBar(Context context) {
+ super(context);
+ initPaints();
+ }
+
+ public TorrentProgressBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initPaints();
+
+ // Parse any set attributes from XML
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TorrentProgressBar);
+ if (a.hasValue(R.styleable.TorrentProgressBar_progress)) {
+ this.progress = a.getIndex(R.styleable.TorrentProgressBar_progress);
+ this.isActive = a.getBoolean(R.styleable.TorrentProgressBar_isActive, false);
+ }
+ a.recycle();
+ }
+
+ private void initPaints() {
+ notdonePaint.setColor(0xFFEEEEEE); // Light grey
+ inactiveDonePaint.setColor(0xFFA759D4); // Purple
+ inactivePaint.setColor(0xFF9E9E9E); // Grey
+ progressPaint.setColor(0xFF42A8FA); // Blue
+ donePaint.setColor(0xFF8ACC12); // Green
+ errorPaint.setColor(0xFFDE3939); // Red
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int ws = MeasureSpec.getSize(widthMeasureSpec);
+ int hs = Math.max(getHeight(), MINIMUM_HEIGHT);
+ setMeasuredDimension(ws, hs);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ int height = getHeight();
+ int width = getWidth();
+ fullRect.set(0, 0, width, height);
+
+ // Error?
+ if (isError) {
+ canvas.drawRect(fullRect, errorPaint);
+ } else {
+ // Background rounded rectangle
+ canvas.drawRect(fullRect, notdonePaint);
+
+ // Foreground progress indicator
+ if (progress > 0) {
+ progressRect.set(0, 0, width * ((float) progress / 100), height);
+ canvas.drawRect(progressRect, (isActive ? (progress == 100 ? donePaint : progressPaint)
+ : (progress == 100 ? inactiveDonePaint : inactivePaint)));
+ }
+ }
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/lists/TorrentStatusLayout.java b/core/src/org/transdroid/core/gui/lists/TorrentStatusLayout.java
new file mode 100644
index 00000000..8f4d2865
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/TorrentStatusLayout.java
@@ -0,0 +1,93 @@
+package org.transdroid.core.gui.lists;
+
+import org.transdroid.daemon.TorrentStatus;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import fr.marvinlabs.widget.CheckableRelativeLayout;
+
+/**
+ * A relative layout that is checkable (to be used in a contextual action bar) and shows a coloured bar in the far left
+ * indicating the status of the represented torrent. Active downloads are blue, seeding torrents are green, errors are
+ * red, etc.
+ * @author Eric Kok
+ */
+public class TorrentStatusLayout extends CheckableRelativeLayout {
+
+ private final float scale = getContext().getResources().getDisplayMetrics().density;
+ private final int WIDTH = (int) (6 * scale + 0.5f);
+
+ private TorrentStatus status = null;
+ private final Paint inactiveDonePaint = new Paint();
+ private final Paint inactivePaint = new Paint();
+ private final Paint progressPaint = new Paint();
+ private final Paint donePaint = new Paint();
+ private final Paint errorPaint = new Paint();
+ private final RectF fullRect = new RectF();
+
+ public TorrentStatusLayout(Context context) {
+ super(context);
+ initPaints();
+ setWillNotDraw(false);
+ }
+
+ public TorrentStatusLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initPaints();
+ setWillNotDraw(false);
+ }
+
+ private void initPaints() {
+ inactiveDonePaint.setColor(0xFFA759D4); // Purple
+ inactivePaint.setColor(0xFF9E9E9E); // Grey
+ progressPaint.setColor(0xFF42A8FA); // Blue
+ donePaint.setColor(0xFF8ACC12); // Green
+ errorPaint.setColor(0xFFDE3939); // Red
+ }
+
+ /**
+ * Registers the status of the represented torrent and invalidates the view so the status colour will be updated
+ * accordingly.
+ * @param status
+ */
+ public void setStatus(TorrentStatus status) {
+ this.status = status;
+ this.invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ int height = getHeight();
+ int width = WIDTH;
+ fullRect.set(0, 0, width, height);
+
+ if (status == null) {
+ return;
+ }
+
+ switch (status) {
+ case Downloading:
+ canvas.drawRect(fullRect, progressPaint);
+ break;
+ case Paused:
+ canvas.drawRect(fullRect, inactiveDonePaint);
+ break;
+ case Seeding:
+ canvas.drawRect(fullRect, donePaint);
+ break;
+ case Error:
+ canvas.drawRect(fullRect, errorPaint);
+ break;
+ default: // Checking, Waiting, Queued, Unknown
+ canvas.drawRect(fullRect, inactivePaint);
+ break;
+ }
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/lists/TorrentView.java b/core/src/org/transdroid/core/gui/lists/TorrentView.java
new file mode 100644
index 00000000..1572cb27
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/TorrentView.java
@@ -0,0 +1,58 @@
+package org.transdroid.core.gui.lists;
+
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.TorrentStatus;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * View that represents some {@link Torrent} object and displays progress, status, speeds, etc.
+ * @author Eric Kok
+ */
+@EViewGroup(resName = "list_item_torrent")
+public class TorrentView extends TorrentStatusLayout {
+
+ @ViewById
+ protected ImageView priorityImage;
+ @ViewById
+ protected TextView nameText, ratioText, progressText, speedText, peersText;
+ @ViewById
+ protected TorrentProgressBar torrentProgressbar;
+
+ public TorrentView(Context context) {
+ super(context);
+ }
+
+ public void bind(Torrent torrent) {
+ LocalTorrent local = LocalTorrent.fromTorrent(torrent);
+ setStatus(torrent.getStatusCode());
+ nameText.setText(torrent.getName());
+ progressText.setText(local.getProgressSizeText(getResources(), false));
+ ratioText.setText(local.getProgressEtaRatioText(getResources()));
+ // TODO: Implement per-torrent priority and set priorityImage
+ priorityImage.setVisibility(View.INVISIBLE);
+
+ // Only show status bar, peers and speed fields if relevant, i.e. when downloading or actively seeding
+ if (torrent.getStatusCode() == TorrentStatus.Downloading
+ || (torrent.getStatusCode() == TorrentStatus.Seeding && torrent.getRateUpload() > 0)) {
+ torrentProgressbar.setVisibility(View.VISIBLE);
+ torrentProgressbar.setProgress((int) (torrent.getDownloadedPercentage() * 100));
+ torrentProgressbar.setActive(torrent.canPause());
+ torrentProgressbar.setError(torrent.getStatusCode() == TorrentStatus.Error);
+ peersText.setVisibility(View.VISIBLE);
+ peersText.setText(local.getProgressConnectionText(getResources()));
+ speedText.setVisibility(View.VISIBLE);
+ speedText.setText(local.getProgressSpeedText(getResources()));
+ } else {
+ torrentProgressbar.setVisibility(View.GONE);
+ peersText.setVisibility(View.GONE);
+ speedText.setVisibility(View.GONE);
+ }
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/lists/TorrentsAdapter.java b/core/src/org/transdroid/core/gui/lists/TorrentsAdapter.java
new file mode 100644
index 00000000..5c634f1c
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/TorrentsAdapter.java
@@ -0,0 +1,71 @@
+package org.transdroid.core.gui.lists;
+
+import java.util.ArrayList;
+
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.RootContext;
+import org.transdroid.daemon.Torrent;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * Adapter that contains a list of torrent objects to show.
+ * @author Eric Kok
+ */
+@EBean
+public class TorrentsAdapter extends BaseAdapter {
+
+ private ArrayList torrents = null;
+
+ @RootContext
+ protected Context context;
+
+ /**
+ * Allows updating the full internal list of torrents at once, replacing the old list
+ * @param newTorrents The new list of torrent objects
+ */
+ public void update(ArrayList newTorrents) {
+ this.torrents = newTorrents;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ if (torrents == null)
+ return 0;
+ return torrents.size();
+ }
+
+ @Override
+ public Torrent getItem(int position) {
+ if (torrents == null)
+ return null;
+ return torrents.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TorrentView torrentView;
+ if (convertView == null) {
+ torrentView = TorrentView_.build(context);
+ } else {
+ torrentView = (TorrentView) convertView;
+ }
+ torrentView.bind(getItem(position));
+ return torrentView;
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/lists/ViewHolderAdapter.java b/core/src/org/transdroid/core/gui/lists/ViewHolderAdapter.java
new file mode 100644
index 00000000..626e4f58
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/ViewHolderAdapter.java
@@ -0,0 +1,107 @@
+package org.transdroid.core.gui.lists;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+
+/**
+ * A simple wrapper adapter around a single view, typically for use in a {@link MergeAdapter}. Notably, this adapter
+ * handles the proper showing or hiding of the view according to the contained view's visibility if
+ * {@link #setViewVisibility(int)} is used on this adapter rather than setting the visibility of the view directly on
+ * the view object. This is required since otherwise the adapter's consumer (i.e. a {@link ListView}) does not update
+ * the list row accordingly. Use {@link #setViewEnabled(boolean)} to enable or disable this contained view for user
+ * interaction.
+ * @author Eric Kok
+ */
+public class ViewHolderAdapter extends BaseAdapter {
+
+ private final View view;
+
+ /**
+ * Instantiates this wrapper adapter with the one and only view to show. It can not be updated and view visibility
+ * should be set directly on this adapter using {@link #setViewVisibility(int)}. Use
+ * {@link #setViewEnabled(boolean)} to enable or disable this contained view for user interaction.
+ * @param view The view that will be wrapper in an adapter to show in a list view
+ */
+ public ViewHolderAdapter(View view) {
+ this.view = view;
+ }
+
+ /**
+ * Sets the visibility on the contained view and notifies consumers of this adapter (i.e. a {@link ListView})
+ * accordingly. Use {@link View#GONE} to hide this adapter's view altogether.
+ * @param visibility The visibility to set on the contained view
+ */
+ public void setViewVisibility(int visibility) {
+ view.setVisibility(visibility);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Sets whether the contained view should be enabled and notifies consumers of this adapter (i.e. a {@link ListView}
+ * ) accordingly. A contained enabled view allows user interaction (clicks, focus), while a disabled view does not.
+ * @param enabled Whether the contained view should be enabled
+ */
+ public void setViewEnabled(boolean enabled) {
+ view.setEnabled(enabled);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Returns 1 if the contained view is {@link View#VISIBLE} or {@link View#INVISIBLE}, return 0 if {@link View#GONE}.
+ */
+ @Override
+ public int getCount() {
+ return view.getVisibility() == View.VISIBLE ? 1 : 0;
+ }
+
+ /**
+ * Always directly returns the single contained view instance.
+ */
+ @Override
+ public Object getItem(int position) {
+ return view;
+ }
+
+ /**
+ * Always returns the position directly as item id.
+ */
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ /**
+ * Always directly returns the single contained view instance.
+ */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return view;
+ }
+
+ /**
+ * Always returns true, as there is only one contained item and it is never changed.
+ */
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * Returns false, as the contained view can still be enabled and disabled.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ /**
+ * Returns true if the contained view is enabled, returns false otherwise.
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ return view.isEnabled();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/log/DatabaseHelper.java b/core/src/org/transdroid/core/gui/log/DatabaseHelper.java
new file mode 100644
index 00000000..d95cd00a
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/log/DatabaseHelper.java
@@ -0,0 +1,51 @@
+package org.transdroid.core.gui.log;
+
+import java.sql.SQLException;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper;
+import com.j256.ormlite.support.ConnectionSource;
+import com.j256.ormlite.table.TableUtils;
+
+/**
+ * Helper to access the database to access persisting objects.
+ * @author Eric Kok
+ */
+public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
+
+ private static final String DATABASE_NAME = "transdroid.db";
+ private static final int DATABASE_VERSION = 1;
+
+ public DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource) {
+ try {
+ TableUtils.createTable(connectionSource, ErrorLogEntry.class);
+ } catch (SQLException e) {
+ Log.e(org.transdroid.core.gui.log.Log.LOG_NAME, "Could not create new table for ErrorLogEntry", e);
+ }
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase sqLiteDatabase, ConnectionSource connectionSource, int oldVersion,
+ int newVersion) {
+ try {
+ switch (oldVersion) {
+ case 1:
+ TableUtils.createTable(connectionSource, ErrorLogEntry.class);
+ /*case 1:
+ etc...*/
+ }
+
+ } catch (SQLException e) {
+ Log.e(org.transdroid.core.gui.log.Log.LOG_NAME, "Could not upgrade the table for ErrorLogEntry", e);
+ }
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/log/ErrorLogEntry.java b/core/src/org/transdroid/core/gui/log/ErrorLogEntry.java
new file mode 100644
index 00000000..b14656a2
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/log/ErrorLogEntry.java
@@ -0,0 +1,92 @@
+package org.transdroid.core.gui.log;
+
+import java.util.Date;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.j256.ormlite.field.DatabaseField;
+import com.j256.ormlite.table.DatabaseTable;
+
+/**
+ * Represents an error log entry to be registered in the database.
+ * @author Eric Kok
+ */
+@DatabaseTable(tableName = "ErrorLogEntry")
+public class ErrorLogEntry implements Parcelable {
+
+ public static final String ID = "logId";
+ public static final String DATEANDTIME = "dateAndTime";
+
+ @DatabaseField(id = true, columnName = ID)
+ private Integer logId;
+ @DatabaseField(columnName = DATEANDTIME)
+ private Date dateAndTime;
+ @DatabaseField
+ private Integer priority;
+ @DatabaseField
+ private String tag;
+ @DatabaseField
+ private String message;
+
+ public ErrorLogEntry() {
+ }
+
+ public ErrorLogEntry(Integer priority, String tag, String message) {
+ this.dateAndTime = new Date();
+ this.priority = priority;
+ this.tag = tag;
+ this.message = message;
+ }
+
+ public Integer getLogId() {
+ return logId;
+ }
+
+ public Date getDateAndTime() {
+ return dateAndTime;
+ }
+
+ public Integer getPriority() {
+ return priority;
+ }
+
+ public String getTag() {
+ return tag;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(logId);
+ out.writeLong(dateAndTime.getTime());
+ out.writeInt(priority);
+ out.writeString(tag);
+ out.writeString(message);
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public ErrorLogEntry createFromParcel(Parcel in) {
+ return new ErrorLogEntry(in);
+ }
+
+ public ErrorLogEntry[] newArray(int size) {
+ return new ErrorLogEntry[size];
+ }
+ };
+
+ private ErrorLogEntry(Parcel in) {
+ logId = in.readInt();
+ dateAndTime = new Date(in.readLong());
+ priority = in.readInt();
+ tag = in.readString();
+ message = in.readString();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/log/ErrorLogSender.java b/core/src/org/transdroid/core/gui/log/ErrorLogSender.java
new file mode 100644
index 00000000..1740c1ad
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/log/ErrorLogSender.java
@@ -0,0 +1,77 @@
+package org.transdroid.core.gui.log;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.OrmLiteDao;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.ServerSetting;
+import org.transdroid.core.gui.navigation.NavigationHelper;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+
+import com.j256.ormlite.dao.Dao;
+
+@EBean
+public class ErrorLogSender {
+
+ @Bean
+ protected NavigationHelper navigationHelper;
+ @OrmLiteDao(helper = DatabaseHelper.class, model = ErrorLogEntry.class)
+ protected Dao errorLogDao;
+
+ public void collectAndSendLog(final Activity callingActivity, final ServerSetting serverSetting) {
+
+ try {
+
+ // Prepare an email with error logging information
+ StringBuilder body = new StringBuilder();
+ body.append("Please describe your problem:\n\n\n");
+ body.append("\n");
+ body.append(navigationHelper.getAppNameAndVersion());
+ body.append("\n");
+ if (serverSetting == null) {
+ body.append("(No server settings)");
+ } else {
+ body.append(serverSetting.getType().toString());
+ body.append(" settings: ");
+ body.append(serverSetting.getHumanReadableIdentifier());
+ }
+ body.append("\n\nConnection and error log:");
+
+ // Print the individual error log messages as stored in the database
+ List all = errorLogDao.queryBuilder().orderBy(ErrorLogEntry.ID, true).query();
+ for (ErrorLogEntry errorLogEntry : all) {
+ body.append("\n");
+ body.append(errorLogEntry.getLogId());
+ body.append(" -- ");
+ body.append(errorLogEntry.getDateAndTime());
+ body.append(" -- ");
+ body.append(errorLogEntry.getPriority());
+ body.append(" -- ");
+ body.append(errorLogEntry.getMessage());
+ }
+
+ Intent target = new Intent(Intent.ACTION_SEND);
+ target.setType("message/rfc822");
+ target.putExtra(Intent.EXTRA_EMAIL, new String[] { "transdroid.org@gmail.com" });
+ target.putExtra(Intent.EXTRA_SUBJECT, "Transdroid error report");
+ target.putExtra(Intent.EXTRA_TEXT, body.toString());
+ try {
+ callingActivity.startActivity(Intent.createChooser(target,
+ callingActivity.getString(R.string.pref_sendlog)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ } catch (ActivityNotFoundException e) {
+ Log.i(callingActivity, "Tried to send error log, but there is no email app installed.");
+ }
+
+ } catch (SQLException e) {
+ Log.e(callingActivity, "Cannot read the error log to build an error report to send: " + e.toString());
+ }
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/log/Log.java b/core/src/org/transdroid/core/gui/log/Log.java
new file mode 100644
index 00000000..51c0c79f
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/log/Log.java
@@ -0,0 +1,76 @@
+package org.transdroid.core.gui.log;
+
+import java.sql.SQLException;
+import java.util.Date;
+
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.EBean.Scope;
+import org.androidannotations.annotations.OrmLiteDao;
+import org.transdroid.core.gui.navigation.NavigationHelper;
+import org.transdroid.daemon.util.ITLogger;
+
+import android.content.Context;
+
+import com.j256.ormlite.dao.Dao;
+import com.j256.ormlite.stmt.DeleteBuilder;
+
+/**
+ * Application-wide logging class that registers entries in the database (for a certain time).
+ * @author Eric Kok
+ */
+@EBean(scope = Scope.Singleton)
+public class Log implements ITLogger {
+
+ public static final String LOG_NAME = "Transdroid";
+ private static final long MAX_LOG_AGE = 15 * 60 * 1000; // 15 minutes
+
+ // Access to resources and database in local singleton instance
+ private Context context;
+ @OrmLiteDao(helper = DatabaseHelper.class, model = ErrorLogEntry.class)
+ Dao errorLogDao;
+ @Bean
+ protected NavigationHelper navigationHelper;
+
+ protected Log(Context context) {
+ this.context = context;
+ }
+
+ protected void log(String logName, int priority, String message) {
+ if (navigationHelper.inDebugMode())
+ android.util.Log.println(priority, LOG_NAME, message);
+ try {
+ // Store this log message to the database
+ errorLogDao.create(new ErrorLogEntry(priority, logName, message));
+ // Truncate the error log
+ DeleteBuilder db = errorLogDao.deleteBuilder();
+ db.setWhere(db.where().le(ErrorLogEntry.DATEANDTIME, new Date(new Date().getTime() - MAX_LOG_AGE)));
+ errorLogDao.delete(db.prepare());
+ } catch (SQLException e) {
+ android.util.Log.e(LOG_NAME, "Cannot write log message to database: " + e.toString());
+ }
+ }
+
+ public static void e(Context caller, String message) {
+ Log_.getInstance_(caller).log(caller.getClass().toString(), android.util.Log.ERROR, message);
+ }
+
+ public static void i(Context caller, String message) {
+ Log_.getInstance_(caller).log(caller.getClass().toString(), android.util.Log.INFO, message);
+ }
+
+ public static void d(Context caller, String message) {
+ Log_.getInstance_(caller).log(caller.getClass().toString(), android.util.Log.DEBUG, message);
+ }
+
+ @Override
+ public void d(String self, String msg) {
+ Log.d(context, msg);
+ }
+
+ @Override
+ public void e(String self, String msg) {
+ Log.e(context, msg);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/DialogHelper.java b/core/src/org/transdroid/core/gui/navigation/DialogHelper.java
new file mode 100644
index 00000000..618337ea
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/DialogHelper.java
@@ -0,0 +1,99 @@
+package org.transdroid.core.gui.navigation;
+
+import java.io.Serializable;
+
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.Extra;
+import org.transdroid.core.gui.TorrentsActivity_;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Window;
+
+import com.actionbarsherlock.app.SherlockActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+
+/**
+ * Helper class that show a dialog either as pop-up or as full screen activity. Should be used by calling
+ * {@link #showDialog(Context, DialogSpecification)} with in instance of the dialog specification that should be shown,
+ * from the calling activity's {@link Activity#onCreateDialog(int)}.
+ * @author Eric Kok
+ */
+@EActivity
+public class DialogHelper extends SherlockActivity {
+
+ @Extra
+ protected DialogSpecification dialog;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(dialog.getDialogLayoutId());
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater menuInflater = getSupportMenuInflater();
+ menuInflater.inflate(dialog.getDialogMenuId(), menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ // Action bar up button clicked; navigate up all the way back to the torrents activity
+ TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
+ return true;
+ }
+ return dialog.onMenuItemSelected(this, item.getItemId());
+ }
+
+ /**
+ * Call this from {@link Activity#onCreateDialog(int)}, supplying an instance of the {@link DialogSpecification}
+ * that should be shown to the user.
+ * @param context The activity that calls this method and which will own the constructed dialog
+ * @param dialog An instance of the specification for the dialog that needs to be shown
+ * @return Either an instance of a {@link Dialog} that the activity should further control or null if the dialog
+ * will instead be opened as a full screen activity
+ */
+ public static Dialog showDialog(Context context, DialogSpecification dialog) {
+
+ // If the device is large (i.e. a tablet) then return a dialog to show
+ if (!NavigationHelper_.getInstance_(context).isSmallScreen())
+ return new PopupDialog(context, dialog);
+
+ // This is a small device; create a full screen dialog (which is just an activity)
+ DialogHelper_.intent(context).dialog(dialog).start();
+ return null;
+
+ }
+
+ /**
+ * A specific dialog that shows some layout (resource) as contents. It has no buttons or other chrome.
+ */
+ protected static class PopupDialog extends Dialog {
+ public PopupDialog(Context context, DialogSpecification dialog) {
+ super(context);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(dialog.getDialogLayoutId());
+ }
+ }
+
+ /**
+ * Specification for some dialog that can be show to the user, consisting of a custom layout and possibly an action
+ * bar menu. Warning: the action bar, and thus the menu options, is only shown when the dialog is presented as full
+ * screen activity. Use only for unimportant actions.
+ */
+ public interface DialogSpecification extends Serializable {
+ int getDialogLayoutId();
+ int getDialogMenuId();
+ boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/FilterListAdapter.java b/core/src/org/transdroid/core/gui/navigation/FilterListAdapter.java
new file mode 100644
index 00000000..cc85b8c8
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/FilterListAdapter.java
@@ -0,0 +1,96 @@
+package org.transdroid.core.gui.navigation;
+
+import java.util.List;
+
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.RootContext;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.ServerSetting;
+import org.transdroid.core.gui.lists.MergeAdapter;
+import org.transdroid.core.gui.lists.ViewHolderAdapter;
+import org.transdroid.core.gui.navigation.StatusType.StatusTypeFilter;
+
+import android.content.Context;
+import android.view.View;
+
+/**
+ * List adapter that holds filter items, that is, servers, view types and labels. A header item is inserted where
+ * appropriate.
+ * @author Eric Kok
+ */
+@EBean
+public class FilterListAdapter extends MergeAdapter {
+
+ @RootContext
+ protected Context context;
+ private FilterListItemAdapter serverItems = null;
+ private FilterListItemAdapter statusTypeItems = null;
+ private FilterListItemAdapter labelItems = null;
+ protected ViewHolderAdapter statusTypeSeparator;
+ protected ViewHolderAdapter labelSeperator;
+ protected ViewHolderAdapter serverSeparator;
+
+ /**
+ * Update the list of available servers
+ * @param servers The new list of available servers
+ */
+ public void updateServers(List servers) {
+ if (this.serverItems == null && servers != null) {
+ serverSeparator = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
+ context.getString(R.string.navigation_servers)));
+ serverSeparator.setViewVisibility(servers.isEmpty() ? View.GONE : View.VISIBLE);
+ addAdapter(serverSeparator);
+ this.serverItems = new FilterListItemAdapter(context, servers);
+ addAdapter(serverItems);
+ } else if (this.serverItems != null && servers != null) {
+ serverSeparator.setViewVisibility(servers.isEmpty() ? View.GONE : View.VISIBLE);
+ this.serverItems.update(servers);
+ } else {
+ serverSeparator.setViewVisibility(View.GONE);
+ this.serverItems = null;
+ }
+ }
+
+ /**
+ * Update the list of available status types
+ * @param statusTypes The new list of available status types
+ */
+ public void updateStatusTypes(List statusTypes) {
+ if (this.statusTypeItems == null && statusTypes != null) {
+ statusTypeSeparator = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
+ context.getString(R.string.navigation_status)));
+ statusTypeSeparator.setViewVisibility(statusTypes.isEmpty() ? View.GONE : View.VISIBLE);
+ addAdapter(statusTypeSeparator);
+ this.statusTypeItems = new FilterListItemAdapter(context, statusTypes);
+ addAdapter(statusTypeItems);
+ } else if (this.statusTypeItems != null && statusTypes != null) {
+ statusTypeSeparator.setViewVisibility(statusTypes.isEmpty() ? View.GONE : View.VISIBLE);
+ this.statusTypeItems.update(statusTypes);
+ } else {
+ statusTypeSeparator.setViewVisibility(View.GONE);
+ this.statusTypeItems = null;
+ }
+ }
+
+ /**
+ * Update the list of available labels
+ * @param labels The new list of available labels
+ */
+ public void updateLabels(List labels) {
+ if (this.labelItems == null && labels != null) {
+ labelSeperator = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
+ context.getString(R.string.navigation_labels)));
+ labelSeperator.setViewVisibility(labels.isEmpty() ? View.GONE : View.VISIBLE);
+ addAdapter(labelSeperator);
+ this.labelItems = new FilterListItemAdapter(context, labels);
+ addAdapter(labelItems);
+ } else if (this.labelItems != null && labels != null) {
+ labelSeperator.setViewVisibility(labels.isEmpty() ? View.GONE : View.VISIBLE);
+ this.labelItems.update(labels);
+ } else {
+ labelSeperator.setViewVisibility(View.GONE);
+ this.labelItems = null;
+ }
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/FilterListDropDownAdapter.java b/core/src/org/transdroid/core/gui/navigation/FilterListDropDownAdapter.java
new file mode 100644
index 00000000..cf61b615
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/FilterListDropDownAdapter.java
@@ -0,0 +1,54 @@
+package org.transdroid.core.gui.navigation;
+
+import org.androidannotations.annotations.EBean;
+import org.transdroid.daemon.IDaemonAdapter;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * List adapter that holds filter items, that is, servers, view types and labels and is displayed as content to a
+ * Spinner instead of a ListView.
+ * @author Eric Kok
+ */
+@EBean
+public class FilterListDropDownAdapter extends FilterListAdapter {
+
+ protected NavigationSelectionView navigationSelectionView = null;
+ private String currentServer = null;
+ private String currentFilter = null;
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // This returns the singleton navigation spinner view
+ if (navigationSelectionView == null) {
+ navigationSelectionView = NavigationSelectionView_.build(context);
+ }
+ navigationSelectionView.bind(currentServer, currentFilter);
+ return navigationSelectionView;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ // This returns the item to show in the drop down list
+ return super.getView(position, convertView, parent);
+ }
+
+ public void updateCurrentFilter(NavigationFilter currentFilter) {
+ this.currentFilter = currentFilter.getName();
+ if (navigationSelectionView != null)
+ navigationSelectionView.bind(this.currentServer, this.currentFilter);
+ }
+
+ public void updateCurrentServer(IDaemonAdapter currentConnection) {
+ this.currentServer = currentConnection.getSettings().getName();
+ if (navigationSelectionView != null)
+ navigationSelectionView.bind(this.currentServer, this.currentFilter);
+ }
+
+ public void hideServersLabel() {
+ serverSeparator.setViewVisibility(View.GONE);
+ notifyDataSetInvalidated();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/FilterListItemAdapter.java b/core/src/org/transdroid/core/gui/navigation/FilterListItemAdapter.java
new file mode 100644
index 00000000..d3bb577c
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/FilterListItemAdapter.java
@@ -0,0 +1,59 @@
+package org.transdroid.core.gui.navigation;
+
+import java.util.List;
+
+import org.transdroid.core.gui.lists.SimpleListItem;
+import org.transdroid.core.gui.lists.SimpleListItemView;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+public class FilterListItemAdapter extends BaseAdapter {
+
+ private final Context context;
+ private List extends SimpleListItem> items;
+
+ public FilterListItemAdapter(Context context, List extends SimpleListItem> items) {
+ this.context = context;
+ this.items = items;
+ }
+
+ /**
+ * Allows updating of the full data list underlying this adapter, replacing all items
+ * @param newItems The new list of filter items to display
+ */
+ public void update(List extends SimpleListItem> newItems) {
+ this.items = newItems;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return items.size();
+ }
+
+ @Override
+ public SimpleListItem getItem(int position) {
+ return items.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ FilterListItemView filterItemView;
+ if (convertView == null || !(convertView instanceof SimpleListItemView)) {
+ filterItemView = FilterListItemView_.build(context);
+ } else {
+ filterItemView = (FilterListItemView) convertView;
+ }
+ filterItemView.bind(getItem(position));
+ return filterItemView;
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/org/transdroid/core/gui/navigation/FilterListItemView.java b/core/src/org/transdroid/core/gui/navigation/FilterListItemView.java
new file mode 100644
index 00000000..649ff45c
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/FilterListItemView.java
@@ -0,0 +1,29 @@
+package org.transdroid.core.gui.navigation;
+
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.core.gui.lists.SimpleListItem;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+/**
+ * View that represents some {@link SimpleListItem} object specifically used to represent a navigation filter item.
+ * @author Eric Kok
+ */
+@EViewGroup(resName="list_item_filter")
+public class FilterListItemView extends FrameLayout {
+
+ @ViewById
+ protected TextView itemText;
+
+ public FilterListItemView(Context context) {
+ super(context);
+ }
+
+ public void bind(SimpleListItem filterItem) {
+ itemText.setText(filterItem.getName());
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/FilterSeparatorView.java b/core/src/org/transdroid/core/gui/navigation/FilterSeparatorView.java
new file mode 100644
index 00000000..2a831fd2
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/FilterSeparatorView.java
@@ -0,0 +1,39 @@
+package org.transdroid.core.gui.navigation;
+
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+
+import android.content.Context;
+import android.widget.AbsListView;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+/**
+ * A list item that shows a sub header or separator (in underlined Holo style).
+ * @author Eric Kok
+ */
+@EViewGroup(resName="list_item_separator")
+public class FilterSeparatorView extends FrameLayout {
+
+ protected String text;
+
+ @ViewById
+ protected TextView separatorText;
+
+ public FilterSeparatorView(Context context) {
+ super(context);
+ }
+
+ /**
+ * Sets the text that will be shown in this separator (sub header)
+ * @param text The new text to show
+ * @return Itself, for convenience of method chaining
+ */
+ public FilterSeparatorView setText(String text) {
+ separatorText.setText(text);
+ setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT,
+ AbsListView.LayoutParams.WRAP_CONTENT));
+ return this;
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/Label.java b/core/src/org/transdroid/core/gui/navigation/Label.java
new file mode 100644
index 00000000..a4fafe1f
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/Label.java
@@ -0,0 +1,100 @@
+package org.transdroid.core.gui.navigation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.transdroid.core.gui.lists.SimpleListItem;
+import org.transdroid.daemon.Torrent;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/**
+ * Represents some label that is active or available on the server.
+ * @author Eric Kok
+ */
+public class Label implements SimpleListItem, NavigationFilter, Comparable {
+
+ private static String unnamedLabelText = null;
+
+ private final String name;
+ private final int count;
+
+ public Label(org.transdroid.daemon.Label daemonLabel) {
+ this.name = daemonLabel.getName();
+ this.count = daemonLabel.getCount();
+ }
+
+ @Override
+ public String getName() {
+ if (TextUtils.isEmpty(this.name))
+ return unnamedLabelText;
+ return this.name;
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ /**
+ * Returns true if the torrent label's name matches this (selected) label's name, false otherwise
+ */
+ @Override
+ public boolean matches(Torrent torrent) {
+ return torrent.getLabelName() != null && torrent.getLabelName().equals(name);
+ }
+
+ @Override
+ public int compareTo(Label another) {
+ return this.name.compareTo(another.getName());
+ }
+
+ /**
+ * Converts a list of labels as retrieved from a server daemon into a list of labels that can be used in the UI as
+ * navigation filters.
+ * @param daemonLabels The raw list of labels as received from the server daemon adapter
+ * @param unnamedLabel The text to show for the empty label (i.e. the unnamed label)
+ * @return A label items that can be used in a filter list such as the action bar spinner
+ */
+ public static ArrayList convertToNavigationLabels(List daemonLabels,
+ String unnamedLabel) {
+ if (daemonLabels == null)
+ return null;
+ unnamedLabelText = unnamedLabel;
+ ArrayList localLabels = new ArrayList();
+ for (org.transdroid.daemon.Label label : daemonLabels) {
+ localLabels.add(new Label(label));
+ }
+ Collections.sort(localLabels);
+ return localLabels;
+ }
+
+ private Label(Parcel in) {
+ this.name = in.readString();
+ this.count = in.readInt();
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public Label createFromParcel(Parcel in) {
+ return new Label(in);
+ }
+
+ public Label[] newArray(int size) {
+ return new Label[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(name);
+ dest.writeInt(count);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/NavigationFilter.java b/core/src/org/transdroid/core/gui/navigation/NavigationFilter.java
new file mode 100644
index 00000000..32207480
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/NavigationFilter.java
@@ -0,0 +1,27 @@
+package org.transdroid.core.gui.navigation;
+
+import org.transdroid.daemon.Torrent;
+
+import android.os.Parcelable;
+
+/**
+ * Represents a filter, used in the app navigation, that can check if some torrent matches the user-set filter
+ * @author Eric Kok
+ */
+public interface NavigationFilter extends Parcelable {
+
+ /**
+ * Implementations should check if the supplied torrent matches the filter; for example a label filter should return
+ * true if the torrent's label equals this items label name.
+ * @param torrent The torrent to check for matches
+ * @return True if the torrent matches the filter and should be shown in the current screen, false otherwise
+ */
+ boolean matches(Torrent torrent);
+
+ /**
+ * Implementations should return a name that can be shown to indicate the active filter
+ * @return The name of the filter item as string
+ */
+ String getName();
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/NavigationHelper.java b/core/src/org/transdroid/core/gui/navigation/NavigationHelper.java
new file mode 100644
index 00000000..2a3dd573
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/NavigationHelper.java
@@ -0,0 +1,153 @@
+package org.transdroid.core.gui.navigation;
+
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.RootContext;
+import org.transdroid.core.R;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.TypefaceSpan;
+
+import com.nostra13.universalimageloader.cache.disc.impl.FileCountLimitedDiscCache;
+import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator;
+import com.nostra13.universalimageloader.cache.memory.impl.UsingFreqLimitedMemoryCache;
+import com.nostra13.universalimageloader.core.DisplayImageOptions;
+import com.nostra13.universalimageloader.core.ImageLoader;
+import com.nostra13.universalimageloader.core.ImageLoaderConfiguration.Builder;
+import com.nostra13.universalimageloader.core.assist.ImageScaleType;
+
+import de.keyboardsurfer.android.widget.crouton.Crouton;
+import de.keyboardsurfer.android.widget.crouton.Style;
+
+/**
+ * Helper for activities to make navigation-related decisions, such as when a device can display a larger, tablet style
+ * layout or how to display errors.
+ * @author Eric Kok
+ */
+@SuppressLint("ResourceAsColor")
+@EBean
+public class NavigationHelper {
+
+ @RootContext
+ protected Context context;
+ private Boolean inDebugMode;
+ private static ImageLoader imageCache;
+
+ /**
+ * Use with {@link Crouton#showText(android.app.Activity, int, Style)} (and variants) to display error messages.
+ */
+ public static Style CROUTON_ERROR_STYLE = new Style.Builder().setBackgroundColor(R.color.crouton_error)
+ .setTextSize(13).setDuration(2500).build();
+
+ /**
+ * Use with {@link Crouton#showText(android.app.Activity, int, Style)} (and variants) to display info messages.
+ */
+ public static Style CROUTON_INFO_STYLE = new Style.Builder().setBackgroundColor(R.color.crouton_info)
+ .setTextSize(13).setDuration(1500).build();
+
+ /**
+ * Returns (and initialises, if needed) an image cache that uses memory and (1MB) local storage.
+ * @return An image cache that loads web images synchronously and transparently
+ */
+ public ImageLoader getImageCache() {
+ if (imageCache == null) {
+ // @formatter:off
+ imageCache = ImageLoader.getInstance();
+ Builder imageCacheBuilder = new Builder(context)
+ .defaultDisplayImageOptions(
+ new DisplayImageOptions.Builder()
+ .cacheInMemory()
+ .cacheOnDisc()
+ .imageScaleType(ImageScaleType.IN_SAMPLE_INT)
+ .showImageForEmptyUri(R.drawable.ic_launcher)
+ .build())
+ .memoryCache(
+ new UsingFreqLimitedMemoryCache(1024 * 1024))
+ .discCache(
+ new FileCountLimitedDiscCache(context.getCacheDir(), new Md5FileNameGenerator(), 25));
+ imageCache.init(imageCacheBuilder.build());
+ // @formatter:on
+ }
+ return imageCache;
+ }
+
+ /**
+ * Whether any search-related UI components should be shown in the interface. At the moment returns false only if we
+ * run as Transdroid Lite version.
+ * @return True if search is enabled, false otherwise
+ */
+ public String getAppNameAndVersion() {
+ String appName = context.getString(R.string.app_name);
+ try {
+ PackageInfo m = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ return appName + " " + m.versionName + " (" + m.versionCode + ")";
+ } catch (NameNotFoundException e) {
+ return appName;
+ }
+ }
+
+ /**
+ * Returns whether the application is running in debug mode, as opposed to release mode. Use to show/hide features
+ * in the ui based on the build mode.
+ * @return True if the app is compiled in/running as debug mode, false otherwise
+ */
+ public boolean inDebugMode() {
+ try {
+ if (inDebugMode == null) {
+ PackageInfo pi = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ inDebugMode = (pi.applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
+ }
+ return inDebugMode;
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns whether the device is considered small (i.e. a phone) rather than large (i.e. a tablet). Can, for
+ * example, be used to determine if a dialog should be shown full screen. Currently is true if the device's smallest
+ * dimension is 500 dip.
+ * @return True if the app runs on a small device, false otherwise
+ */
+ public boolean isSmallScreen() {
+ return context.getResources().getBoolean(R.bool.show_dialog_fullscreen);
+ }
+
+ /**
+ * Whether any search-related UI components should be shown in the interface. At the moment returns false only if we
+ * run as Transdroid Lite version.
+ * @return True if search is enabled, false otherwise
+ */
+ public boolean enableSearchUi() {
+ return !context.getPackageName().equals("org.transdroid.lite");
+ }
+
+ /**
+ * Whether any RSS-related UI components should be shown in the interface. At the moment returns false only if we
+ * run as Transdroid Lite version.
+ * @return True if search is enabled, false otherwise
+ */
+ public boolean enableRssUi() {
+ return !context.getPackageName().equals("org.transdroid.lite");
+ }
+
+ /**
+ * Converts a string into a {@link Spannable} that displays the string in the Roboto Condensed font
+ * @param string A plain text {@link String}
+ * @return A {@link Spannable} that can be applied to supporting views (such as the action bar title) so that the
+ * input string will be displayed using the Roboto Condensed font (if the OS has this)
+ */
+ public static SpannableString buildCondensedFontString(String string) {
+ if (string == null)
+ return null;
+ SpannableString s = new SpannableString(string);
+ s.setSpan(new TypefaceSpan("sans-serif-condensed"), 0, s.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ return s;
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/NavigationSelectionView.java b/core/src/org/transdroid/core/gui/navigation/NavigationSelectionView.java
new file mode 100644
index 00000000..e3cdd720
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/NavigationSelectionView.java
@@ -0,0 +1,36 @@
+package org.transdroid.core.gui.navigation;
+
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+
+import android.content.Context;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * View that displays the user-selected server and display filter inside the action bar list navigation spinner
+ * @author Eric Kok
+ */
+@EViewGroup(resName="actionbar_navigation")
+public class NavigationSelectionView extends LinearLayout {
+
+ @ViewById
+ protected TextView filterText;
+ @ViewById
+ protected TextView serverText;
+
+ public NavigationSelectionView(Context context) {
+ super(context);
+ }
+
+ /**
+ * Binds the names of the current connected server and selected filter to this navigation view.
+ * @param currentServer The name of the server currently connected to
+ * @param currentFilter The name of the filter that is currently selected
+ */
+ public void bind(String currentServer, String currentFilter) {
+ serverText.setText(currentServer);
+ filterText.setText(currentFilter);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/SelectionManagerMode.java b/core/src/org/transdroid/core/gui/navigation/SelectionManagerMode.java
new file mode 100644
index 00000000..3468bd23
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/SelectionManagerMode.java
@@ -0,0 +1,112 @@
+package org.transdroid.core.gui.navigation;
+
+import org.transdroid.core.gui.navigation.SelectionModificationSpinner.OnModificationActionSelectedListener;
+
+import android.util.SparseBooleanArray;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+import com.actionbarsherlock.view.ActionMode;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.SherlockListView.MultiChoiceModeListenerCompat;
+
+/**
+ * A helper to implement {@link ListView} selection modification behaviour with the {@link SelectionModificationSpinner}
+ * by implementing the specific actions and providing a title based on the number of currently selected items. It is
+ * important that the provided list was instantiated already.
+ * @author Eric Kok
+ */
+public class SelectionManagerMode implements MultiChoiceModeListenerCompat, OnModificationActionSelectedListener {
+
+ private ListView managedList;
+ private int titleTemplateResource;
+ private Class> onlyCheckClass = null;
+
+ /**
+ * Instantiates the helper by binding it to a specific {@link ListView} and providing the text resource to display
+ * as title in the spinner.
+ * @param managedList The list to manage the selection for and execute selection action to
+ * @param titleTemplateResource The string resource id to show as the spinners title; the number of selected items
+ * will be supplied as numeric formatting argument
+ */
+ public SelectionManagerMode(ListView managedList, int titleTemplateResource) {
+ this.managedList = managedList;
+ this.titleTemplateResource = titleTemplateResource;
+ }
+
+ /**
+ * Set the class type of items that are allowed to be checked in the {@link ListView}. Defaults to null, which means
+ * every list view row can be checked.
+ * @param onlyCheckClass The {@link Class} instance to use to check list item types against
+ */
+ public void setOnlyCheckClass(Class> onlyCheckClass) {
+ this.onlyCheckClass = onlyCheckClass;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ // Allow modification of selection through a spinner
+ SelectionModificationSpinner selectionSpinner = new SelectionModificationSpinner(managedList.getContext());
+ selectionSpinner.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ selectionSpinner.setOnModificationActionSelectedListener(this);
+ mode.setCustomView(selectionSpinner);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
+ int checkedCount = 0;
+ for (int i = 0; i < managedList.getCheckedItemPositions().size(); i++) {
+ if (managedList.getCheckedItemPositions().valueAt(i)
+ && (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(managedList
+ .getCheckedItemPositions().keyAt(i)))))
+ checkedCount++;
+ }
+ ((SelectionModificationSpinner) mode.getCustomView()).updateTitle(managedList.getContext().getResources()
+ .getQuantityString(titleTemplateResource, checkedCount, checkedCount));
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return false;
+ }
+
+ /**
+ * Implements the {@link SelectionModificationSpinner}'s invert selection command by flipping the checked status for
+ * each (enabled) items in the {@link ListView}.
+ */
+ @Override
+ public void invertSelection() {
+ SparseBooleanArray checked = managedList.getCheckedItemPositions();
+ for (int i = 0; i < managedList.getAdapter().getCount(); i++) {
+ if (managedList.getAdapter().isEnabled(i)
+ && (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i))))
+ managedList.setItemChecked(i, !checked.get(i, false));
+ }
+ }
+
+ /**
+ * Implements the {@link SelectionModificationSpinner}'s select all command by checking each (enabled) item in the
+ * {@link ListView}.
+ */
+ @Override
+ public void selectionAll() {
+ for (int i = 0; i < managedList.getAdapter().getCount(); i++) {
+ if (managedList.getAdapter().isEnabled(i)
+ && (onlyCheckClass == null || onlyCheckClass.isInstance(managedList.getItemAtPosition(i))))
+ managedList.setItemChecked(i, true);
+ }
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/SelectionModificationSpinner.java b/core/src/org/transdroid/core/gui/navigation/SelectionModificationSpinner.java
new file mode 100644
index 00000000..93270c1f
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/SelectionModificationSpinner.java
@@ -0,0 +1,98 @@
+package org.transdroid.core.gui.navigation;
+
+import org.transdroid.core.R;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+/**
+ * Spinner that holds actions that can be performed on list selections. The spinner itself has some title, which can for
+ * example be used to show the number of selected items.
+ * @author Eric Kok
+ */
+public class SelectionModificationSpinner extends Spinner {
+
+ private SelectionDropDownAdapter selectionAdapter;
+ private OnModificationActionSelectedListener onModificationActionSelected = null;
+
+ /**
+ * Instantiates a spinner that contains some fixed actions for a user to modify selections.
+ * @param context The interface context where the spinner will be shown in
+ */
+ public SelectionModificationSpinner(Context context) {
+ super(context);
+ selectionAdapter = new SelectionDropDownAdapter(context);
+ setAdapter(selectionAdapter);
+ }
+
+ /**
+ * Updates the fixed title text shown in the spinner, regardless of spinner item action selection.
+ * @param title The new static string to show, such as the number of selected items
+ */
+ public void updateTitle(String title) {
+ selectionAdapter.titleView.setText(title);
+ invalidate();
+ }
+
+ /**
+ * Sets the listener for action selection events.
+ * @param onModificationActionSelected The listener that handles performing of the actions as selected in this
+ * spinner by the user
+ */
+ public void setOnModificationActionSelectedListener(OnModificationActionSelectedListener onModificationActionSelected) {
+ this.onModificationActionSelected = onModificationActionSelected;
+ }
+
+ @Override
+ public void setSelection(int position) {
+ if (position == 0) {
+ onModificationActionSelected.selectionAll();
+ } else if (position == 1) {
+ onModificationActionSelected.invertSelection();
+ }
+ super.setSelection(position);
+ }
+
+ /**
+ * Local adapter that holds the actions which can be performed and a title text view that always shows instead of a
+ * list item as in a normal spinner.
+ */
+ private class SelectionDropDownAdapter extends ArrayAdapter {
+
+ protected TextView titleView = null;
+
+ public SelectionDropDownAdapter(Context context) {
+ super(context, android.R.layout.simple_list_item_1, new String[] {
+ context.getString(R.string.navigation_selectall),
+ context.getString(R.string.navigation_invertselection) });
+ titleView = new TextView(getContext());
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // This returns the singleton text view showing the title with the number of selected items
+ return titleView;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ // This returns the actions to show in the spinner list
+ return super.getView(position, convertView, parent);
+ }
+
+ }
+
+ /**
+ * Interface to implement if an interface want to respond to selection modification actions.
+ */
+ public interface OnModificationActionSelectedListener {
+ public void invertSelection();
+
+ public void selectionAll();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/SetLabelDialog.java b/core/src/org/transdroid/core/gui/navigation/SetLabelDialog.java
new file mode 100644
index 00000000..df544116
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/SetLabelDialog.java
@@ -0,0 +1,106 @@
+package org.transdroid.core.gui.navigation;
+
+import java.security.InvalidParameterException;
+import java.util.List;
+
+import org.transdroid.core.R;
+import org.transdroid.core.gui.lists.SimpleListItem;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.EditText;
+import android.widget.ListView;
+import de.keyboardsurfer.android.widget.crouton.Crouton;
+
+/**
+ * A dialog fragment that allows picking a label or entering a new label to set this new label to the torrent.
+ * @author Eric Kok
+ */
+public class SetLabelDialog extends DialogFragment {
+
+ private OnLabelPickedListener onLabelPickedListener = null;
+ private List extends SimpleListItem> currentLabels = null;
+
+ public SetLabelDialog() {
+ setRetainInstance(true);
+ }
+
+ /**
+ * Sets the callback for when the user is has picked a label for the target torrent.
+ * @param onLabelPickedListener The event listener to this dialog
+ * @return This dialog, for method chaining
+ */
+ public SetLabelDialog setOnLabelPickedListener(OnLabelPickedListener onLabelPickedListener) {
+ this.onLabelPickedListener = onLabelPickedListener;
+ return this;
+ }
+
+ /**
+ * Sets the list of currently known labels as are active on the server. These are offered to the user to pick a new
+ * label for the target torrents.
+ * @param currentLabels The list of torrent labels
+ * @return This dialog, for method chaining
+ */
+ public SetLabelDialog setCurrentLabels(List extends SimpleListItem> currentLabels) {
+ this.currentLabels = currentLabels;
+ return this;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ if (onLabelPickedListener == null)
+ throw new InvalidParameterException(
+ "Please first set the callback listener using setOnLabelPickedListener before opening the dialog.");
+ if (currentLabels == null)
+ throw new InvalidParameterException(
+ "Please first set the list of currently known labels before opening the dialog, even if the list is empty.");
+ final View setlabelFrame = getActivity().getLayoutInflater().inflate(R.layout.dialog_setlabel, null, false);
+ final ListView labelsList = (ListView) setlabelFrame.findViewById(R.id.labels_list);
+ final EditText newlabelEdit = (EditText) setlabelFrame.findViewById(R.id.newlabel_edit);
+ if (currentLabels.size() == 0) {
+ // Hide the list (and its label) if there are no labels yet
+ setlabelFrame.findViewById(R.id.pick_label).setVisibility(View.GONE);
+ setlabelFrame.findViewById(R.id.line1).setVisibility(View.GONE);
+ setlabelFrame.findViewById(R.id.line2).setVisibility(View.GONE);
+ labelsList.setVisibility(View.GONE);
+ } else {
+ labelsList.setAdapter(new FilterListItemAdapter(getActivity(), currentLabels));
+ labelsList.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ onLabelPickedListener.onLabelPicked(((Label) labelsList.getItemAtPosition(position)).getName());
+ dismiss();
+ }
+ });
+ }
+ return new AlertDialog.Builder(getActivity()).setView(setlabelFrame)
+ .setPositiveButton(R.string.status_update, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // User should have provided a new label
+ if (newlabelEdit.getText().toString().equals("")) {
+ Crouton.showText(getActivity(), R.string.error_notalabel,
+ NavigationHelper.CROUTON_ERROR_STYLE);
+ }
+ onLabelPickedListener.onLabelPicked(newlabelEdit.getText().toString());
+ }
+ }).setNeutralButton(R.string.status_label_remove, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ onLabelPickedListener.onLabelPicked(null);
+ }
+ }).setNegativeButton(android.R.string.cancel, null).show();
+ }
+
+ public interface OnLabelPickedListener {
+ public void onLabelPicked(String newLabel);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/SetTrackersDialog.java b/core/src/org/transdroid/core/gui/navigation/SetTrackersDialog.java
new file mode 100644
index 00000000..8def3d41
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/SetTrackersDialog.java
@@ -0,0 +1,77 @@
+package org.transdroid.core.gui.navigation;
+
+import java.security.InvalidParameterException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.transdroid.core.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.view.View;
+import android.widget.EditText;
+
+/**
+ * A dialog fragment that allows changing the trackers of a torrent by editing the text directly.
+ * @author Eric Kok
+ */
+public class SetTrackersDialog extends DialogFragment {
+
+ private OnTrackersUpdatedListener onTrackersUpdatedListener = null;
+ private String currentTrackers = null;
+
+ public SetTrackersDialog() {
+ setRetainInstance(true);
+ }
+
+ /**
+ * Sets the callback for when the user is done updating the trackers list.
+ * @param onTrackersUpdatedListener The event listener to this dialog
+ * @return This dialog, for method chaining
+ */
+ public SetTrackersDialog setOnTrackersUpdated(OnTrackersUpdatedListener onTrackersUpdatedListener) {
+ this.onTrackersUpdatedListener = onTrackersUpdatedListener;
+ return this;
+ }
+
+ /**
+ * Sets the current trackers text/list that will be available to the user to edit
+ * @param currentTrackers The current trackers for the target torrent
+ * @return This dialog, for method chaining
+ */
+ public SetTrackersDialog setCurrentTrackers(String currentTrackers) {
+ this.currentTrackers = currentTrackers;
+ return this;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ if (currentTrackers == null)
+ throw new InvalidParameterException(
+ "Please first set the current trackers text using setCurrentTrackers before opening the dialog.");
+ if (onTrackersUpdatedListener == null)
+ throw new InvalidParameterException(
+ "Please first set the callback listener using setOnTrackersUpdated before opening the dialog.");
+ final View trackersFrame = getActivity().getLayoutInflater().inflate(R.layout.dialog_trackers, null, false);
+ final EditText trackersText = (EditText) trackersFrame.findViewById(R.id.trackers_edit);
+ trackersText.setText(currentTrackers);
+ return new AlertDialog.Builder(getActivity()).setView(trackersFrame)
+ .setPositiveButton(R.string.status_update, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // User is done editing and requested to update given the text input
+ onTrackersUpdatedListener.onTrackersUpdated(Arrays.asList(trackersText.getText().toString()
+ .split("\n")));
+ }
+ }).setNegativeButton(android.R.string.cancel, null).show();
+ }
+
+ public interface OnTrackersUpdatedListener {
+ public void onTrackersUpdated(List updatedTrackers);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/SetTransferRatesDialog.java b/core/src/org/transdroid/core/gui/navigation/SetTransferRatesDialog.java
new file mode 100644
index 00000000..d129deb2
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/SetTransferRatesDialog.java
@@ -0,0 +1,109 @@
+package org.transdroid.core.gui.navigation;
+
+import java.security.InvalidParameterException;
+
+import org.transdroid.core.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+/**
+ * A dialog fragment that allow picking of maximum download and upload transfer rates as well as the resetting of these
+ * values.
+ * @author Eric Kok
+ */
+public class SetTransferRatesDialog extends DialogFragment {
+
+ private OnRatesPickedListener onRatesPickedListener = null;
+ private TextView maxSpeedDown, maxSpeedUp;
+
+ public SetTransferRatesDialog() {
+ setRetainInstance(true);
+ }
+
+ /**
+ * Sets the callback for results in this dialog (with newly selected values or a reset).
+ * @param onRatesPickedListener The event listener to this dialog
+ * @return This dialog, for method chaining
+ */
+ public SetTransferRatesDialog setOnRatesPickedListener(OnRatesPickedListener onRatesPickedListener) {
+ this.onRatesPickedListener = onRatesPickedListener;
+ return this;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ if (onRatesPickedListener == null)
+ throw new InvalidParameterException(
+ "Please first set the callback listener using setOnRatesPickedListener before opening the dialog.");
+ final View transferRatesContent = getActivity().getLayoutInflater().inflate(R.layout.dialog_transferrates,
+ null, false);
+ maxSpeedDown = (TextView) transferRatesContent.findViewById(R.id.maxspeeddown_text);
+ maxSpeedUp = (TextView) transferRatesContent.findViewById(R.id.maxspeedup_text);
+ bindButtons(transferRatesContent, maxSpeedDown, R.id.down1Button, R.id.down2Button, R.id.down3Button,
+ R.id.down4Button, R.id.down5Button, R.id.down6Button, R.id.down7Button, R.id.down8Button,
+ R.id.down9Button, R.id.down0Button);
+ bindButtons(transferRatesContent, maxSpeedUp, R.id.up1Button, R.id.up2Button, R.id.up3Button, R.id.up4Button,
+ R.id.up5Button, R.id.up6Button, R.id.up7Button, R.id.up8Button, R.id.up9Button, R.id.up0Button);
+ return new AlertDialog.Builder(getActivity()).setTitle(R.string.status_maxspeed).setView(transferRatesContent)
+ .setPositiveButton(android.R.string.ok, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ int maxDown = -1, maxUp = -1;
+ try {
+ maxDown = Integer.parseInt(maxSpeedDown.getText().toString());
+ maxUp = Integer.parseInt(maxSpeedUp.getText().toString());
+ } catch (NumberFormatException e) {
+ }
+ if (maxDown <= 0 || maxUp <= 0) {
+ onRatesPickedListener.onInvalidNumber();
+ }
+ onRatesPickedListener.onRatesPicked(maxDown, maxUp);
+ }
+ }).setNeutralButton(R.string.status_maxspeed_reset, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ onRatesPickedListener.resetRates();
+ }
+ }).setNegativeButton(android.R.string.cancel, null).create();
+ }
+
+ private void bindButtons(View transferRatesContent, View numberView, int... buttonResource) {
+ for (int i : buttonResource) {
+ // Keep the relevant number as reference in the view tag and bind the click listerner
+ transferRatesContent.findViewById(i).setTag(numberView);
+ transferRatesContent.findViewById(i).setOnClickListener(onNumberClicked);
+ }
+ }
+
+ private android.view.View.OnClickListener onNumberClicked = new android.view.View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Append the text contents of the button itself as text to the current number (as reference in the view's
+ // tag)
+ TextView numberView = (TextView) v.getTag();
+ if (numberView.getText().equals("-"))
+ numberView.setText("");
+ numberView.setText(numberView.getText().toString() + ((Button) v).getText().toString());
+ }
+ };
+
+ /**
+ * Listener interface to the user having picked or wanting to resets the current maximum transfer speeds;
+ */
+ public interface OnRatesPickedListener {
+ public void onRatesPicked(int maxDownloadSpeed, int maxUploadSpeed);
+
+ public void resetRates();
+
+ public void onInvalidNumber();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/navigation/StatusType.java b/core/src/org/transdroid/core/gui/navigation/StatusType.java
new file mode 100644
index 00000000..a433b869
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/StatusType.java
@@ -0,0 +1,142 @@
+package org.transdroid.core.gui.navigation;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.transdroid.core.R;
+import org.transdroid.core.gui.lists.SimpleListItem;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.TorrentStatus;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Enumeration of all status types, which filter the list of shown torrents based on transfer activity.
+ * @author Eric Kok
+ */
+public enum StatusType {
+
+ ShowAll {
+ StatusTypeFilter getFilterItem(Context context) {
+ return new StatusTypeFilter(StatusType.ShowAll, context.getString(R.string.navigation_status_showall));
+ }
+ },
+ OnlyDownloading {
+ StatusTypeFilter getFilterItem(Context context) {
+ return new StatusTypeFilter(StatusType.OnlyDownloading, context.getString(R.string.navigation_status_onlydown));
+ }
+ },
+ OnlyUploading {
+ StatusTypeFilter getFilterItem(Context context) {
+ return new StatusTypeFilter(StatusType.OnlyUploading, context.getString(R.string.navigation_status_onlyup));
+ }
+ },
+ OnlyActive {
+ StatusTypeFilter getFilterItem(Context context) {
+ return new StatusTypeFilter(StatusType.OnlyActive, context.getString(R.string.navigation_status_onlyactive));
+ }
+ },
+ OnlyInactive {
+ StatusTypeFilter getFilterItem(Context context) {
+ return new StatusTypeFilter(StatusType.OnlyInactive, context.getString(R.string.navigation_status_onlyinactive));
+ }
+ };
+
+ /**
+ * Returns the status type to show all torrents, represented as filter item to show in the navigation list.
+ * @param context The Android UI context, to access translations
+ * @return The show ShowAll status type filter item
+ */
+ public static StatusTypeFilter getShowAllType(Context context) {
+ return ShowAll.getFilterItem(context);
+ }
+
+ /**
+ * Returns a list with all status types, represented as filter item that can be shown in the GUI.
+ * @param context The Android UI context, to access translations
+ * @return A list of filter items for all available status types
+ */
+ public static List getAllStatusTypes(Context context) {
+ return Arrays.asList(ShowAll.getFilterItem(context), OnlyDownloading.getFilterItem(context),
+ OnlyUploading.getFilterItem(context), OnlyActive.getFilterItem(context),
+ OnlyInactive.getFilterItem(context));
+ }
+
+ /**
+ * Every status type can return a filter item that represents it in the navigation
+ * @param context The Android UI context, to access translations
+ * @return A filter item object to show in the GUI
+ */
+ abstract StatusTypeFilter getFilterItem(Context context);
+
+ public static class StatusTypeFilter implements SimpleListItem, NavigationFilter {
+
+ private final StatusType statusType;
+ private final String name;
+
+ StatusTypeFilter(StatusType statusType, String name) {
+ this.statusType = statusType;
+ this.name = name;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns true if the torrent status matches this (selected) status type, false otherwise
+ */
+ @Override
+ public boolean matches(Torrent torrent) {
+ switch (statusType) {
+ case OnlyDownloading:
+ return torrent.getStatusCode() == TorrentStatus.Downloading;
+ case OnlyUploading:
+ return torrent.getStatusCode() == TorrentStatus.Seeding;
+ case OnlyActive:
+ return torrent.getStatusCode() == TorrentStatus.Downloading
+ || torrent.getStatusCode() == TorrentStatus.Seeding;
+ case OnlyInactive:
+ return torrent.getStatusCode() == TorrentStatus.Checking
+ || torrent.getStatusCode() == TorrentStatus.Error
+ || torrent.getStatusCode() == TorrentStatus.Paused
+ || torrent.getStatusCode() == TorrentStatus.Queued
+ || torrent.getStatusCode() == TorrentStatus.Unknown
+ || torrent.getStatusCode() == TorrentStatus.Waiting;
+ default:
+ return true;
+ }
+ }
+
+ private StatusTypeFilter(Parcel in) {
+ this.statusType = StatusType.valueOf(in.readString());
+ this.name = in.readString();
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public StatusTypeFilter createFromParcel(Parcel in) {
+ return new StatusTypeFilter(in);
+ }
+
+ public StatusTypeFilter[] newArray(int size) {
+ return new StatusTypeFilter[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(statusType.name());
+ dest.writeString(name);
+ }
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedLoader.java b/core/src/org/transdroid/core/gui/rss/RssfeedLoader.java
new file mode 100644
index 00000000..5873fb84
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/rss/RssfeedLoader.java
@@ -0,0 +1,60 @@
+package org.transdroid.core.gui.rss;
+
+import org.transdroid.core.app.settings.RssfeedSetting;
+import org.transdroid.core.rssparser.Channel;
+import org.transdroid.core.rssparser.Item;
+
+/**
+ * A container class that holds RSS feed settings and, after they have been retrieved, the contents as {@link Channel},
+ * the number of new items and an indication of a connection error.
+ * @author Eric Kok
+ */
+public class RssfeedLoader {
+
+ private final RssfeedSetting setting;
+ private Channel channel = null;
+ private int newCount = -1;
+ private boolean hasError = false;
+
+ public RssfeedLoader(RssfeedSetting setting) {
+ this.setting = setting;
+ }
+
+ public void update(Channel channel, boolean hasError) {
+ this.channel = channel;
+ this.hasError = hasError;
+ if (channel == null || hasError) {
+ hasError = true;
+ newCount = -1;
+ return;
+ }
+ // Count the number of new items, based on the date that this RSS feed was last viewed by the user
+ newCount = 0;
+ for (Item item : channel.getItems()) {
+ if (item.getPubdate() == null || setting.getLastViewed() == null
+ || item.getPubdate().after(setting.getLastViewed())) {
+ newCount++;
+ item.setIsNew(true);
+ } else {
+ item.setIsNew(false);
+ }
+ }
+ }
+
+ public Channel getChannel() {
+ return channel;
+ }
+
+ public RssfeedSetting getSetting() {
+ return setting;
+ }
+
+ public int getNewCount() {
+ return newCount;
+ }
+
+ public boolean hasError() {
+ return hasError ;
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedView.java b/core/src/org/transdroid/core/gui/rss/RssfeedView.java
new file mode 100644
index 00000000..13a6a7da
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/rss/RssfeedView.java
@@ -0,0 +1,67 @@
+package org.transdroid.core.gui.rss;
+
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.core.app.settings.RssfeedSetting;
+import org.transdroid.core.gui.navigation.NavigationHelper;
+import org.transdroid.core.rssparser.Channel;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+/**
+ * View that represents some {@link RssfeedSetting} object and displays name as well as loads a favicon for the feed's
+ * site and can load how many new items are available.
+ * @author Eric Kok
+ */
+@EViewGroup(resName = "list_item_rssfeed")
+public class RssfeedView extends LinearLayout {
+
+ private static final String GETFVO_URL = "http://g.etfv.co/%1$s";
+
+ @Bean
+ protected NavigationHelper navigationHelper;
+
+ // Views
+ @ViewById
+ protected ImageView faviconImage;
+ @ViewById
+ protected TextView nameText, newcountText;
+ @ViewById
+ protected ProgressBar loadingProgress;
+
+ public RssfeedView(Context context) {
+ super(context);
+ }
+
+ public void bind(RssfeedLoader rssfeedLoader) {
+
+ // Show the RSS feed name and either a loading indicator or the number of new items
+ nameText.setText(rssfeedLoader.getSetting().getName());
+ if (rssfeedLoader.hasError() || rssfeedLoader.getChannel() != null) {
+ loadingProgress.setVisibility(View.GONE);
+ newcountText.setVisibility(View.VISIBLE);
+ newcountText.setText(rssfeedLoader.hasError()? "?": Integer.toString(rssfeedLoader.getNewCount()));
+ } else {
+ loadingProgress.setVisibility(View.VISIBLE);
+ newcountText.setVisibility(View.GONE);
+ }
+
+ // Clear and then asynchronously load the RSS feed site' favicon
+ // Uses the g.etfv.co service to resolve the favicon of any feed URL
+ faviconImage.setImageDrawable(null);
+ navigationHelper.getImageCache().displayImage(String.format(GETFVO_URL, rssfeedLoader.getSetting().getUrl()),
+ faviconImage);
+
+ }
+
+ public Channel getChannel() {
+ return null;
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedsActivity.java b/core/src/org/transdroid/core/gui/rss/RssfeedsActivity.java
new file mode 100644
index 00000000..d61f39e2
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/rss/RssfeedsActivity.java
@@ -0,0 +1,176 @@
+package org.transdroid.core.gui.rss;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.androidannotations.annotations.AfterViews;
+import org.androidannotations.annotations.Background;
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.FragmentById;
+import org.androidannotations.annotations.OptionsItem;
+import org.androidannotations.annotations.UiThread;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.*;
+import org.transdroid.core.gui.*;
+import org.transdroid.core.gui.log.Log;
+import org.transdroid.core.gui.navigation.NavigationHelper;
+import org.transdroid.core.rssparser.Channel;
+import org.transdroid.core.rssparser.RssParser;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+
+import de.keyboardsurfer.android.widget.crouton.Crouton;
+
+@EActivity(resName = "activity_rssfeeds")
+public class RssfeedsActivity extends SherlockFragmentActivity {
+
+ // Settings and local data
+ @Bean
+ protected ApplicationSettings applicationSettings;
+ protected List loaders;
+
+ // Contained feeds and items fragments
+ @FragmentById(resName = "rssfeeds_list")
+ protected RssfeedsFragment fragmentFeeds;
+ @FragmentById(resName = "rssitems_list")
+ protected RssitemsFragment fragmentItems;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ // Set the theme according to the user preference
+ if (SystemSettings_.getInstance_(this).useDarkTheme()) {
+ setTheme(R.style.TransdroidTheme_Dark);
+ getSupportActionBar().setIcon(R.drawable.ic_activity_torrents);
+ }
+ super.onCreate(savedInstanceState);
+ }
+
+ @AfterViews
+ protected void init() {
+ // Simple action bar with up button and correct title font
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(getString(R.string.rss_feeds)));
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @OptionsItem(android.R.id.home)
+ protected void navigateUp() {
+ TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ refreshFeeds();
+ }
+
+ /**
+ * Reload the RSS feed settings and start loading all the feeds. To be called from contained fragments.
+ */
+ public void refreshFeeds() {
+ loaders = new ArrayList();
+ // For each RSS feed setting the user created, start a loader that retrieved the RSS feed (via a background
+ // thread) and, on success, determines the new items in the feed
+ for (RssfeedSetting setting : applicationSettings.getRssfeedSettings()) {
+ RssfeedLoader loader = new RssfeedLoader(setting);
+ loaders.add(loader);
+ loadRssfeed(loader);
+ }
+ fragmentFeeds.update(loaders);
+ }
+
+ /**
+ * Performs the loading of the RSS feed content and parsing of items, in a background thread.
+ * @param loader The RSS feed loader for which to retrieve the contents
+ */
+ @Background
+ protected void loadRssfeed(RssfeedLoader loader) {
+ try {
+ // Load and parse the feed
+ RssParser parser = new RssParser(loader.getSetting().getUrl());
+ parser.parse();
+ handleRssfeedResult(loader, parser.getChannel(), false);
+ } catch (Exception e) {
+ // Catch any error that may occurred and register this failure
+ handleRssfeedResult(loader, null, true);
+ Log.i(this, "RSS feed " + loader.getSetting().getUrl() + " error: " + e.toString());
+ }
+
+ }
+
+ /**
+ * Stores the retrieved RSS feed content channel into the loader and updates the RSS feed in the feeds list
+ * fragment.
+ * @param loader The RSS feed loader that was executed
+ * @param channel The data that was retrieved, or null if it could not be parsed
+ * @param hasError True if a connection error occurred in the loading of the feed; false otherwise
+ */
+ @UiThread
+ protected void handleRssfeedResult(RssfeedLoader loader, Channel channel, boolean hasError) {
+ loader.update(channel, hasError);
+ fragmentFeeds.notifyDataSetChanged();
+ }
+
+ /**
+ * Opens an RSS feed in the dedicated fragment (if there was space in the UI) or a new {@link RssitemsActivity}.
+ * Optionally this also registers in the user preferences that the feed was now viewed, so that in the future the
+ * new items can be properly marked.
+ * @param loader The RSS feed loader (with settings and the loaded content channel) to show
+ * @param markAsViewedNow True if the user settings should be updated to reflect this feed's last viewed date; false
+ * otherwise
+ */
+ public void openRssfeed(RssfeedLoader loader, boolean markAsViewedNow) {
+
+ // The RSS feed content was loaded and can now be shown in the dedicated fragment or a new activity
+ if (fragmentItems != null) {
+
+ // If desired, update the lastViewedDate of this feed in the user setting; this won't be loaded until the
+ // RSS
+ // feeds screen in opened again.
+ if (!loader.hasError() && loader.getChannel() != null && markAsViewedNow) {
+ applicationSettings.setRssfeedLastViewer(loader.getSetting().getOrder(), new Date());
+ }
+ fragmentItems.update(loader.getChannel(), loader.hasError());
+
+ } else {
+
+ // Error message or not yet loaded? Show a toast message instead of opening the items activity
+ if (loader.hasError()) {
+ Crouton.showText(this, R.string.rss_error, NavigationHelper.CROUTON_INFO_STYLE);
+ return;
+ }
+ if (loader.getChannel() == null || loader.getChannel().getItems().size() == 0) {
+ Crouton.showText(this, R.string.rss_notloaded, NavigationHelper.CROUTON_INFO_STYLE);
+ return;
+ }
+
+ // If desired, update the lastViewedDate of this feed in the user setting; this won't be loaded until the
+ // RSS
+ // feeds screen in opened again
+ if (markAsViewedNow) {
+ applicationSettings.setRssfeedLastViewer(loader.getSetting().getOrder(), new Date());
+ }
+
+ String name = loader.getChannel().getTitle();
+ if (TextUtils.isEmpty(name))
+ name = loader.getSetting().getName();
+ if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(loader.getSetting().getUrl())) {
+ String host = Uri.parse(loader.getSetting().getUrl()).getHost();
+ name = host;
+ }
+ RssitemsActivity_.intent(this).rssfeed(loader.getChannel()).rssfeedName(name).start();
+
+ }
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedsAdapter.java b/core/src/org/transdroid/core/gui/rss/RssfeedsAdapter.java
new file mode 100644
index 00000000..305d7867
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/rss/RssfeedsAdapter.java
@@ -0,0 +1,72 @@
+package org.transdroid.core.gui.rss;
+
+import java.util.List;
+
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.RootContext;
+import org.transdroid.core.app.settings.RssfeedSetting;
+import org.transdroid.core.rssparser.Channel;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * Adapter that contains a list of {@link RssfeedSetting}s, each with associated loaded RSS feed {@link Channel}.
+ * @author Eric Kok
+ */
+@EBean
+public class RssfeedsAdapter extends BaseAdapter {
+
+ private List loaders = null;
+
+ @RootContext
+ protected Context context;
+
+ /**
+ * Allows updating the full internal list of feed loaders at once, replacing the old list
+ * @param loaders The new list of RSS feed loader objects, which pair settings and a loaded channel
+ */
+ public void update(List loaders) {
+ this.loaders = loaders;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ if (loaders == null)
+ return 0;
+ return loaders.size();
+ }
+
+ @Override
+ public RssfeedLoader getItem(int position) {
+ if (loaders == null)
+ return null;
+ return loaders.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ RssfeedView rssfeedView;
+ if (convertView == null) {
+ rssfeedView = RssfeedView_.build(context);
+ } else {
+ rssfeedView = (RssfeedView) convertView;
+ }
+ rssfeedView.bind(getItem(position));
+ return rssfeedView;
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/rss/RssfeedsFragment.java b/core/src/org/transdroid/core/gui/rss/RssfeedsFragment.java
new file mode 100644
index 00000000..e0bd46c4
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/rss/RssfeedsFragment.java
@@ -0,0 +1,82 @@
+package org.transdroid.core.gui.rss;
+
+import java.util.List;
+
+import org.androidannotations.annotations.AfterViews;
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EFragment;
+import org.androidannotations.annotations.ItemClick;
+import org.androidannotations.annotations.OptionsItem;
+import org.androidannotations.annotations.OptionsMenu;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.core.R;
+import org.transdroid.core.gui.settings.MainSettingsActivity_;
+
+import android.view.View;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.SherlockListView;
+
+/**
+ * Fragment lists the RSS feeds the user wants to monitor and, if room, the list of items in a feed in a right pane.
+ * @author Eric Kok
+ */
+@EFragment(resName = "fragment_rssfeeds")
+@OptionsMenu(resName = "fragment_rssfeeds")
+public class RssfeedsFragment extends SherlockFragment {
+
+ // Views
+ @ViewById(resName = "rssfeeds_list")
+ protected SherlockListView feedsList;
+ @Bean
+ protected RssfeedsAdapter rssfeedsAdapter;
+ @ViewById
+ protected TextView nosettingsText;
+
+ @AfterViews
+ protected void init() {
+ feedsList.setAdapter(rssfeedsAdapter);
+ }
+
+ public void update(List loaders) {
+ rssfeedsAdapter.update(loaders);
+ boolean hasSettings = !(loaders == null || loaders.size() == 0);
+ feedsList.setVisibility(hasSettings ? View.VISIBLE: View.GONE);
+ nosettingsText.setVisibility(hasSettings ? View.GONE: View.VISIBLE);
+ getActivity().supportInvalidateOptionsMenu();
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(R.id.action_settings).setShowAsAction(
+ rssfeedsAdapter == null || rssfeedsAdapter.getCount() == 0 ? MenuItem.SHOW_AS_ACTION_ALWAYS
+ : MenuItem.SHOW_AS_ACTION_NEVER);
+ }
+
+ @OptionsItem(resName = "action_settings")
+ protected void openSettings() {
+ MainSettingsActivity_.intent(getActivity()).start();
+ }
+
+ @OptionsItem(resName = "action_refresh")
+ protected void refreshScreen() {
+ ((RssfeedsActivity)getActivity()).refreshFeeds();
+ }
+
+ @ItemClick(resName = "rssfeeds_list")
+ protected void onFeedClicked(RssfeedLoader loader) {
+ ((RssfeedsActivity)getActivity()).openRssfeed(loader, true);
+ }
+
+ /**
+ * Notifies the contained list of RSS feeds that the underlying data has been changed.
+ */
+ public void notifyDataSetChanged() {
+ rssfeedsAdapter.notifyDataSetChanged();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/rss/RssitemStatusLayout.java b/core/src/org/transdroid/core/gui/rss/RssitemStatusLayout.java
new file mode 100644
index 00000000..7c6ff9a5
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/rss/RssitemStatusLayout.java
@@ -0,0 +1,63 @@
+package org.transdroid.core.gui.rss;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import fr.marvinlabs.widget.CheckableRelativeLayout;
+
+/**
+ * A relative layout that that is checkable (to be used in a contextual action bar) and shows a coloured bar in the far
+ * left indicating the view status, that is, if the item is new to the user or was viewed earlier.
+ * @author Eric Kok
+ */
+public class RssitemStatusLayout extends CheckableRelativeLayout {
+
+ private final float scale = getContext().getResources().getDisplayMetrics().density;
+ private final int WIDTH = (int) (6 * scale + 0.5f);
+
+ private Boolean isNew = null;
+ private final Paint oldPaint = new Paint();
+ private final Paint newPaint = new Paint();
+ private final RectF fullRect = new RectF();
+
+ public RssitemStatusLayout(Context context) {
+ super(context);
+ initPaints();
+ setWillNotDraw(false);
+ }
+
+ public RssitemStatusLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initPaints();
+ setWillNotDraw(false);
+ }
+
+ private void initPaints() {
+ oldPaint.setColor(0xFF9E9E9E); // Grey
+ newPaint.setColor(0xFF8ACC12); // Normal green
+ }
+
+ public void setIsNew(Boolean isNew) {
+ this.isNew = isNew;
+ this.invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ int height = getHeight();
+ int width = WIDTH;
+ fullRect.set(0, 0, width, height);
+
+ if (isNew == null) {
+ return;
+ }
+
+ canvas.drawRect(fullRect, isNew? newPaint: oldPaint);
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/rss/RssitemView.java b/core/src/org/transdroid/core/gui/rss/RssitemView.java
new file mode 100644
index 00000000..a38a1c3e
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/rss/RssitemView.java
@@ -0,0 +1,36 @@
+package org.transdroid.core.gui.rss;
+
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.core.rssparser.Item;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+import android.widget.TextView;
+
+/**
+ * View that represents some {@link Item} object, which is a single item in some RSS feed.
+ * @author Eric Kok
+ */
+@EViewGroup(resName = "list_item_rssitem")
+public class RssitemView extends RssitemStatusLayout {
+
+ // Views
+ @ViewById
+ protected TextView nameText, dateText;
+
+ public RssitemView(Context context) {
+ super(context);
+ }
+
+ public void bind(Item rssitem) {
+
+ nameText.setText(rssitem.getTitle());
+ dateText.setText(rssitem.getPubdate() == null ? "" : DateUtils.getRelativeDateTimeString(getContext(), rssitem
+ .getPubdate().getTime(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS,
+ DateUtils.FORMAT_ABBREV_MONTH));
+ setIsNew(rssitem.isNew());
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/rss/RssitemsActivity.java b/core/src/org/transdroid/core/gui/rss/RssitemsActivity.java
new file mode 100644
index 00000000..9126af1d
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/rss/RssitemsActivity.java
@@ -0,0 +1,65 @@
+package org.transdroid.core.gui.rss;
+
+import org.androidannotations.annotations.AfterViews;
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.Extra;
+import org.androidannotations.annotations.FragmentById;
+import org.androidannotations.annotations.OptionsItem;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.*;
+import org.transdroid.core.gui.*;
+import org.transdroid.core.gui.navigation.NavigationHelper;
+import org.transdroid.core.rssparser.Channel;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+
+@EActivity(resName = "activity_rssitems")
+public class RssitemsActivity extends SherlockFragmentActivity {
+
+ @Extra
+ protected Channel rssfeed = null;
+ @Extra
+ protected String rssfeedName;
+
+ @FragmentById(resName = "rssitems_list")
+ protected RssitemsFragment fragmentItems;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ // Set the theme according to the user preference
+ if (SystemSettings_.getInstance_(this).useDarkTheme()) {
+ setTheme(R.style.TransdroidTheme_Dark);
+ getSupportActionBar().setIcon(R.drawable.ic_activity_torrents);
+ }
+ super.onCreate(savedInstanceState);
+ }
+
+ @AfterViews
+ protected void init() {
+
+ // We require an RSS feed to be specified; otherwise close the activity
+ if (rssfeed == null) {
+ finish();
+ return;
+ }
+
+ // Simple action bar with up button and torrent name as title
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(rssfeedName));
+
+ // Get the intent extras and show them to the already loaded fragment
+ fragmentItems.update(rssfeed, false);
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @OptionsItem(android.R.id.home)
+ protected void navigateUp() {
+ TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/rss/RssitemsAdapter.java b/core/src/org/transdroid/core/gui/rss/RssitemsAdapter.java
new file mode 100644
index 00000000..e174ff4d
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/rss/RssitemsAdapter.java
@@ -0,0 +1,70 @@
+package org.transdroid.core.gui.rss;
+
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.RootContext;
+import org.transdroid.core.rssparser.Channel;
+import org.transdroid.core.rssparser.Item;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * Adapter that contains a list of {@link Item}s in an RSS feed.
+ * @author Eric Kok
+ */
+@EBean
+public class RssitemsAdapter extends BaseAdapter {
+
+ private Channel rssfeed = null;
+
+ @RootContext
+ protected Context context;
+
+ /**
+ * Allows updating the full RSS feed (channel and contained items), replacing the old data
+ * @param newRssfeeds The new RSS feed contents
+ */
+ public void update(Channel rssfeed) {
+ this.rssfeed = rssfeed;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ if (rssfeed == null)
+ return 0;
+ return rssfeed.getItems().size();
+ }
+
+ @Override
+ public Item getItem(int position) {
+ if (rssfeed == null)
+ return null;
+ return rssfeed.getItems().get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ RssitemView rssitemView;
+ if (convertView == null) {
+ rssitemView = RssitemView_.build(context);
+ } else {
+ rssitemView = (RssitemView) convertView;
+ }
+ rssitemView.bind(getItem(position));
+ return rssitemView;
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/rss/RssitemsFragment.java b/core/src/org/transdroid/core/gui/rss/RssitemsFragment.java
new file mode 100644
index 00000000..f58d0ff2
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/rss/RssitemsFragment.java
@@ -0,0 +1,151 @@
+package org.transdroid.core.gui.rss;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.androidannotations.annotations.AfterViews;
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EFragment;
+import org.androidannotations.annotations.InstanceState;
+import org.androidannotations.annotations.ItemClick;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.core.R;
+import org.transdroid.core.gui.navigation.SelectionManagerMode;
+import org.transdroid.core.rssparser.Channel;
+import org.transdroid.core.rssparser.Item;
+
+import android.content.Intent;
+import android.view.View;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.actionbarsherlock.view.ActionMode;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.SherlockListView;
+import com.actionbarsherlock.view.SherlockListView.MultiChoiceModeListenerCompat;
+
+/**
+ * Fragment that lists the items in a specific RSS feed
+ * @author Eric Kok
+ */
+@EFragment(resName = "fragment_rssitems")
+public class RssitemsFragment extends SherlockFragment {
+
+ @InstanceState
+ protected Channel rssfeed = null;
+ @InstanceState
+ protected boolean hasError = false;
+
+ // Views
+ @ViewById(resName = "rssitems_list")
+ protected SherlockListView rssitemsList;
+ @Bean
+ protected RssitemsAdapter rssitemsAdapter;
+ @ViewById
+ protected TextView emptyText;
+
+ @AfterViews
+ protected void init() {
+
+ // Set up the list adapter, which allows multi-select
+ rssitemsList.setAdapter(rssitemsAdapter);
+ rssitemsList.setMultiChoiceModeListener(onItemsSelected);
+ update(rssfeed, hasError);
+
+ }
+
+ /**
+ * Update the shown RSS items in the list.
+ * @param channel The loaded RSS content channel object
+ * @param hasError True if there were errors in loading the channel, in which case an error text is shown; false
+ * otherwise
+ */
+ public void update(Channel channel, boolean hasError) {
+ rssitemsAdapter.update(channel);
+ rssitemsList.setVisibility(View.GONE);
+ emptyText.setVisibility(View.VISIBLE);
+ if (hasError) {
+ emptyText.setText(R.string.rss_error);
+ return;
+ }
+ if (channel == null) {
+ emptyText.setText(R.string.rss_noselection);
+ return;
+ }
+ if (channel.getItems().size() == 0) {
+ emptyText.setText(R.string.rss_empty);
+ return;
+ }
+ rssitemsList.setVisibility(View.VISIBLE);
+ emptyText.setVisibility(View.INVISIBLE);
+ }
+
+ @ItemClick(resName = "rssitems_list")
+ protected void onItemClicked(Item item) {
+ Intent i = new Intent(Intent.ACTION_VIEW, item.getTheLinkUri());
+ i.putExtra("TORRENT_TITLE", item.getTitle());
+ startActivity(i);
+ }
+
+ private MultiChoiceModeListenerCompat onItemsSelected = new MultiChoiceModeListenerCompat() {
+
+ SelectionManagerMode selectionManagerMode;
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ // Show contextual action bar to add items in batch mode
+ mode.getMenuInflater().inflate(R.menu.fragment_rssitems_cab, menu);
+ selectionManagerMode = new SelectionManagerMode(rssitemsList, R.plurals.rss_itemsselected);
+ selectionManagerMode.onCreateActionMode(mode, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return selectionManagerMode.onPrepareActionMode(mode, menu);
+ }
+
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+
+ // Get checked torrents
+ List- checked = new ArrayList
- ();
+ for (int i = 0; i < rssitemsList.getCheckedItemPositions().size(); i++) {
+ if (rssitemsList.getCheckedItemPositions().valueAt(i))
+ checked.add(rssitemsAdapter.getItem(rssitemsList.getCheckedItemPositions().keyAt(i)));
+ }
+
+ int itemId = item.getItemId();
+ if (itemId == R.id.action_addall) {
+ // Start an Intent that adds multiple items at once, by supplying the urls and titles as string array
+ // extras and setting the Intent action to ADD_MULTIPLE
+ Intent intent = new Intent("org.transdroid.ADD_MULTIPLE");
+ String[] urls = new String[checked.size()];
+ String[] titles = new String[checked.size()];
+ for (int i = 0; i < checked.size(); i++) {
+ urls[i] = checked.get(i).getTheLink();
+ titles[i] = checked.get(i).getTitle();
+ }
+ intent.putExtra("TORRENT_URLS", urls);
+ intent.putExtra("TORRENT_TITLES", titles);
+ startActivity(intent);
+ mode.finish();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
+ selectionManagerMode.onItemCheckedStateChanged(mode, position, id, checked);
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ selectionManagerMode.onDestroyActionMode(mode);
+ }
+
+ };
+
+}
diff --git a/core/src/org/transdroid/core/gui/search/BarcodeHelper.java b/core/src/org/transdroid/core/gui/search/BarcodeHelper.java
new file mode 100644
index 00000000..ccf46913
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/BarcodeHelper.java
@@ -0,0 +1,72 @@
+package org.transdroid.core.gui.search;
+
+import org.transdroid.core.R;
+import org.transdroid.core.app.search.GoogleWebSearchBarcodeResolver;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.v4.app.DialogFragment;
+import android.text.TextUtils;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+
+public class BarcodeHelper {
+
+ public static final int ACTIVITY_BARCODE = 0x0000c0de; // A 'random' ID to identify scan intents
+ public static final Uri SCANNER_MARKET_URI = Uri.parse("market://search?q=pname:com.google.zxing.client.android");
+
+ /**
+ * Call this to start a bar code scanner intent. The calling activity will receive an Intent result with ID
+ * {@link #ACTIVITY_BARCODE}. From there {@link #handleScanResult(int, Intent)} should be called to parse the result
+ * into a search query.
+ * @param activity The calling activity, to which the result is returned or a dialog is bound that asks to install
+ * the bar code scanner
+ */
+ public static void startBarcodeScanner(final SherlockFragmentActivity activity) {
+ try {
+ // Start a bar code scanner that can handle the SCAN intent (specifically ZXing)
+ activity.startActivityForResult(new Intent("com.google.zxing.client.android.SCAN"), ACTIVITY_BARCODE);
+ } catch (Exception e) {
+ // Can't start the bar code scanner, for example with a SecurityException or when ZXing is not present
+ new DialogFragment() {
+ public android.app.Dialog onCreateDialog(android.os.Bundle savedInstanceState) {
+ return new AlertDialog.Builder(activity).setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(activity.getString(R.string.search_barcodescannernotfound))
+ .setPositiveButton(android.R.string.yes, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (activity != null)
+ activity.startActivity(new Intent(Intent.ACTION_VIEW, SCANNER_MARKET_URI));
+ }
+ }).setNegativeButton(android.R.string.no, null).create();
+ };
+ }.show(activity.getSupportFragmentManager(), "installscanner");
+ }
+ }
+
+ /**
+ * The activity that called {@link #startBarcodeScanner(SherlockFragmentActivity)} should call this after the scan
+ * result was returned. This will parse the scan data and return a query search query appropriate to the bar code.
+ * @param resultCode The raw result code as returned by the bar code scanner
+ * @param data The raw data as returned from the bar code scanner
+ * @return A String that can be used as new search query, or null if the bar code could not be scanned or no query
+ * can be constructed for it
+ */
+ public static String handleScanResult(int resultCode, Intent data) {
+ String contents = data.getStringExtra("SCAN_RESULT");
+ String formatName = data.getStringExtra("SCAN_RESULT_FORMAT");
+ if (formatName != null && formatName.equals("QR_CODE")) {
+ // Scanned barcode was a QR code: return the contents directly
+ return contents;
+ } else {
+ if (TextUtils.isEmpty(contents))
+ return null;
+ // Get a meaningful search query based on a Google Search product lookup
+ return GoogleWebSearchBarcodeResolver.resolveBarcode(contents);
+ }
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/search/FilePickerHelper.java b/core/src/org/transdroid/core/gui/search/FilePickerHelper.java
new file mode 100644
index 00000000..de463505
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/FilePickerHelper.java
@@ -0,0 +1,47 @@
+package org.transdroid.core.gui.search;
+
+import org.transdroid.core.R;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.v4.app.DialogFragment;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+
+public class FilePickerHelper {
+
+ public static final int ACTIVITY_FILEPICKER = 0x0000c0df; // A 'random' ID to identify file picker intents
+ public static final Uri FILEMANAGER_MARKET_URI = Uri.parse("market://search?q=pname:org.openintents.filemanager");
+
+ /**
+ * Call this to start a file picker intent. The calling activity will receive an Intent result with ID
+ * {@link #ACTIVITY_FILEPICKER} with an Intent that contains the selected local file as data Intent.
+ * @param activity The calling activity, to which the result is returned or a dialog is bound that asks to install
+ * the file picker
+ */
+ public static void startFilePicker(final SherlockFragmentActivity activity) {
+ try {
+ // Start a file manager that can handle the PICK_FILE intent (specifically IO File Manager)
+ activity.startActivityForResult(new Intent("org.openintents.action.PICK_FILE"), ACTIVITY_FILEPICKER);
+ } catch (Exception e) {
+ // Can't start the file manager, for example with a SecurityException or when IO File Manager is not present
+ new DialogFragment() {
+ public android.app.Dialog onCreateDialog(android.os.Bundle savedInstanceState) {
+ return new AlertDialog.Builder(activity).setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(activity.getString(R.string.search_filemanagernotfound))
+ .setPositiveButton(android.R.string.yes, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (activity != null)
+ activity.startActivity(new Intent(Intent.ACTION_VIEW, FILEMANAGER_MARKET_URI));
+ }
+ }).setNegativeButton(android.R.string.no, null).create();
+ };
+ }.show(activity.getSupportFragmentManager(), "installfilemanager");
+ }
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/search/SearchActivity.java b/core/src/org/transdroid/core/gui/search/SearchActivity.java
new file mode 100644
index 00000000..da834b76
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/SearchActivity.java
@@ -0,0 +1,251 @@
+package org.transdroid.core.gui.search;
+
+import java.util.List;
+
+import org.androidannotations.annotations.AfterViews;
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.FragmentById;
+import org.androidannotations.annotations.OptionsItem;
+import org.androidannotations.annotations.OptionsMenu;
+import org.androidannotations.annotations.SystemService;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.core.R;
+import org.transdroid.core.app.search.SearchHelper;
+import org.transdroid.core.app.search.SearchSite;
+import org.transdroid.core.app.settings.*;
+import org.transdroid.core.gui.*;
+import org.transdroid.core.gui.navigation.NavigationHelper;
+
+import android.annotation.TargetApi;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.SearchRecentSuggestions;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.ActionBar.OnNavigationListener;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.SherlockListView;
+import com.actionbarsherlock.widget.SearchView;
+
+/**
+ * An activity that shows search results to the user (after a query was supplied by the standard Android search manager)
+ * and either shows the list of search sites on the left (e.g. on tablets) or allows switching between search sites via
+ * the action bar spinner.
+ * @author Eric Kok
+ */
+@EActivity(resName = "activity_search")
+@OptionsMenu(resName = "activity_search")
+public class SearchActivity extends SherlockFragmentActivity implements OnNavigationListener {
+
+ @FragmentById(resName = "searchresults_list")
+ protected SearchResultsFragment fragmentResults;
+ @ViewById
+ protected SherlockListView searchsitesList;
+ @ViewById
+ protected TextView installmoduleText;
+ @Bean
+ protected ApplicationSettings applicationSettings;
+ @Bean
+ protected NavigationHelper navigationHelper;
+ @Bean
+ protected SearchHelper searchHelper;
+ @SystemService
+ protected SearchManager searchManager;
+ private SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this,
+ TorrentSearchHistoryProvider.AUTHORITY, TorrentSearchHistoryProvider.MODE);
+
+ private List
searchSites;
+ private SearchSetting lastUsedSite;
+ private String lastUsedQuery;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ // Set the theme according to the user preference
+ if (SystemSettings_.getInstance_(this).useDarkTheme()) {
+ setTheme(R.style.TransdroidTheme_Dark);
+ getSupportActionBar().setIcon(R.drawable.ic_activity_torrents);
+ }
+ super.onCreate(savedInstanceState);
+ }
+
+ @AfterViews
+ protected void init() {
+
+ // Get the user query, as coming from the standard SearchManager
+ handleIntent(getIntent());
+
+ if (!searchHelper.isTorrentSearchInstalled()) {
+ // The module install text will be shown instead (in onPrepareOptionsMenu)
+ return;
+ }
+
+ // Load sites and find the last used (or set as default) search site
+ searchSites = applicationSettings.getSearchSettings();
+ lastUsedSite = applicationSettings.getLastUsedSearchSite();
+ int lastUsedPosition = -1;
+ if (lastUsedSite != null) {
+ for (int i = 0; i < searchSites.size(); i++) {
+ if (searchSites.get(i).getKey().equals(lastUsedSite.getKey())) {
+ lastUsedPosition = i;
+ break;
+ }
+ }
+ }
+
+ // Allow site selection via list (on large screens) or action bar spinner
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ if (searchsitesList != null) {
+ // The current layout has a dedicated list view to select the search site
+ SearchSitesAdapter searchSitesAdapter = SearchSitesAdapter_.getInstance_(this);
+ searchSitesAdapter.update(searchSites);
+ searchsitesList.setAdapter(searchSitesAdapter);
+ searchsitesList.setOnItemClickListener(onSearchSiteClicked);
+ // Select the last used site; this also starts the search!
+ if (lastUsedPosition >= 0)
+ searchsitesList.setItemChecked(lastUsedPosition, true);
+ } else {
+ // Use the action bar spinner to select sites
+ getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+ getSupportActionBar().setDisplayShowTitleEnabled(false);
+ getSupportActionBar()
+ .setListNavigationCallbacks(new SearchSettingsDropDownAdapter(this, searchSites), this);
+ // Select the last used site; this also starts the search!
+ if (lastUsedPosition >= 0)
+ getSupportActionBar().setSelectedNavigationItem(lastUsedPosition);
+ }
+
+ }
+
+ @TargetApi(Build.VERSION_CODES.FROYO)
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ if (navigationHelper.enableSearchUi()) {
+ // For Android 2.1+, add an expandable SearchView to the action bar
+ MenuItem item = menu.findItem(R.id.action_search);
+ if (android.os.Build.VERSION.SDK_INT >= 8) {
+ final SearchView searchView = new SearchView(this);
+ searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
+ searchView.setQueryRefinementEnabled(true);
+ item.setActionView(searchView);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ boolean searchInstalled = searchHelper.isTorrentSearchInstalled();
+ menu.findItem(R.id.action_search).setVisible(searchInstalled);
+ menu.findItem(R.id.action_refresh).setVisible(searchInstalled);
+ menu.findItem(R.id.action_downloadsearch).setVisible(!searchInstalled);
+ if (searchsitesList != null)
+ searchsitesList.setVisibility(searchInstalled ? View.VISIBLE : View.GONE);
+ if (searchInstalled)
+ getSupportFragmentManager().beginTransaction().show(fragmentResults).commit();
+ else
+ getSupportFragmentManager().beginTransaction().hide(fragmentResults).commit();
+ installmoduleText.setVisibility(searchInstalled ? View.GONE : View.VISIBLE);
+
+ return true;
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ handleIntent(intent);
+ refreshSearch();
+ }
+
+ private void handleIntent(Intent intent) {
+ lastUsedQuery = parseQuery(intent);
+ getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(lastUsedQuery));
+
+ // Is this actually a full HTTP URL? Then redirect this request to add the URL directly
+ if (lastUsedQuery != null
+ && (lastUsedQuery.startsWith("http") || lastUsedQuery.startsWith("https")
+ || lastUsedQuery.startsWith("magnet") || lastUsedQuery.startsWith("file"))) {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(lastUsedQuery)));
+ finish();
+ return;
+ }
+
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @OptionsItem(android.R.id.home)
+ protected void navigateUp() {
+ TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
+ }
+
+ private OnItemClickListener onSearchSiteClicked = new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ lastUsedSite = searchSites.get(position);
+ refreshSearch();
+ }
+ };
+
+ @Override
+ public boolean onNavigationItemSelected(int itemPosition, long itemId) {
+ lastUsedSite = searchSites.get(itemPosition);
+ refreshSearch();
+ return true;
+ }
+
+ /**
+ * Extracts the query string from the search {@link Intent}
+ * @return The query string that was entered by the user
+ */
+ private String parseQuery(Intent intent) {
+
+ String query = null;
+ if (intent.getAction().equals(Intent.ACTION_SEARCH)) {
+ query = intent.getStringExtra(SearchManager.QUERY);
+ } else if (intent.getAction().equals(Intent.ACTION_SEND)) {
+ query = SendIntentHelper.cleanUpText(intent);
+ }
+ if (query != null && query.length() > 0) {
+
+ // Remember this search query to later show as a suggestion
+ suggestions.saveRecentQuery(query, null);
+ return query;
+
+ }
+ return null;
+
+ }
+
+ @OptionsItem(resName = "action_refresh")
+ protected void refreshSearch() {
+ if (lastUsedSite instanceof WebsearchSetting) {
+ // Start a browser page directly to the requested search results
+ WebsearchSetting websearch = (WebsearchSetting) lastUsedSite;
+ startActivity(new Intent(Intent.ACTION_VIEW,
+ Uri.parse(String.format(websearch.getBaseUrl(), lastUsedQuery))));
+ } else if (lastUsedSite instanceof SearchSite) {
+ // Save the search site currently used to search for future usage
+ applicationSettings.setLastUsedSearchSite((SearchSite) lastUsedSite);
+ // Ask the results fragment to start a search for the specified query
+ fragmentResults.startSearch(lastUsedQuery, (SearchSite) lastUsedSite);
+ }
+ }
+
+ @OptionsItem(resName = "action_downloadsearch")
+ protected void downloadSearchModule() {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.transdroid.org/latest-search")));
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/search/SearchHistoryProvider.java b/core/src/org/transdroid/core/gui/search/SearchHistoryProvider.java
new file mode 100644
index 00000000..1419d177
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/SearchHistoryProvider.java
@@ -0,0 +1,25 @@
+package org.transdroid.core.gui.search;
+
+import android.content.Context;
+import android.content.SearchRecentSuggestionsProvider;
+import android.provider.SearchRecentSuggestions;
+
+/**
+ * Provides search suggestions by simply returning previous user entries.
+ * @author Eric Kok
+ */
+public class SearchHistoryProvider extends SearchRecentSuggestionsProvider {
+
+ public final static String AUTHORITY = "org.transdroid.core.gui.search.SearchHistoryProvider";
+ public final static int MODE = DATABASE_MODE_QUERIES;
+
+ public SearchHistoryProvider() {
+ setupSuggestions(AUTHORITY, MODE);
+ }
+
+ public static void clearHistory(Context context) {
+ SearchRecentSuggestions suggestions = new SearchRecentSuggestions(context, SearchHistoryProvider.AUTHORITY,
+ SearchHistoryProvider.MODE);
+ suggestions.clearHistory();
+ }
+}
diff --git a/core/src/org/transdroid/core/gui/search/SearchResultView.java b/core/src/org/transdroid/core/gui/search/SearchResultView.java
new file mode 100644
index 00000000..1fb7e4a8
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/SearchResultView.java
@@ -0,0 +1,40 @@
+package org.transdroid.core.gui.search;
+
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.core.R;
+import org.transdroid.core.app.search.SearchResult;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+import android.widget.TextView;
+import fr.marvinlabs.widget.CheckableRelativeLayout;
+
+/**
+ * View that represents a {@link SearchResult} object from an in-app search
+ * @author Eric Kok
+ */
+@EViewGroup(resName = "list_item_searchresult")
+public class SearchResultView extends CheckableRelativeLayout {
+
+ // Views
+ @ViewById
+ protected TextView nameText, seedersText, leechersText, sizeText, dateText;
+
+ public SearchResultView(Context context) {
+ super(context);
+ }
+
+ public void bind(SearchResult result) {
+
+ nameText.setText(result.getName());
+ sizeText.setText(result.getSize());
+ dateText.setText(result.getAddedOn() == null ? "" : DateUtils.getRelativeDateTimeString(getContext(), result
+ .getAddedOn().getTime(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS,
+ DateUtils.FORMAT_ABBREV_MONTH));
+ seedersText.setText(getContext().getString(R.string.search_seeders, result.getSeeders()));
+ leechersText.setText(getContext().getString(R.string.search_leechers, result.getLeechers()));
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/search/SearchResultsAdapter.java b/core/src/org/transdroid/core/gui/search/SearchResultsAdapter.java
new file mode 100644
index 00000000..20b37678
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/SearchResultsAdapter.java
@@ -0,0 +1,71 @@
+package org.transdroid.core.gui.search;
+
+import java.util.List;
+
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.RootContext;
+import org.transdroid.core.app.search.SearchResult;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * Adapter that contains a list of {@link SearchResult}s.
+ * @author Eric Kok
+ */
+@EBean
+public class SearchResultsAdapter extends BaseAdapter {
+
+ private List results = null;
+
+ @RootContext
+ protected Context context;
+
+ /**
+ * Allows updating the search results, replacing the old data
+ * @param newRssfeeds The new list of search results
+ */
+ public void update(List results) {
+ this.results = results;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ if (results == null)
+ return 0;
+ return results.size();
+ }
+
+ @Override
+ public SearchResult getItem(int position) {
+ if (results == null)
+ return null;
+ return results.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ SearchResultView rssitemView;
+ if (convertView == null) {
+ rssitemView = SearchResultView_.build(context);
+ } else {
+ rssitemView = (SearchResultView) convertView;
+ }
+ rssitemView.bind(getItem(position));
+ return rssitemView;
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/search/SearchResultsFragment.java b/core/src/org/transdroid/core/gui/search/SearchResultsFragment.java
new file mode 100644
index 00000000..67f5a490
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/SearchResultsFragment.java
@@ -0,0 +1,169 @@
+package org.transdroid.core.gui.search;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.androidannotations.annotations.AfterViews;
+import org.androidannotations.annotations.Background;
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EFragment;
+import org.androidannotations.annotations.InstanceState;
+import org.androidannotations.annotations.ItemClick;
+import org.androidannotations.annotations.UiThread;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.core.R;
+import org.transdroid.core.app.search.SearchHelper;
+import org.transdroid.core.app.search.SearchHelper.SearchSortOrder;
+import org.transdroid.core.app.search.SearchResult;
+import org.transdroid.core.app.search.SearchSite;
+import org.transdroid.core.gui.navigation.SelectionManagerMode;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.actionbarsherlock.app.SherlockFragment;
+import com.actionbarsherlock.view.ActionMode;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.SherlockListView;
+import com.actionbarsherlock.view.SherlockListView.MultiChoiceModeListenerCompat;
+
+/**
+ * Fragment that lists the items in a specific RSS feed
+ * @author Eric Kok
+ */
+@EFragment(resName = "fragment_searchresults")
+public class SearchResultsFragment extends SherlockFragment {
+
+ @InstanceState
+ protected ArrayList results = null;
+ @Bean
+ protected SearchHelper searchHelper;
+
+ // Views
+ @ViewById(resName = "searchresults_list")
+ protected SherlockListView resultsList;
+ @Bean
+ protected SearchResultsAdapter resultsAdapter;
+ @ViewById
+ protected TextView emptyText;
+ @ViewById
+ protected ProgressBar loadingProgress;
+
+ @AfterViews
+ protected void init() {
+
+ // Set up the list adapter, which allows multi-select
+ resultsList.setAdapter(resultsAdapter);
+ resultsList.setMultiChoiceModeListener(onItemsSelected);
+ if (results != null)
+ showResults();
+
+ }
+
+ public void startSearch(String query, SearchSite site) {
+ loadingProgress.setVisibility(View.VISIBLE);
+ resultsList.setVisibility(View.GONE);
+ emptyText.setVisibility(View.GONE);
+ performSearch(query, site);
+ }
+
+ @Background
+ protected void performSearch(String query, SearchSite site) {
+ results = searchHelper.search(query, site, SearchSortOrder.BySeeders);
+ showResults();
+ }
+
+ @UiThread
+ protected void showResults() {
+ loadingProgress.setVisibility(View.GONE);
+ if (results == null || results.size() == 0) {
+ resultsList.setVisibility(View.GONE);
+ emptyText.setVisibility(View.VISIBLE);
+ return;
+ }
+ resultsAdapter.update(results);
+ resultsList.setVisibility(View.VISIBLE);
+ emptyText.setVisibility(View.GONE);
+ }
+
+ @ItemClick(resName = "searchresults_list")
+ protected void onItemClicked(SearchResult item) {
+ Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(item.getTorrentUrl()));
+ i.putExtra("TORRENT_TITLE", item.getName());
+ startActivity(i);
+ }
+
+ private MultiChoiceModeListenerCompat onItemsSelected = new MultiChoiceModeListenerCompat() {
+
+ SelectionManagerMode selectionManagerMode;
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ // Show contextual action bar to add items in batch mode
+ mode.getMenuInflater().inflate(R.menu.fragment_searchresults_cab, menu);
+ selectionManagerMode = new SelectionManagerMode(resultsList, R.plurals.search_resutlsselected);
+ selectionManagerMode.onCreateActionMode(mode, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return selectionManagerMode.onPrepareActionMode(mode, menu);
+ }
+
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+
+ // Get checked torrents
+ List checked = new ArrayList();
+ for (int i = 0; i < resultsList.getCheckedItemPositions().size(); i++) {
+ if (resultsList.getCheckedItemPositions().valueAt(i))
+ checked.add(resultsAdapter.getItem(resultsList.getCheckedItemPositions().keyAt(i)));
+ }
+
+ int itemId = item.getItemId();
+ if (itemId == R.id.action_addall) {
+ // Start an Intent that adds multiple items at once, by supplying the urls and titles as string array
+ // extras and setting the Intent action to ADD_MULTIPLE
+ Intent intent = new Intent("org.transdroid.ADD_MULTIPLE");
+ String[] urls = new String[checked.size()];
+ String[] titles = new String[checked.size()];
+ for (int i = 0; i < checked.size(); i++) {
+ urls[i] = checked.get(i).getTorrentUrl();
+ titles[i] = checked.get(i).getName();
+ }
+ intent.putExtra("TORRENT_URLS", urls);
+ intent.putExtra("TORRENT_TITLES", titles);
+ startActivity(intent);
+ mode.finish();
+ return true;
+ } else if (itemId == R.id.action_showdetails) {
+ SearchResult first = checked.get(0);
+ // Open the torrent's web page in the browser
+ if (checked.size() > 1)
+ Toast.makeText(getActivity(), getString(R.string.search_openingdetails, first), Toast.LENGTH_LONG)
+ .show();
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(first.getDetailsUrl())));
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
+ selectionManagerMode.onItemCheckedStateChanged(mode, position, id, checked);
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ selectionManagerMode.onDestroyActionMode(mode);
+ }
+
+ };
+
+}
diff --git a/core/src/org/transdroid/core/gui/search/SearchSetting.java b/core/src/org/transdroid/core/gui/search/SearchSetting.java
new file mode 100644
index 00000000..b0fce14d
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/SearchSetting.java
@@ -0,0 +1,19 @@
+package org.transdroid.core.gui.search;
+
+import org.transdroid.core.gui.lists.SimpleListItem;
+
+public interface SearchSetting extends SimpleListItem {
+
+ /**
+ * Should return a unique key for this search setting, so that it can be compared (using equals()) to other settings.
+ * @return A unique string identifying this search setting
+ */
+ public String getKey();
+
+ /**
+ * Should return an URL (which may still be abstract and not the actual search URL) specific to the search site
+ * @return A clean URL directing to the search site, to, for example, get the favicon of the site
+ */
+ public String getBaseUrl();
+
+}
diff --git a/core/src/org/transdroid/core/gui/search/SearchSettingSelectionView.java b/core/src/org/transdroid/core/gui/search/SearchSettingSelectionView.java
new file mode 100644
index 00000000..0749399a
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/SearchSettingSelectionView.java
@@ -0,0 +1,28 @@
+package org.transdroid.core.gui.search;
+
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+/**
+ * View that shows, as part of the action bar spinner, which {@link SearchSetting} is currently chosen.
+ * @author Eric Kok
+ */
+@EViewGroup(resName = "actionbar_searchsite")
+public class SearchSettingSelectionView extends FrameLayout {
+
+ @ViewById
+ protected TextView searchsiteText;
+
+ public SearchSettingSelectionView(Context context) {
+ super(context);
+ }
+
+ public void bind(SearchSetting searchSettingItem) {
+ searchsiteText.setText(searchSettingItem.getName());
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/search/SearchSettingsDropDownAdapter.java b/core/src/org/transdroid/core/gui/search/SearchSettingsDropDownAdapter.java
new file mode 100644
index 00000000..a82c3029
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/SearchSettingsDropDownAdapter.java
@@ -0,0 +1,45 @@
+package org.transdroid.core.gui.search;
+
+import java.util.List;
+
+import org.transdroid.core.gui.lists.SimpleListItem;
+import org.transdroid.core.gui.navigation.FilterListItemAdapter;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * List adapter that holds search settings, that is, web searches and in-app search sites, displayed as content to a
+ * Spinner instead of a ListView.
+ * @author Eric Kok
+ */
+public class SearchSettingsDropDownAdapter extends FilterListItemAdapter {
+
+ private final Context context;
+
+ public SearchSettingsDropDownAdapter(Context context, List extends SimpleListItem> items) {
+ super(context, items);
+ this.context = context;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // This returns the item to show in the action bar spinner
+ SearchSettingSelectionView filterItemView;
+ if (convertView == null || !(convertView instanceof SearchSettingSelectionView)) {
+ filterItemView = SearchSettingSelectionView_.build(context);
+ } else {
+ filterItemView = (SearchSettingSelectionView) convertView;
+ }
+ filterItemView.bind((SearchSetting) getItem(position));
+ return filterItemView;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ // This returns the item to show in the drop down list
+ return super.getView(position, convertView, parent);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/search/SearchSiteView.java b/core/src/org/transdroid/core/gui/search/SearchSiteView.java
new file mode 100644
index 00000000..e28581b1
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/SearchSiteView.java
@@ -0,0 +1,49 @@
+package org.transdroid.core.gui.search;
+
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.core.app.settings.RssfeedSetting;
+import org.transdroid.core.gui.navigation.NavigationHelper;
+
+import android.content.Context;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * View that represents some {@link RssfeedSetting} object and displays name as well as loads a favicon for the feed's
+ * site and can load how many new items are available.
+ * @author Eric Kok
+ */
+@EViewGroup(resName = "list_item_searchsite")
+public class SearchSiteView extends LinearLayout {
+
+ private static final String GETFVO_URL = "http://g.etfv.co/%1$s";
+
+ @Bean
+ protected NavigationHelper navigationHelper;
+
+ // Views
+ @ViewById
+ protected ImageView faviconImage;
+ @ViewById
+ protected TextView nameText;
+
+ public SearchSiteView(Context context) {
+ super(context);
+ }
+
+ public void bind(SearchSetting rssfeedLoader) {
+
+ // Show the RSS feed name and either a loading indicator or the number of new items
+ nameText.setText(rssfeedLoader.getName());
+ // Clear and then asynchronously load the site's favicon
+ // Uses the g.etfv.co service to resolve the favicon of any URL
+ faviconImage.setImageDrawable(null);
+ navigationHelper.getImageCache().displayImage(String.format(GETFVO_URL, rssfeedLoader.getBaseUrl()),
+ faviconImage);
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/search/SearchSitesAdapter.java b/core/src/org/transdroid/core/gui/search/SearchSitesAdapter.java
new file mode 100644
index 00000000..eabf68b0
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/SearchSitesAdapter.java
@@ -0,0 +1,72 @@
+package org.transdroid.core.gui.search;
+
+import java.util.List;
+
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.RootContext;
+import org.transdroid.core.app.search.SearchSite;
+import org.transdroid.core.app.settings.WebsearchSetting;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * Adapter that contains a list of {@link SearchSetting}s, either {@link SearchSite} or {@link WebsearchSetting}.
+ * @author Eric Kok
+ */
+@EBean
+public class SearchSitesAdapter extends BaseAdapter {
+
+ private List sites = null;
+
+ @RootContext
+ protected Context context;
+
+ /**
+ * Allows updating the full internal list of sites at once, replacing the old list
+ * @param sites The new list of search sites, either in-app or web search settings
+ */
+ public void update(List sites) {
+ this.sites = sites;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ if (sites == null)
+ return 0;
+ return sites.size();
+ }
+
+ @Override
+ public SearchSetting getItem(int position) {
+ if (sites == null)
+ return null;
+ return sites.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ SearchSiteView rssfeedView;
+ if (convertView == null) {
+ rssfeedView = SearchSiteView_.build(context);
+ } else {
+ rssfeedView = (SearchSiteView) convertView;
+ }
+ rssfeedView.bind(getItem(position));
+ return rssfeedView;
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/search/SendIntentHelper.java b/core/src/org/transdroid/core/gui/search/SendIntentHelper.java
new file mode 100644
index 00000000..a82dfc46
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/SendIntentHelper.java
@@ -0,0 +1,61 @@
+package org.transdroid.core.gui.search;
+
+import android.content.Intent;
+
+/**
+ * Used to clean up text as received from a generic ACTION_SEND intent. This class is highly custom-based for known
+ * applications, i.e. the EXTRA_TEXT send by some known applications.
+ * @author Eric Kok
+ */
+public class SendIntentHelper {
+
+ private static final String SOUNDHOUND1 = "Just used #SoundHound to find ";
+ private static final String SOUNDHOUND1_END = " http://";
+ private static final String SHAZAM = "I just used Shazam to discover ";
+ private static final String SHAZAM_END = ". http://";
+ private static final String YOUTUBE_ID = "Watch \"";
+ private static final String YOUTUBE_START = "\"";
+ private static final String YOUTUBE_END = "\"";
+
+ /**
+ * Cleans a SEND intent text string by removing irrelevant parts, so that the remaining text can be used as search
+ * string. Typically deals with specific known applications such as Shazam and YouTube's SEND intents.
+ * @param intent The original SEND intent that was received
+ * @return A cleaned string to be used as search query
+ */
+ public static String cleanUpText(Intent intent) {
+
+ if (intent == null || !intent.hasExtra(Intent.EXTRA_TEXT)) {
+ return null;
+ }
+ String text = intent.getStringExtra(Intent.EXTRA_TEXT);
+ try {
+
+ // Soundhound song/artist share
+ if (text.startsWith(SOUNDHOUND1)) {
+ return cutOut(text, SOUNDHOUND1, SOUNDHOUND1_END).replace(" by ", " ");
+ }
+ // Shazam song share
+ if (text.startsWith(SHAZAM)) {
+ return cutOut(text, SHAZAM, SHAZAM_END).replace(" by ", " ");
+ }
+ // YouTube app share (stores title in EXTRA_SUBJECT)
+ if (intent.hasExtra(Intent.EXTRA_SUBJECT)) {
+ String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
+ if (subject.startsWith(YOUTUBE_ID)) {
+ return cutOut(subject, YOUTUBE_START, YOUTUBE_END);
+ }
+ }
+
+ } catch (Exception e) {
+ // Ignore any errors in parsing; just return the raw text
+ }
+ return text;
+ }
+
+ private static String cutOut(String text, String start, String end) {
+ int startAt = text.indexOf(start) + start.length();
+ return text.substring(startAt, text.indexOf(end, startAt));
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/search/TorrentSearchHistoryProvider.java b/core/src/org/transdroid/core/gui/search/TorrentSearchHistoryProvider.java
new file mode 100644
index 00000000..548e0d34
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/TorrentSearchHistoryProvider.java
@@ -0,0 +1,26 @@
+package org.transdroid.core.gui.search;
+
+import android.content.Context;
+import android.content.SearchRecentSuggestionsProvider;
+import android.provider.SearchRecentSuggestions;
+
+/**
+ * Provides a wrapper for the {@link SearchRecentSuggestionsProvider} to show the last torrent searches to the user.
+ * @author Eric Kok
+ */
+public class TorrentSearchHistoryProvider extends SearchRecentSuggestionsProvider {
+
+ public static final String AUTHORITY = "org.transdroid.core.gui.search.TorrentSearchHistoryProvider";
+ public static final int MODE = DATABASE_MODE_QUERIES;
+
+ public TorrentSearchHistoryProvider() {
+ super();
+ setupSuggestions(AUTHORITY, MODE);
+ }
+
+ public static void clearHistory(Context context) {
+ SearchRecentSuggestions suggestions = new SearchRecentSuggestions(context,
+ TorrentSearchHistoryProvider.AUTHORITY, TorrentSearchHistoryProvider.MODE);
+ suggestions.clearHistory();
+ }
+}
diff --git a/core/src/org/transdroid/core/gui/search/UrlEntryDialog.java b/core/src/org/transdroid/core/gui/search/UrlEntryDialog.java
new file mode 100644
index 00000000..0c00cb2a
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/search/UrlEntryDialog.java
@@ -0,0 +1,43 @@
+package org.transdroid.core.gui.search;
+
+import org.transdroid.core.gui.TorrentsActivity;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.support.v4.app.DialogFragment;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+public class UrlEntryDialog {
+
+ /**
+ * Opens a dialog that allows entry of a single URL string, which (on confirmation) will be supplied to the calling
+ * activity's {@link TorrentsActivity#addTorrentByUrl(String, String) method}.
+ * @param activity The activity that opens (and owns) this dialog
+ */
+ public static void startUrlEntry(final TorrentsActivity activity) {
+ new DialogFragment() {
+ public android.app.Dialog onCreateDialog(android.os.Bundle savedInstanceState) {
+ final EditText urlInput = new EditText(activity);
+ urlInput.setInputType(InputType.TYPE_TEXT_VARIATION_URI);
+ ((InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE)).toggleSoftInput(
+ InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
+ return new AlertDialog.Builder(activity).setView(urlInput)
+ .setPositiveButton(android.R.string.ok, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // Assume text entry box input as URL and treat the filename (after the last /) as title
+ String url = urlInput.getText().toString();
+ if (activity != null && !TextUtils.isEmpty(url))
+ activity.addTorrentByUrl(url, url.substring(url.lastIndexOf("/")));
+ }
+ }).setNegativeButton(android.R.string.cancel, null).create();
+ };
+ }.show(activity.getSupportFragmentManager(), "urlentry");
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/settings/AboutDialog.java b/core/src/org/transdroid/core/gui/settings/AboutDialog.java
new file mode 100644
index 00000000..72ccfe95
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/AboutDialog.java
@@ -0,0 +1,37 @@
+package org.transdroid.core.gui.settings;
+
+import org.transdroid.core.R;
+import org.transdroid.core.gui.navigation.DialogHelper;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ * Fragment that shows info about the application developer and used open source libraries.
+ * @author Eric Kok
+ */
+public class AboutDialog implements DialogHelper.DialogSpecification {
+
+ private static final long serialVersionUID = -4711432869714292985L;
+
+ @Override
+ public int getDialogLayoutId() {
+ return R.layout.dialog_about;
+ }
+
+ @Override
+ public int getDialogMenuId() {
+ return R.menu.dialog_about;
+ }
+
+ @Override
+ public boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId) {
+ if (selectedItemId == R.id.action_visitwebsite) {
+ ownerActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://transdroid.org")));
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/settings/ChangelogDialog.java b/core/src/org/transdroid/core/gui/settings/ChangelogDialog.java
new file mode 100644
index 00000000..c777598c
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/ChangelogDialog.java
@@ -0,0 +1,37 @@
+package org.transdroid.core.gui.settings;
+
+import org.transdroid.core.R;
+import org.transdroid.core.gui.navigation.DialogHelper;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ * Fragment that shows recent app changes.
+ * @author Eric Kok
+ */
+public class ChangelogDialog implements DialogHelper.DialogSpecification {
+
+ private static final long serialVersionUID = -4563410777022941124L;
+
+ @Override
+ public int getDialogLayoutId() {
+ return R.layout.dialog_changelog;
+ }
+
+ @Override
+ public int getDialogMenuId() {
+ return R.menu.dialog_about;
+ }
+
+ @Override
+ public boolean onMenuItemSelected(Activity ownerActivity, int selectedItemId) {
+ if (selectedItemId == R.id.action_visitwebsite) {
+ ownerActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://transdroid.org/about/changelog/")));
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/settings/KeyBoundPreferencesActivity.java b/core/src/org/transdroid/core/gui/settings/KeyBoundPreferencesActivity.java
new file mode 100644
index 00000000..2d1a5c0a
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/KeyBoundPreferencesActivity.java
@@ -0,0 +1,191 @@
+package org.transdroid.core.gui.settings;
+
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.Extra;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+
+import com.actionbarsherlock.app.SherlockPreferenceActivity;
+
+/**
+ * Abstract activity that helps implement a preference screen for key-bound settings, i.e. settings of which there can
+ * be multiple and which are identified by an ascending order number/unique key. A typical implementation calls
+ * {@link #init(int, int)} during the {@link #onCreate(android.os.Bundle)} (but after calling super.onCreate(Bundle))
+ * and then call initXPreference for each contained preference. {@link #onPreferencesChanged()} can be overridden to
+ * react to preference changes, e.g. when field availability should be updated (and where preference dependency isn't
+ * enough).
+ * @author Eric Kok
+ */
+@EActivity
+public abstract class KeyBoundPreferencesActivity extends SherlockPreferenceActivity {
+
+ @Extra
+ protected int key = -1;
+
+ private SharedPreferences sharedPrefs;
+
+ /**
+ * Should be called during the activity {@link #onCreate(android.os.Bundle)} (but after super.onCreate(Bundle)) to
+ * load the preferences for this screen from an XML resource.
+ * @param preferencesResId The XML resource to read preferences from, which may contain embedded
+ * {@link PreferenceScreen} objects
+ * @param currentMaxKey The value of what is currently the last defined settings object, or -1 of no settings were
+ * defined so far at all
+ */
+ @SuppressWarnings("deprecation")
+ protected final void init(int preferencesResId, int currentMaxKey) {
+
+ // Load the raw preferences to show in this screen
+ addPreferencesFromResource(preferencesResId);
+ sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ // If no key was supplied (in the extra bundle) then use a new key instead
+ if (key < 0) {
+ key = currentMaxKey + 1;
+ }
+
+ }
+
+ protected void onResume() {
+ super.onResume();
+ // Monitor preference changes
+ PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(
+ onPreferenceChangeListener);
+ };
+
+ protected void onPause() {
+ super.onPause();
+ // Stop monitoring preference changes
+ PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(
+ onPreferenceChangeListener);
+ };
+
+ private OnSharedPreferenceChangeListener onPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ onPreferencesChanged();
+ }
+ };
+
+ /**
+ * Key-bound preference activities may override this method if they want to react to preference changes.
+ */
+ protected void onPreferencesChanged() {
+ }
+
+ /**
+ * Updates a preference that allows for text entry via a dialog. This is used for both string and integer values. No
+ * default value will be shown.
+ * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
+ * item_name_[key]
+ * @return The concrete {@link EditTextPreference} that is bound to this preference
+ */
+ protected final EditTextPreference initTextPreference(String baseName) {
+ return initTextPreference(baseName, null);
+ }
+
+ /**
+ * Updates a preference that allows for text entry via a dialog. This is used for both string and integer values.
+ * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
+ * item_name_[key]
+ * @param defValue The default value for this preference, as shown when no value was yet stored
+ * @return The concrete {@link EditTextPreference} that is bound to this preference
+ */
+ protected final EditTextPreference initTextPreference(String baseName, String defValue) {
+ return initTextPreference(baseName, defValue, null);
+ }
+
+ /**
+ * Updates a preference (including dependency) that allows for text entry via a dialog. This is used for both string
+ * and integer values.
+ * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
+ * item_name_[key]
+ * @param defValue The default value for this preference, as shown when no value was yet stored
+ * @param dependency The base name of the preference to which this preference depends
+ * @return The concrete {@link EditTextPreference} that is bound to this preference
+ */
+ @SuppressWarnings("deprecation")
+ protected final EditTextPreference initTextPreference(String baseName, String defValue, String dependency) {
+ // Update the loaded Preference with the actual preference key to load/store with
+ EditTextPreference pref = (EditTextPreference) findPreference(baseName);
+ pref.setKey(baseName + "_" + key);
+ pref.setDependency(dependency == null? null: dependency + "_" + key);
+ // Update the Preference by loading the current stored value into the EditText, if it exists
+ pref.setText(sharedPrefs.getString(baseName + "_" + key, defValue));
+ return pref;
+ }
+
+ /**
+ * Updates a preference that simply shows a check box. No default value will be shown.
+ * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
+ * item_name_[key]
+ * @return The concrete {@link CheckBoxPreference} that is bound to this preference
+ */
+ protected final CheckBoxPreference initBooleanPreference(String baseName) {
+ return initBooleanPreference(baseName, false);
+ }
+
+ /**
+ * Updates a preference that simply shows a check box.
+ * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
+ * item_name_[key]
+ * @param defValue The default value for this preference, as shown when no value was yet stored
+ * @return The concrete {@link CheckBoxPreference} that is bound to this preference
+ */
+ protected final CheckBoxPreference initBooleanPreference(String baseName, boolean defValue) {
+ return initBooleanPreference(baseName, defValue, null);
+ }
+
+ /**
+ * Updates a preference (including dependency) that simply shows a check box.
+ * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
+ * item_name_[key]
+ * @param defValue The default value for this preference, as shown when no value was yet stored
+ * @param dependency The base name of the preference to which this preference depends
+ * @return The concrete {@link CheckBoxPreference} that is bound to this preference
+ */
+ @SuppressWarnings("deprecation")
+ protected final CheckBoxPreference initBooleanPreference(String baseName, boolean defValue, String dependency) {
+ // Update the loaded Preference with the actual preference key to load/store with
+ CheckBoxPreference pref = (CheckBoxPreference) findPreference(baseName);
+ pref.setKey(baseName + "_" + key);
+ pref.setDependency(dependency == null? null: dependency + "_" + key);
+ // Update the Preference by loading the current stored value into the Checkbox, if it exists
+ pref.setChecked(sharedPrefs.getBoolean(baseName + "_" + key, defValue));
+ return pref;
+ }
+
+ /**
+ * Updates a preference that allows picking an item from a list. No default value will be shown.
+ * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
+ * item_name_[key]
+ * @return The concrete {@link ListPreference} that is bound to this preference
+ */
+ protected final ListPreference initListPreference(String baseName) {
+ return initListPreference(baseName, null);
+ }
+
+ /**
+ * Updates a preference that allows picking an item from a list.
+ * @param baseName The base name of the stored preference, e.g. item_name, which will then actually be stored under
+ * item_name_[key]
+ * @param defValue The default value for this preference, as shown when no value was yet stored
+ * @return The concrete {@link ListPreference} that is bound to this preference
+ */
+ @SuppressWarnings("deprecation")
+ protected final ListPreference initListPreference(String baseName, String defValue) {
+ // Update the loaded Preference with the actual preference key to load/store with
+ ListPreference pref = (ListPreference) findPreference(baseName);
+ pref.setKey(baseName + "_" + key);
+ // Update the Preference by selecting the current stored value in the list, if it exists
+ pref.setValue(sharedPrefs.getString(baseName + "_" + key, defValue));
+ return pref;
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/settings/MainSettingsActivity.java b/core/src/org/transdroid/core/gui/settings/MainSettingsActivity.java
new file mode 100644
index 00000000..818851b4
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/MainSettingsActivity.java
@@ -0,0 +1,187 @@
+package org.transdroid.core.gui.settings;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.OptionsItem;
+import org.transdroid.core.R;
+import org.transdroid.core.app.search.SearchHelper;
+import org.transdroid.core.app.search.SearchSite;
+import org.transdroid.core.app.settings.ApplicationSettings;
+import org.transdroid.core.app.settings.RssfeedSetting;
+import org.transdroid.core.app.settings.ServerSetting;
+import org.transdroid.core.app.settings.WebsearchSetting;
+import org.transdroid.core.gui.*;
+import org.transdroid.core.gui.settings.RssfeedPreference.OnRssfeedClickedListener;
+import org.transdroid.core.gui.settings.ServerPreference.OnServerClickedListener;
+import org.transdroid.core.gui.settings.WebsearchPreference.OnWebsearchClickedListener;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+
+import com.actionbarsherlock.app.SherlockPreferenceActivity;
+
+/**
+ * The main activity that provides access to all application settings. It shows the configured serves, web search sites
+ * and RSS feeds along with other general settings.
+ * @author Eric Kok
+ */
+@EActivity
+public class MainSettingsActivity extends SherlockPreferenceActivity {
+
+ @Bean
+ protected ApplicationSettings applicationSettings;
+ @Bean
+ protected SearchHelper searchHelper;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Note: Settings are loaded in onResume()
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ if (getPreferenceScreen() != null)
+ getPreferenceScreen().removeAll();
+
+ // Load the preference menu and attach actions
+ addPreferencesFromResource(R.xml.pref_main);
+ findPreference("header_addserver").setOnPreferenceClickListener(onAddServer);
+ findPreference("header_addwebsearch").setOnPreferenceClickListener(onAddWebsearch);
+ findPreference("header_addrssfeed").setOnPreferenceClickListener(onAddRssfeed);
+ findPreference("header_background").setOnPreferenceClickListener(onBackgroundSettings);
+ findPreference("header_system").setOnPreferenceClickListener(onSystemSettings);
+
+ // Add existing servers
+ List servers = applicationSettings.getServerSettings();
+ for (ServerSetting serverSetting : servers) {
+ getPreferenceScreen().addPreference(
+ new ServerPreference(this).setServerSetting(serverSetting).setOnServerClickedListener(
+ onServerClicked));
+ }
+
+ // Add existing websearch sites
+ List websearches = applicationSettings.getWebsearchSettings();
+ for (WebsearchSetting websearchSetting : websearches) {
+ getPreferenceScreen().addPreference(
+ new WebsearchPreference(this).setWebsearchSetting(websearchSetting).setOnWebsearchClickedListener(
+ onWebsearchClicked));
+ }
+
+ // Add existing RSS feeds
+ List rssfeeds = applicationSettings.getRssfeedSettings();
+ for (RssfeedSetting rssfeedSetting : rssfeeds) {
+ getPreferenceScreen().addPreference(
+ new RssfeedPreference(this).setRssfeedSetting(rssfeedSetting).setOnRssfeedClickedListener(
+ onRssfeedClicked));
+ }
+
+ // Construct list of all available search sites, in-app and web
+ ListPreference setSite = (ListPreference) findPreference("header_setsearchsite");
+ // Retrieve the available in-app search sites (using the Torrent Search package)
+ List searchsites = searchHelper.getAvailableSites();
+ if (searchsites == null)
+ searchsites = new ArrayList();
+ List siteNames = new ArrayList(websearches.size() + searchsites.size());
+ List siteValues = new ArrayList(websearches.size() + searchsites.size());
+ for (SearchSite searchSite : searchsites) {
+ siteNames.add(searchSite.getName());
+ siteValues.add(searchSite.getKey());
+ }
+ for (WebsearchSetting websearch : websearches) {
+ siteNames.add(websearch.getName());
+ siteValues.add(websearch.getKey());
+ }
+ // Supply the Preference list names and values
+ setSite.setEntries(siteNames.toArray(new String[siteNames.size()]));
+ setSite.setEntryValues(siteValues.toArray(new String[siteValues.size()]));
+
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @OptionsItem(android.R.id.home)
+ protected void navigateUp() {
+ TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
+ }
+
+ @Override
+ public void onBuildHeaders(List target) {
+ // TODO: Add two-pane support in settings
+ super.onBuildHeaders(target);
+ }
+
+ private OnPreferenceClickListener onAddServer = new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ ServerSettingsActivity_.intent(MainSettingsActivity.this).start();
+ return true;
+ }
+ };
+
+ private OnPreferenceClickListener onAddWebsearch = new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ WebsearchSettingsActivity_.intent(MainSettingsActivity.this).start();
+ return true;
+ }
+ };
+
+ private OnPreferenceClickListener onAddRssfeed = new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ RssfeedSettingsActivity_.intent(MainSettingsActivity.this).start();
+ return true;
+ }
+ };
+
+ private OnPreferenceClickListener onBackgroundSettings = new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ NotificationSettingsActivity_.intent(MainSettingsActivity.this).start();
+ return true;
+ }
+ };
+
+ private OnPreferenceClickListener onSystemSettings = new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ SystemSettingsActivity_.intent(MainSettingsActivity.this).start();
+ return true;
+ }
+ };
+
+ private OnServerClickedListener onServerClicked = new OnServerClickedListener() {
+ @Override
+ public void onServerClicked(ServerSetting serverSetting) {
+ ServerSettingsActivity_.intent(MainSettingsActivity.this).key(serverSetting.getOrder()).start();
+ }
+ };
+
+ private OnWebsearchClickedListener onWebsearchClicked = new OnWebsearchClickedListener() {
+ @Override
+ public void onWebsearchClicked(WebsearchSetting websearchSetting) {
+ WebsearchSettingsActivity_.intent(MainSettingsActivity.this).key(websearchSetting.getOrder()).start();
+ }
+ };
+
+ private OnRssfeedClickedListener onRssfeedClicked = new OnRssfeedClickedListener() {
+ @Override
+ public void onRssfeedClicked(RssfeedSetting rssfeedSetting) {
+ RssfeedSettingsActivity_.intent(MainSettingsActivity.this).key(rssfeedSetting.getOrder()).start();
+ }
+ };
+
+}
diff --git a/core/src/org/transdroid/core/gui/settings/NotificationSettingsActivity.java b/core/src/org/transdroid/core/gui/settings/NotificationSettingsActivity.java
new file mode 100644
index 00000000..73fc4775
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/NotificationSettingsActivity.java
@@ -0,0 +1,71 @@
+package org.transdroid.core.gui.settings;
+
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.OptionsItem;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.NotificationSettings;
+import org.transdroid.core.service.BootReceiver;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Build;
+import android.os.Bundle;
+
+import com.actionbarsherlock.app.SherlockPreferenceActivity;
+
+@EActivity
+public class NotificationSettingsActivity extends SherlockPreferenceActivity implements
+ OnSharedPreferenceChangeListener {
+
+ @Bean
+ protected NotificationSettings notificationSettings;
+
+ @SuppressWarnings("deprecation")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ // Just load the notification-related preferences from XML
+ addPreferencesFromResource(R.xml.pref_notifications);
+
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // Start/stop the background service appropriately
+ getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @OptionsItem(android.R.id.home)
+ protected void navigateUp() {
+ MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+
+ if (!notificationSettings.isEnabled()) {
+ // Disabled background notifications; disable the alarms that start the service
+ BootReceiver.cancelBackgroundServices(getApplicationContext());
+ }
+
+ // (Re-)enable the alarms for the background services
+ BootReceiver.startBackgroundServices(getApplicationContext(), true);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/settings/RssfeedPreference.java b/core/src/org/transdroid/core/gui/settings/RssfeedPreference.java
new file mode 100644
index 00000000..b47e4772
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/RssfeedPreference.java
@@ -0,0 +1,60 @@
+package org.transdroid.core.gui.settings;
+
+import org.transdroid.core.app.settings.RssfeedSetting;
+
+import android.content.Context;
+import android.preference.Preference;
+
+/**
+ * Represents a {@link RssfeedSetting} in a preferences screen.
+ * @author Eric Kok
+ */
+public class RssfeedPreference extends Preference {
+
+ private static final int ORDER_START = 201;
+
+ private RssfeedSetting rssfeedSetting;
+ private OnRssfeedClickedListener onRssfeedClickedListener = null;
+
+ public RssfeedPreference(Context context) {
+ super(context);
+ setOnPreferenceClickListener(onPreferenceClicked);
+ }
+
+ /**
+ * Set the RSS feed settings object that is bound to this preference item
+ * @param rssfeedSetting The RSS feed settings
+ * @return Itself, for method chaining
+ */
+ public RssfeedPreference setRssfeedSetting(RssfeedSetting rssfeedSetting) {
+ this.rssfeedSetting = rssfeedSetting;
+ setTitle(rssfeedSetting.getName());
+ setSummary(rssfeedSetting.getHumanReadableIdentifier());
+ setOrder(ORDER_START + rssfeedSetting.getOrder());
+ return this;
+ }
+
+ /**
+ * Set a listener that will be notified of click events on this preference
+ * @param onRssfeedClickedListener The click listener to register
+ * @return Itself, for method chaining
+ */
+ public RssfeedPreference setOnRssfeedClickedListener(OnRssfeedClickedListener onRssfeedClickedListener) {
+ this.onRssfeedClickedListener = onRssfeedClickedListener;
+ return this;
+ }
+
+ private OnPreferenceClickListener onPreferenceClicked = new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (onRssfeedClickedListener != null)
+ onRssfeedClickedListener.onRssfeedClicked(rssfeedSetting);
+ return true;
+ }
+ };
+
+ public interface OnRssfeedClickedListener {
+ public void onRssfeedClicked(RssfeedSetting rssfeedSetting);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/settings/RssfeedSettingsActivity.java b/core/src/org/transdroid/core/gui/settings/RssfeedSettingsActivity.java
new file mode 100644
index 00000000..9347cb0d
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/RssfeedSettingsActivity.java
@@ -0,0 +1,50 @@
+package org.transdroid.core.gui.settings;
+
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.OptionsItem;
+import org.androidannotations.annotations.OptionsMenu;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.*;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+
+/**
+ * Activity that allows for a configuration of some RSS feed. The key can be supplied to update an
+ * existing RSS feed setting instead of creating a new one.
+ * @author Eric Kok
+ */
+@EActivity
+@OptionsMenu(resName="activity_deleteableprefs")
+public class RssfeedSettingsActivity extends KeyBoundPreferencesActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ // Load the raw preferences to show in this screen
+ init(R.xml.pref_rssfeed, ApplicationSettings_.getInstance_(this).getMaxRssfeed());
+ initTextPreference("rssfeed_name");
+ initTextPreference("rssfeed_url");
+ initBooleanPreference("rssfeed_reqauth");
+ // TODO: Replace this for cookies support like web searches
+
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @OptionsItem(android.R.id.home)
+ protected void navigateUp() {
+ MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
+ }
+
+ @OptionsItem(resName = "action_removesettings")
+ protected void removeSettings() {
+ ApplicationSettings_.getInstance_(this).removeRssfeedSettings(key);
+ finish();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/settings/ServerPreference.java b/core/src/org/transdroid/core/gui/settings/ServerPreference.java
new file mode 100644
index 00000000..01ab0402
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/ServerPreference.java
@@ -0,0 +1,60 @@
+package org.transdroid.core.gui.settings;
+
+import org.transdroid.core.app.settings.ServerSetting;
+
+import android.content.Context;
+import android.preference.Preference;
+
+/**
+ * Represents a {@link ServerSetting} in a preferences screen.
+ * @author Eric Kok
+ */
+public class ServerPreference extends Preference {
+
+ private static final int ORDER_START = 1;
+
+ private ServerSetting serverSetting;
+ private OnServerClickedListener onServerClickedListener = null;
+
+ public ServerPreference(Context context) {
+ super(context);
+ setOnPreferenceClickListener(onPreferenceClicked);
+ }
+
+ /**
+ * Set the server settings object that is bound to this preference item
+ * @param serverSetting The server settings
+ * @return Itself, for method chaining
+ */
+ public ServerPreference setServerSetting(ServerSetting serverSetting) {
+ this.serverSetting = serverSetting;
+ setTitle(serverSetting.getName());
+ setSummary(serverSetting.getHumanReadableIdentifier());
+ setOrder(ORDER_START + serverSetting.getOrder());
+ return this;
+ }
+
+ /**
+ * Set a listener that will be notified of click events on this preference
+ * @param onServerClickedListener The click listener to register
+ * @return Itself, for method chaining
+ */
+ public ServerPreference setOnServerClickedListener(OnServerClickedListener onServerClickedListener) {
+ this.onServerClickedListener = onServerClickedListener;
+ return this;
+ }
+
+ private OnPreferenceClickListener onPreferenceClicked = new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (onServerClickedListener != null)
+ onServerClickedListener.onServerClicked(serverSetting);
+ return true;
+ }
+ };
+
+ public interface OnServerClickedListener {
+ public void onServerClicked(ServerSetting serverSetting);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/settings/ServerSettingsActivity.java b/core/src/org/transdroid/core/gui/settings/ServerSettingsActivity.java
new file mode 100644
index 00000000..772975d9
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/ServerSettingsActivity.java
@@ -0,0 +1,88 @@
+package org.transdroid.core.gui.settings;
+
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.OptionsItem;
+import org.androidannotations.annotations.OptionsMenu;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.*;
+import org.transdroid.daemon.Daemon;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.EditTextPreference;
+import android.preference.PreferenceManager;
+
+/**
+ * Activity that allows for a configuration of a server. The key can be supplied to update an existing server setting
+ * instead of creating a new one.
+ * @author Eric Kok
+ */
+@EActivity
+@OptionsMenu(resName = "activity_deleteableprefs")
+public class ServerSettingsActivity extends KeyBoundPreferencesActivity {
+
+ private EditTextPreference extraPass, folder, downloadDir;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ // Load the raw preferences to show in this screen
+ init(R.xml.pref_server, ApplicationSettings_.getInstance_(this).getMaxServer());
+ initTextPreference("server_name");
+ initListPreference("server_type");
+ initTextPreference("server_address");
+ initTextPreference("server_port");
+ initTextPreference("server_user");
+ initTextPreference("server_pass");
+ extraPass = initTextPreference("server_extrapass");
+ initTextPreference("server_localaddress");
+ initTextPreference("server_localnetwork");
+ folder = initTextPreference("server_folder");
+ initTextPreference("server_timeout", "8");
+ initBooleanPreference("server_alarmfinished", true);
+ initBooleanPreference("server_alarmnew");
+ initListPreference("server_os", "type_linux");
+ downloadDir = initTextPreference("server_downloaddir");
+ initTextPreference("server_ftpurl");
+ initTextPreference("server_ftppass");
+ initBooleanPreference("server_sslenabled");
+ initBooleanPreference("server_ssltrustall", false, "server_sslenabled");
+ initTextPreference("server_ssltrustkey", null, "server_sslenabled");
+
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @OptionsItem(android.R.id.home)
+ protected void navigateUp() {
+ MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
+ }
+
+ @OptionsItem(resName = "action_removesettings")
+ protected void removeSettings() {
+ ApplicationSettings_.getInstance_(this).removeServerSettings(key);
+ finish();
+ }
+
+ @Override
+ protected void onPreferencesChanged() {
+
+ // Use daemon factory to see if the newly selected daemon supports the feature
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ Daemon daemonType = Daemon.fromCode(prefs.getString("server_type_" + key, null));
+ extraPass.setEnabled(Daemon.supportsExtraPassword(daemonType));
+ folder.setEnabled(daemonType == null ? false : Daemon.supportsCustomFolder(daemonType));
+ downloadDir.setEnabled(daemonType == null ? false : Daemon.needsManualPathSpecified(daemonType));
+ // sslTrustKey.setEnabled(sslValue && !sslTAValue);
+
+ // Adjust title texts accordingly
+ folder.setTitle(daemonType == Daemon.rTorrent ? R.string.pref_scgifolder : R.string.pref_folder);
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/settings/SystemSettingsActivity.java b/core/src/org/transdroid/core/gui/settings/SystemSettingsActivity.java
new file mode 100644
index 00000000..8b9f6352
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/SystemSettingsActivity.java
@@ -0,0 +1,204 @@
+package org.transdroid.core.gui.settings;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.OptionsItem;
+import org.json.JSONException;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.ApplicationSettings;
+import org.transdroid.core.app.settings.SettingsPersistence;
+import org.transdroid.core.gui.log.ErrorLogSender;
+import org.transdroid.core.gui.navigation.DialogHelper;
+import org.transdroid.core.gui.navigation.NavigationHelper;
+import org.transdroid.core.service.BootReceiver;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceManager;
+
+import com.actionbarsherlock.app.SherlockPreferenceActivity;
+
+import de.keyboardsurfer.android.widget.crouton.Crouton;
+
+@EActivity
+public class SystemSettingsActivity extends SherlockPreferenceActivity {
+
+ protected static final int DIALOG_CHANGELOG = 0;
+ protected static final int DIALOG_ABOUT = 1;
+ protected static final int DIALOG_IMPORTSETTINGS = 2;
+ protected static final int DIALOG_EXPORTSETTINGS = 3;
+ protected static final String INSTALLHELP_URI = "http://www.transdroid.org/download/";
+
+ @Bean
+ protected ApplicationSettings applicationSettings;
+ @Bean
+ protected ErrorLogSender errorLogSender;
+ @Bean
+ protected SettingsPersistence settingsPersistence;
+
+ @SuppressWarnings("deprecation")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ // Just load the system-related preferences from XML
+ addPreferencesFromResource(R.xml.pref_system);
+
+ // Handle outgoing links and preference changes
+ findPreference("system_checkupdates").setOnPreferenceClickListener(onCheckUpdatesClick);
+ findPreference("system_sendlog").setOnPreferenceClickListener(onSendLogClick);
+ findPreference("system_installhelp").setOnPreferenceClickListener(onInstallHelpClick);
+ findPreference("system_changelog").setOnPreferenceClickListener(onChangeLogClick);
+ findPreference("system_importsettings").setOnPreferenceClickListener(onImportSettingsClick);
+ findPreference("system_exportsettings").setOnPreferenceClickListener(onExportSettingsClick);
+ findPreference("system_about").setOnPreferenceClickListener(onAboutClick);
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @OptionsItem(android.R.id.home)
+ protected void navigateUp() {
+ MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
+ }
+
+ private OnPreferenceClickListener onCheckUpdatesClick = new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (((CheckBoxPreference) preference).isChecked())
+ BootReceiver.startAppUpdatesService(getApplicationContext());
+ else
+ BootReceiver.cancelAppUpdates(getApplicationContext());
+ return true;
+ }
+ };
+
+ private OnPreferenceClickListener onSendLogClick = new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ errorLogSender.collectAndSendLog(SystemSettingsActivity.this, applicationSettings.getLastUsedServer());
+ return true;
+ }
+ };
+
+ private OnPreferenceClickListener onInstallHelpClick = new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(INSTALLHELP_URI)));
+ return true;
+ }
+ };
+
+ private OnPreferenceClickListener onImportSettingsClick = new OnPreferenceClickListener() {
+ @SuppressWarnings("deprecation")
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ showDialog(DIALOG_IMPORTSETTINGS);
+ return true;
+ }
+ };
+
+ private OnPreferenceClickListener onExportSettingsClick = new OnPreferenceClickListener() {
+ @SuppressWarnings("deprecation")
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ showDialog(DIALOG_EXPORTSETTINGS);
+ return true;
+ }
+ };
+
+ private OnPreferenceClickListener onChangeLogClick = new OnPreferenceClickListener() {
+ @SuppressWarnings("deprecation")
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ showDialog(DIALOG_CHANGELOG);
+ return true;
+ }
+ };
+
+ private OnPreferenceClickListener onAboutClick = new OnPreferenceClickListener() {
+ @SuppressWarnings("deprecation")
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ showDialog(DIALOG_ABOUT);
+ return true;
+ }
+ };
+
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case DIALOG_CHANGELOG:
+ return DialogHelper.showDialog(this, new ChangelogDialog());
+ case DIALOG_ABOUT:
+ return DialogHelper.showDialog(this, new AboutDialog());
+ case DIALOG_IMPORTSETTINGS:
+ // @formatter:off
+ return new AlertDialog.Builder(this)
+ .setMessage(
+ getString(R.string.pref_import_dialog, SettingsPersistence.DEFAULT_SETTINGS_FILE.toString()))
+ .setPositiveButton(android.R.string.ok, importSettings)
+ .setNegativeButton(android.R.string.cancel, null).create();
+ // @formatter:on
+ case DIALOG_EXPORTSETTINGS:
+ // @formatter:off
+ return new AlertDialog.Builder(this)
+ .setMessage(
+ getString(R.string.pref_export_dialog, SettingsPersistence.DEFAULT_SETTINGS_FILE.toString()))
+ .setPositiveButton(android.R.string.ok, exportSettings)
+ .setNegativeButton(android.R.string.cancel, null).create();
+ // @formatter:on
+ }
+ return null;
+ }
+
+ private OnClickListener importSettings = new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SystemSettingsActivity.this);
+ try {
+ settingsPersistence.importSettings(prefs, SettingsPersistence.DEFAULT_SETTINGS_FILE);
+ Crouton.showText(SystemSettingsActivity.this, R.string.pref_import_success,
+ NavigationHelper.CROUTON_INFO_STYLE);
+ } catch (FileNotFoundException e) {
+ Crouton.showText(SystemSettingsActivity.this, R.string.error_file_not_found,
+ NavigationHelper.CROUTON_ERROR_STYLE);
+ } catch (JSONException e) {
+ Crouton.showText(SystemSettingsActivity.this, R.string.error_no_valid_settings_file,
+ NavigationHelper.CROUTON_ERROR_STYLE);
+ }
+ }
+ };
+
+ private OnClickListener exportSettings = new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SystemSettingsActivity.this);
+ try {
+ settingsPersistence.exportSettings(prefs, SettingsPersistence.DEFAULT_SETTINGS_FILE);
+ Crouton.showText(SystemSettingsActivity.this, R.string.pref_export_success,
+ NavigationHelper.CROUTON_INFO_STYLE);
+ } catch (JSONException e) {
+ Crouton.showText(SystemSettingsActivity.this, R.string.error_cant_write_settings_file,
+ NavigationHelper.CROUTON_ERROR_STYLE);
+ } catch (IOException e) {
+ Crouton.showText(SystemSettingsActivity.this, R.string.error_cant_write_settings_file,
+ NavigationHelper.CROUTON_ERROR_STYLE);
+ }
+ }
+ };
+
+}
diff --git a/core/src/org/transdroid/core/gui/settings/WebsearchPreference.java b/core/src/org/transdroid/core/gui/settings/WebsearchPreference.java
new file mode 100644
index 00000000..63550f03
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/WebsearchPreference.java
@@ -0,0 +1,60 @@
+package org.transdroid.core.gui.settings;
+
+import org.transdroid.core.app.settings.WebsearchSetting;
+
+import android.content.Context;
+import android.preference.Preference;
+
+/**
+ * Represents a {@link WebsearchSetting} in a preferences screen.
+ * @author Eric Kok
+ */
+public class WebsearchPreference extends Preference {
+
+ private static final int ORDER_START = 102;
+
+ private WebsearchSetting websearchSetting;
+ private OnWebsearchClickedListener onWebsearchClickedListener = null;
+
+ public WebsearchPreference(Context context) {
+ super(context);
+ setOnPreferenceClickListener(onPreferenceClicked);
+ }
+
+ /**
+ * Set the websearch settings object that is bound to this preference item
+ * @param websearchSetting The websearch settings
+ * @return Itself, for method chaining
+ */
+ public WebsearchPreference setWebsearchSetting(WebsearchSetting websearchSetting) {
+ this.websearchSetting = websearchSetting;
+ setTitle(websearchSetting.getName());
+ setSummary(websearchSetting.getHumanReadableIdentifier());
+ setOrder(ORDER_START + websearchSetting.getOrder());
+ return this;
+ }
+
+ /**
+ * Set a listener that will be notified of click events on this preference
+ * @param onWebsearchClickedListener The click listener to register
+ * @return Itself, for method chaining
+ */
+ public WebsearchPreference setOnWebsearchClickedListener(OnWebsearchClickedListener onWebsearchClickedListener) {
+ this.onWebsearchClickedListener = onWebsearchClickedListener;
+ return this;
+ }
+
+ private OnPreferenceClickListener onPreferenceClicked = new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (onWebsearchClickedListener != null)
+ onWebsearchClickedListener.onWebsearchClicked(websearchSetting);
+ return true;
+ }
+ };
+
+ public interface OnWebsearchClickedListener {
+ public void onWebsearchClicked(WebsearchSetting serverSetting);
+ }
+
+}
diff --git a/core/src/org/transdroid/core/gui/settings/WebsearchSettingsActivity.java b/core/src/org/transdroid/core/gui/settings/WebsearchSettingsActivity.java
new file mode 100644
index 00000000..8871fc51
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/WebsearchSettingsActivity.java
@@ -0,0 +1,49 @@
+package org.transdroid.core.gui.settings;
+
+import org.androidannotations.annotations.EActivity;
+import org.androidannotations.annotations.OptionsItem;
+import org.androidannotations.annotations.OptionsMenu;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.*;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+
+/**
+ * Activity that allows for a configuration of a web search site. The key can be supplied to update an existing web
+ * search site setting instead of creating a new one.
+ * @author Eric Kok
+ */
+@EActivity
+@OptionsMenu(resName="activity_deleteableprefs")
+public class WebsearchSettingsActivity extends KeyBoundPreferencesActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ // Load the raw preferences to show in this screen
+ init(R.xml.pref_websearch, ApplicationSettings_.getInstance_(this).getMaxWebsearch());
+ initTextPreference("websearch_name");
+ initTextPreference("websearch_baseurl");
+ initTextPreference("websearch_cookies");
+
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @OptionsItem(android.R.id.home)
+ protected void navigateUp() {
+ MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
+ }
+
+ @OptionsItem(resName = "action_removesettings")
+ protected void removeSettings() {
+ ApplicationSettings_.getInstance_(this).removeWebsearchSettings(key);
+ finish();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/rssparser/Channel.java b/core/src/org/transdroid/core/rssparser/Channel.java
new file mode 100644
index 00000000..68d105ed
--- /dev/null
+++ b/core/src/org/transdroid/core/rssparser/Channel.java
@@ -0,0 +1,154 @@
+/*
+ * Taken from the 'Learning Android' project, released as Public Domain software at
+ * http://github.com/digitalspaghetti/learning-android and modified heavily for Transdroid
+ */
+package org.transdroid.core.rssparser;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class Channel implements Parcelable {
+
+ private int id;
+ private String title;
+ private String link;
+ private String description;
+ private Date pubDate;
+ private long lastBuildDate;
+ private List categories;
+ private List- items;
+ private String image;
+
+ public Channel() {
+ this.categories = new ArrayList
();
+ this.items = new ArrayList- ();
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public String getLink() {
+ return link;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setPubDate(Date date) {
+ this.pubDate = date;
+ }
+
+ public Date getPubDate() {
+ return pubDate;
+ }
+
+ public void setLastBuildDate(long lastBuildDate) {
+ this.lastBuildDate = lastBuildDate;
+ }
+
+ public long getLastBuildDate() {
+ return lastBuildDate;
+ }
+
+ public void setCategories(List
categories) {
+ this.categories = categories;
+ }
+
+ public void addCategory(String category) {
+ this.categories.add(category);
+ }
+
+ public List getCategories() {
+ return categories;
+ }
+
+ public void setItems(List- items) {
+ this.items = items;
+ }
+
+ public void addItem(Item item) {
+ this.items.add(item);
+ }
+
+ public List
- getItems() {
+ return items;
+ }
+
+ public void setImage(String image) {
+ this.image = image;
+ }
+
+ public String getImage() {
+ return image;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(id);
+ out.writeString(title);
+ out.writeString(link);
+ out.writeString(description);
+ out.writeLong(pubDate == null ? -1 : pubDate.getTime());
+ out.writeLong(lastBuildDate);
+ out.writeTypedList(items);
+ out.writeStringList(categories);
+ out.writeString(image);
+ }
+
+ public static final Parcelable.Creator
CREATOR = new Parcelable.Creator() {
+ public Channel createFromParcel(Parcel in) {
+ return new Channel(in);
+ }
+
+ public Channel[] newArray(int size) {
+ return new Channel[size];
+ }
+ };
+
+ private Channel(Parcel in) {
+ this();
+ id = in.readInt();
+ title = in.readString();
+ link = in.readString();
+ description = in.readString();
+ long pubDateIn = in.readLong();
+ pubDate = pubDateIn == -1 ? null : new Date(pubDateIn);
+ lastBuildDate = in.readLong();
+ categories = new ArrayList();
+ in.readTypedList(items, Item.CREATOR);
+ in.readStringList(categories);
+ image = in.readString();
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/org/transdroid/core/rssparser/HttpHelper.java b/core/src/org/transdroid/core/rssparser/HttpHelper.java
new file mode 100644
index 00000000..9baf4f52
--- /dev/null
+++ b/core/src/org/transdroid/core/rssparser/HttpHelper.java
@@ -0,0 +1,150 @@
+/*
+ * This file is part of Transdroid Torrent Search
+ *
+ *
+ * Transdroid Torrent Search is free software: you can redistribute
+ * it and/or modify it under the terms of the GNU Lesser General
+ * Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * Transdroid Torrent Search is distributed in the hope that it will
+ * be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Transdroid. If not, see .
+ */
+package org.transdroid.core.rssparser;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.util.zip.GZIPInputStream;
+
+import org.apache.http.Header;
+import org.apache.http.HeaderElement;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpResponseInterceptor;
+import org.apache.http.entity.HttpEntityWrapper;
+import org.apache.http.protocol.HttpContext;
+
+/**
+ * Provides a set of general helper methods that can be used in web-based communication.
+ *
+ * @author erickok
+ *
+ */
+public class HttpHelper {
+
+ /**
+ * HTTP request interceptor to allow for GZip-encoded data transfer
+ */
+ public static HttpRequestInterceptor gzipRequestInterceptor = new HttpRequestInterceptor() {
+ public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
+ if (!request.containsHeader("Accept-Encoding")) {
+ request.addHeader("Accept-Encoding", "gzip");
+ }
+ }
+ };
+
+ /**
+ * HTTP response interceptor that decodes GZipped data
+ */
+ public static HttpResponseInterceptor gzipResponseInterceptor = new HttpResponseInterceptor() {
+ public void process(final HttpResponse response, final HttpContext context) throws HttpException, IOException {
+ HttpEntity entity = response.getEntity();
+ Header ceheader = entity.getContentEncoding();
+ if (ceheader != null) {
+ HeaderElement[] codecs = ceheader.getElements();
+ for (int i = 0; i < codecs.length; i++) {
+
+ if (codecs[i].getName().equalsIgnoreCase("gzip")) {
+ response.setEntity(new HttpHelper.GzipDecompressingEntity(response.getEntity()));
+ return;
+ }
+ }
+ }
+ }
+
+ };
+
+ /**
+ * HTTP entity wrapper to decompress GZipped HTTP responses
+ */
+ private static class GzipDecompressingEntity extends HttpEntityWrapper {
+
+ public GzipDecompressingEntity(final HttpEntity entity) {
+ super(entity);
+ }
+
+ @Override
+ public InputStream getContent() throws IOException, IllegalStateException {
+
+ // the wrapped entity's getContent() decides about repeatability
+ InputStream wrappedin = wrappedEntity.getContent();
+
+ return new GZIPInputStream(wrappedin);
+ }
+
+ @Override
+ public long getContentLength() {
+ // length of ungzipped content is not known
+ return -1;
+ }
+
+ }
+
+ /*
+ * To convert the InputStream to String we use the BufferedReader.readLine()
+ * method. We iterate until the BufferedReader return null which means
+ * there's no more data to read. Each line will appended to a StringBuilder
+ * and returned as String.
+ *
+ * Taken from http://senior.ceng.metu.edu.tr/2009/praeda/2009/01/11/a-simple-restful-client-at-android/
+ */
+ public static String convertStreamToString(InputStream is, String encoding) throws UnsupportedEncodingException {
+ InputStreamReader isr;
+ if (encoding != null) {
+ isr = new InputStreamReader(is, encoding);
+ } else {
+ isr = new InputStreamReader(is);
+ }
+ BufferedReader reader = new BufferedReader(isr);
+ StringBuilder sb = new StringBuilder();
+
+ String line = null;
+ try {
+ while ((line = reader.readLine()) != null) {
+ sb.append(line + "\n");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ is.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return sb.toString();
+ }
+
+ public static String convertStreamToString(InputStream is) {
+ try {
+ return convertStreamToString(is, null);
+ } catch (UnsupportedEncodingException e) {
+ // Since this is going to use the default encoding, it is never going to crash on an UnsupportedEncodingException
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/org/transdroid/core/rssparser/Item.java b/core/src/org/transdroid/core/rssparser/Item.java
new file mode 100644
index 00000000..42f80ea9
--- /dev/null
+++ b/core/src/org/transdroid/core/rssparser/Item.java
@@ -0,0 +1,167 @@
+/*
+ * Taken from the 'Learning Android' project, released as Public Domain software at
+ * http://github.com/digitalspaghetti/learning-android and modified heavily for Transdroid
+ */
+package org.transdroid.core.rssparser;
+
+import java.util.Date;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class Item implements Parcelable {
+
+ private int id;
+ private String title;
+ private String link;
+ private String description;
+ private Date pubDate;
+ private String enclosureUrl;
+ private String enclosureType;
+ private long enclosureLength;
+
+ /**
+ * isNew is not parsed from the RSS feed but may be set using {@link #setIsNew(boolean)} manually
+ */
+ private boolean isNew;
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getTitle() {
+ return this.title;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getDescription() {
+ return this.description;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public String getLink() {
+ return this.link;
+ }
+
+ public void setPubdate(Date pubdate) {
+ this.pubDate = pubdate;
+ }
+
+ public Date getPubdate() {
+ return this.pubDate;
+ }
+
+ public void setEnclosureUrl(String enclosureUrl) {
+ this.enclosureUrl = enclosureUrl;
+ }
+
+ public void setEnclosureLength(long enclosureLength) {
+ this.enclosureLength = enclosureLength;
+ }
+
+ public void setEnclosureType(String enclosureType) {
+ this.enclosureType = enclosureType;
+ }
+
+ public String getEnclosureUrl() {
+ return this.enclosureUrl;
+ }
+
+ public String getEnclosureType() {
+ return this.enclosureType;
+ }
+
+ public long getEnclosureLength() {
+ return this.enclosureLength;
+ }
+
+ public void setIsNew(boolean isNew) {
+ this.isNew = isNew;
+ }
+
+ public boolean isNew() {
+ return isNew;
+ }
+
+ /**
+ * Returns 'the' item link as string, which preferably is the enclosure URL, but otherwise the link (or null if that
+ * is empty too).
+ * @return A single link URL string to be used
+ */
+ public String getTheLink() {
+ if (this.getEnclosureUrl() != null) {
+ return this.getEnclosureUrl();
+ } else {
+ return this.getLink();
+ }
+ }
+
+ /**
+ * Returns 'the' item link as URI, which preferably is the enclosure URL, but otherwise the link (or null if that is
+ * empty too).
+ * @return A single link URI to be used
+ */
+ public Uri getTheLinkUri() {
+ return Uri.parse(getTheLink());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(id);
+ out.writeString(title);
+ out.writeString(link);
+ out.writeString(description);
+ out.writeLong(pubDate == null ? -1 : pubDate.getTime());
+ out.writeString(enclosureUrl);
+ out.writeString(enclosureType);
+ out.writeLong(enclosureLength);
+ out.writeInt(isNew ? 1 : 0);
+ }
+
+ public static final Parcelable.Creator- CREATOR = new Parcelable.Creator
- () {
+ public Item createFromParcel(Parcel in) {
+ return new Item(in);
+ }
+
+ public Item[] newArray(int size) {
+ return new Item[size];
+ }
+ };
+
+ public Item() {
+ }
+
+ private Item(Parcel in) {
+ id = in.readInt();
+ title = in.readString();
+ link = in.readString();
+ description = in.readString();
+ long pubDateIn = in.readLong();
+ pubDate = pubDateIn == -1 ? null : new Date(pubDateIn);
+ enclosureUrl = in.readString();
+ enclosureType = in.readString();
+ enclosureLength = in.readLong();
+ isNew = in.readInt() == 1 ? true : false;
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/org/transdroid/core/rssparser/RssParser.java b/core/src/org/transdroid/core/rssparser/RssParser.java
new file mode 100644
index 00000000..ba5d809a
--- /dev/null
+++ b/core/src/org/transdroid/core/rssparser/RssParser.java
@@ -0,0 +1,235 @@
+/*
+ * Taken from the 'Learning Android' project, released as Public Domain software at
+ * http://github.com/digitalspaghetti/learning-android and modified heavily for Transdroid
+ */
+package org.transdroid.core.rssparser;
+
+import java.io.IOException;
+import java.util.Date;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+public class RssParser extends DefaultHandler {
+
+ private String urlString;
+ private Channel channel;
+ private StringBuilder text;
+ private Item item;
+ private boolean imageStatus;
+
+ /**
+ * The constructor for the RSS parser; call {@link #parse()} to synchronously create an HTTP connection and parse
+ * the RSS feed contents. The results can be retrieved with {@link #getChannel()}.
+ * @param url
+ */
+ public RssParser(String url) {
+ this.urlString = url;
+ this.text = new StringBuilder();
+ }
+
+ /**
+ * Returns the loaded RSS feed as channel which contains the individual {@link Item}s
+ * @return A channel object that ocntains the feed details and individual items
+ */
+ public Channel getChannel() {
+ return this.channel;
+ }
+
+ /**
+ * Initialises an HTTP connection, retrieves the content and parses the RSS feed as standard XML.
+ * @throws ParserConfigurationException Thrown if the SX parser is not working corectly
+ * @throws SAXException Thrown if the SAX parser can encounters non-standard XML content
+ * @throws IOException Thrown if the RSS feed content can not be retrieved, such as when no connection is available
+ */
+ public void parse() throws ParserConfigurationException, SAXException, IOException {
+
+ DefaultHttpClient httpclient = initialise();
+ HttpResponse result = httpclient.execute(new HttpGet(urlString));
+ SAXParserFactory spf = SAXParserFactory.newInstance();
+ if (spf != null) {
+ SAXParser sp = spf.newSAXParser();
+ sp.parse(result.getEntity().getContent(), this);
+ }
+
+ }
+
+ private DefaultHttpClient initialise() {
+
+ SchemeRegistry registry = new SchemeRegistry();
+ registry.register(new Scheme("http", new PlainSocketFactory(), 80));
+
+ HttpParams httpparams = new BasicHttpParams();
+ HttpConnectionParams.setConnectionTimeout(httpparams, 5000);
+ HttpConnectionParams.setSoTimeout(httpparams, 5000);
+ DefaultHttpClient httpclient = new DefaultHttpClient(new ThreadSafeClientConnManager(httpparams, registry),
+ httpparams);
+
+ httpclient.addRequestInterceptor(HttpHelper.gzipRequestInterceptor);
+ httpclient.addResponseInterceptor(HttpHelper.gzipResponseInterceptor);
+
+ return httpclient;
+
+ }
+
+ /**
+ * By default creates a standard Item (with title, description and links), which may to overridden to add more data
+ * (i.e. custom tags that a feed may supply).
+ * @return A possibly decorated Item instance
+ */
+ protected Item createNewItem() {
+ return new Item();
+ }
+
+ @Override
+ public final void startElement(String uri, String localName, String qName, Attributes attributes) {
+
+ /** First lets check for the channel */
+ if (localName.equalsIgnoreCase("channel")) {
+ this.channel = new Channel();
+ }
+
+ /** Now lets check for an item */
+ if (localName.equalsIgnoreCase("item") && (this.channel != null)) {
+ this.item = createNewItem();
+ this.channel.addItem(this.item);
+ }
+
+ /** Now lets check for an image */
+ if (localName.equalsIgnoreCase("image") && (this.channel != null)) {
+ this.imageStatus = true;
+ }
+
+ /** Checking for a enclosure */
+ if (localName.equalsIgnoreCase("enclosure")) {
+ /** Lets check we are in an item */
+ if (this.item != null && attributes != null && attributes.getLength() > 0) {
+ if (attributes.getValue("url") != null) {
+ this.item.setEnclosureUrl(attributes.getValue("url").trim());
+ }
+ if (attributes.getValue("type") != null) {
+ this.item.setEnclosureType(attributes.getValue("type"));
+ }
+ if (attributes.getValue("length") != null) {
+ this.item.setEnclosureLength(Long.parseLong(attributes.getValue("length")));
+ }
+ }
+ }
+
+ }
+
+ /**
+ * This is where we actually parse for the elements contents
+ */
+ @SuppressWarnings("deprecation")
+ public final void endElement(String uri, String localName, String qName) {
+ /** Check we have an RSS Feed */
+ if (this.channel == null) {
+ return;
+ }
+
+ /** Check are at the end of an item */
+ if (localName.equalsIgnoreCase("item")) {
+ this.item = null;
+ }
+
+ /** Check we are at the end of an image */
+ if (localName.equalsIgnoreCase("image"))
+ this.imageStatus = false;
+
+ /** Now we need to parse which title we are in */
+ if (localName.equalsIgnoreCase("title")) {
+ /** We are an item, so we set the item title */
+ if (this.item != null) {
+ this.item.setTitle(this.text.toString().trim());
+ /** We are in an image */
+ } else {
+ this.channel.setTitle(this.text.toString().trim());
+ }
+ }
+
+ /** Now we are checking for a link */
+ if (localName.equalsIgnoreCase("link")) {
+ /** Check we are in an item **/
+ if (this.item != null) {
+ this.item.setLink(this.text.toString().trim());
+ /** Check we are in an image */
+ } else if (this.imageStatus) {
+ this.channel.setImage(this.text.toString().trim());
+ /** Check we are in a channel */
+ } else {
+ this.channel.setLink(this.text.toString().trim());
+ }
+ }
+
+ /** Checking for a description */
+ if (localName.equalsIgnoreCase("description")) {
+ /** Lets check we are in an item */
+ if (this.item != null) {
+ this.item.setDescription(this.text.toString().trim());
+ /** Lets check we are in the channel */
+ } else {
+ this.channel.setDescription(this.text.toString().trim());
+ }
+ }
+
+ /** Checking for a pubdate */
+ if (localName.equalsIgnoreCase("pubDate")) {
+ /** Lets check we are in an item */
+ if (this.item != null) {
+ try {
+ this.item.setPubdate(new Date(Date.parse(this.text.toString().trim())));
+ } catch (Exception e) {
+ // Date is malformed (not parsable by Date.parse)
+ }
+ /** Lets check we are in the channel */
+ } else {
+ try {
+ this.channel.setPubDate(new Date(Date.parse(this.text.toString().trim())));
+ } catch (Exception e) {
+ // Date is malformed (not parsable by Date.parse)
+ }
+ }
+ }
+
+ /** Check for the category */
+ if (localName.equalsIgnoreCase("category") && (this.item != null)) {
+ this.channel.addCategory(this.text.toString().trim());
+ }
+
+ addAdditionalData(localName, this.item, this.text.toString());
+
+ this.text.setLength(0);
+ }
+
+ /**
+ * May be overridden to add additional data from tags that are not standard in RSS. Not used by this default RSS
+ * style parser. Usually used in conjunction with {@link #createNewItem()}.
+ * @param localName The tag name
+ * @param item The Item we are currently parsing
+ * @param text The new text content
+ */
+ protected void addAdditionalData(String localName, Item item, String text) {
+ }
+
+ @Override
+ public final void characters(char[] ch, int start, int length) {
+ this.text.append(ch, start, length);
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/org/transdroid/core/service/AlarmReceiver.java b/core/src/org/transdroid/core/service/AlarmReceiver.java
new file mode 100644
index 00000000..384e9e58
--- /dev/null
+++ b/core/src/org/transdroid/core/service/AlarmReceiver.java
@@ -0,0 +1,34 @@
+package org.transdroid.core.service;
+
+import org.androidannotations.annotations.EReceiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Acts simply as an intermediary to start the appropriate background service when an alarm goes off.
+ * @author Eric Kok
+ */
+@EReceiver
+public class AlarmReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getIntExtra("service", -1)) {
+ case BootReceiver.ALARM_SERVERCHECKER:
+ context.startService(new Intent(context, ServerCheckerService_.class));
+ break;
+ case BootReceiver.ALARM_RSSCHECKER:
+ context.startService(new Intent(context, RssCheckerService_.class));
+ break;
+ case BootReceiver.ALARM_APPUPDATES:
+ context.startService(new Intent(context, AppUpdateService_.class));
+ break;
+ default:
+ // No valid service start ID
+ break;
+ }
+ }
+
+}
diff --git a/core/src/org/transdroid/core/service/AppUpdateService.java b/core/src/org/transdroid/core/service/AppUpdateService.java
new file mode 100644
index 00000000..f99fdfd4
--- /dev/null
+++ b/core/src/org/transdroid/core/service/AppUpdateService.java
@@ -0,0 +1,136 @@
+package org.transdroid.core.service;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EService;
+import org.androidannotations.annotations.SystemService;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.AbstractHttpClient;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.NotificationSettings;
+import org.transdroid.core.app.settings.SystemSettings;
+import org.transdroid.core.gui.log.Log;
+import org.transdroid.daemon.util.HttpHelper;
+
+import android.app.IntentService;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationCompat.Builder;
+
+@EService
+public class AppUpdateService extends IntentService {
+
+ private static final String LATEST_URL_APP = "http://www.transdroid.org/update/latest-app.php";
+ private static final String LATEST_URL_SEARCH = "http://www.transdroid.org/update/latest-search.php";
+ private static final String DOWNLOAD_URL_APP = "http://www.transdroid.org/latest";
+ private static final String DOWNLOAD_URL_SEARCH = "http://www.transdroid.org/latest-search";
+
+ @Bean
+ protected ConnectivityHelper connectivityHelper;
+ @Bean
+ protected SystemSettings systemSettings;
+ @Bean
+ protected NotificationSettings notificationSettings;
+ @SystemService
+ protected NotificationManager notificationManager;
+
+ public AppUpdateService() {
+ super("AppUpdateService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+
+ if (!connectivityHelper.shouldPerformActions() || !systemSettings.checkForUpdates()) {
+ Log.d(this,
+ "Skip the app update service, as background data is disabled, the service is disabled or we are not connected.");
+ return;
+ }
+
+ DefaultHttpClient httpclient = new DefaultHttpClient();
+ Random random = new Random();
+
+ try {
+
+ // Retrieve what is the latest released app and search module versions
+ String[] app = retrieveLatestVersion(httpclient, LATEST_URL_APP);
+ String[] search = retrieveLatestVersion(httpclient, LATEST_URL_SEARCH);
+ int appVersion = Integer.parseInt(app[0].trim());
+ int searchVersion = Integer.parseInt(search[0].trim());
+
+ // New version of the app?
+ try {
+ PackageInfo appPackage = getPackageManager().getPackageInfo(getPackageName(), 0);
+ if (appPackage.versionCode < appVersion) {
+ // New version available! Notify the user.
+ newNotification(getString(R.string.update_app_newversion),
+ getString(R.string.update_app_newversion),
+ getString(R.string.update_updateto, app[1].trim()),
+ DOWNLOAD_URL_APP + "?" + Integer.toString(random.nextInt()), 90000);
+ }
+ } catch (NameNotFoundException e) {
+ // Not installed... this can never happen since this Service is part of the app itself
+ }
+
+ // New version of the search module?
+ try {
+ PackageInfo searchPackage = getPackageManager().getPackageInfo("org.transdroid.search", 0);
+ if (searchPackage.versionCode < searchVersion) {
+ // New version available! Notify the user.
+ newNotification(getString(R.string.update_search_newversion),
+ getString(R.string.update_search_newversion),
+ getString(R.string.update_updateto, search[1].trim()),
+ DOWNLOAD_URL_SEARCH + "?" + Integer.toString(random.nextInt()), 90001);
+ }
+ } catch (NameNotFoundException e) {
+ // The search module isn't installed yet at all; ignore and wait for the user to manually
+ // install it (when the first search is initiated)
+ }
+
+ } catch (Exception e) {
+ // Cannot check right now for some reason; log and ignore
+ Log.d(this, "Cannot retrieve latest app or search module version code from the site: " + e.toString());
+ }
+
+ }
+
+ /**
+ * Retrieves the latest version number of the app or search module by checking an online text file that looks like
+ * '160|1.1.15' for version code 160 and version name 1.1.15.
+ * @param httpclient An already instantiated HTTP client
+ * @param url The URL of the the text file that contains the current latest version code and name
+ * @return A string array with two elements: the version code and the version number
+ * @throws ClientProtocolException Thrown when the provided URL is invalid
+ * @throws IOException Thrown when the last version information could not be retrieved
+ */
+ private String[] retrieveLatestVersion(AbstractHttpClient httpclient, String url) throws ClientProtocolException,
+ IOException {
+ HttpResponse request = httpclient.execute(new HttpGet(url));
+ InputStream stream = request.getEntity().getContent();
+ String appVersion[] = HttpHelper.convertStreamToString(stream).split("\\|");
+ stream.close();
+ return appVersion;
+ }
+
+ private void newNotification(String ticker, String title, String text, String downloadUrl, int notifyID) {
+ PendingIntent pi = PendingIntent.getActivity(this, notifyID,
+ new Intent(Intent.ACTION_VIEW, Uri.parse(downloadUrl)), Intent.FLAG_ACTIVITY_NEW_TASK);
+ Builder builder = new NotificationCompat.Builder(this).setSmallIcon(R.drawable.ic_stat_notification)
+ .setTicker(ticker).setContentTitle(title).setContentText(text)
+ .setLights(notificationSettings.getDesiredLedColour(), 600, 1000)
+ .setSound(notificationSettings.getSound()).setAutoCancel(true).setContentIntent(pi);
+ notificationManager.notify(notifyID, builder.build());
+ }
+
+}
diff --git a/core/src/org/transdroid/core/service/BootReceiver.java b/core/src/org/transdroid/core/service/BootReceiver.java
new file mode 100644
index 00000000..ceab951f
--- /dev/null
+++ b/core/src/org/transdroid/core/service/BootReceiver.java
@@ -0,0 +1,88 @@
+package org.transdroid.core.service;
+
+import org.transdroid.core.app.settings.NotificationSettings;
+import org.transdroid.core.app.settings.NotificationSettings_;
+import org.transdroid.core.app.settings.SystemSettings;
+import org.transdroid.core.app.settings.SystemSettings_;
+import org.transdroid.core.gui.log.Log;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+
+/**
+ * Receives the intent that the device has been started in order to set up proper alarms for all background services.
+ * @author Eric Kok
+ */
+public class BootReceiver extends BroadcastReceiver {
+
+ public static final int ALARM_SERVERCHECKER = 0;
+ public static final int ALARM_RSSCHECKER = 1;
+ public static final int ALARM_APPUPDATES = 2;
+
+ public static PendingIntent piServerChecker = null, piRssChecker = null, piAppUpdates = null;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ startBackgroundServices(context, false);
+ startAppUpdatesService(context);
+ }
+
+ public static void startBackgroundServices(Context context, boolean forceReload) {
+ NotificationSettings notificationSettings = NotificationSettings_.getInstance_(context);
+ AlarmManager alarms = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ if (notificationSettings.isEnabled() && (forceReload || (piServerChecker == null && piRssChecker == null))) {
+
+ Log.d(context, "Boot signal received, starting server and rss checker background services");
+ // Schedule repeating alarms, with the first being (somewhat) in 1 second from now
+ piServerChecker = PendingIntent.getBroadcast(context, ALARM_SERVERCHECKER, new Intent(context,
+ AlarmReceiver_.class).putExtra("service", ALARM_SERVERCHECKER), 0);
+ piRssChecker = PendingIntent.getBroadcast(context, ALARM_RSSCHECKER, new Intent(context,
+ AlarmReceiver_.class).putExtra("service", ALARM_RSSCHECKER), 0);
+ alarms.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000,
+ notificationSettings.getInvervalInMilliseconds(), piServerChecker);
+ alarms.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000,
+ notificationSettings.getInvervalInMilliseconds(), piRssChecker);
+
+ }
+ }
+
+ public static void startAppUpdatesService(Context context) {
+ SystemSettings systemSettings = SystemSettings_.getInstance_(context);
+ AlarmManager alarms = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ if (systemSettings.checkForUpdates() && piAppUpdates == null) {
+
+ Log.d(context, "Boot signal received, starting app update checker service");
+ // Schedule a daily, with the first being (somewhat) in 1 second from now
+ piAppUpdates = PendingIntent.getBroadcast(context, ALARM_APPUPDATES, new Intent(context,
+ AlarmReceiver_.class).putExtra("service", ALARM_APPUPDATES), 0);
+ alarms.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000,
+ AlarmManager.INTERVAL_DAY, piAppUpdates);
+
+ }
+ }
+
+ public static void cancelBackgroundServices(Context context) {
+ AlarmManager alarms = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ if (piServerChecker != null) {
+ alarms.cancel(piServerChecker);
+ piServerChecker = null;
+ }
+ if (piRssChecker != null) {
+ alarms.cancel(piRssChecker);
+ piRssChecker = null;
+ }
+ }
+
+ public static void cancelAppUpdates(Context context) {
+ if (piAppUpdates != null) {
+ AlarmManager alarms = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ alarms.cancel(piAppUpdates);
+ piAppUpdates = null;
+ }
+ }
+
+}
diff --git a/core/src/org/transdroid/core/service/ConnectivityHelper.java b/core/src/org/transdroid/core/service/ConnectivityHelper.java
new file mode 100644
index 00000000..fe043320
--- /dev/null
+++ b/core/src/org/transdroid/core/service/ConnectivityHelper.java
@@ -0,0 +1,29 @@
+package org.transdroid.core.service;
+
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.SystemService;
+import org.androidannotations.annotations.EBean.Scope;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+
+@EBean(scope = Scope.Singleton)
+public class ConnectivityHelper {
+
+ @SystemService
+ protected ConnectivityManager connectivityManager;
+
+ public ConnectivityHelper(Context context) {
+ }
+
+ @SuppressWarnings("deprecation")
+ public boolean shouldPerformActions() {
+ // First check the old background data setting (this will always be true for ICS+)
+ if (!connectivityManager.getBackgroundDataSetting())
+ return false;
+
+ // Still good? Check the current active network instead
+ return connectivityManager.getActiveNetworkInfo().isConnected();
+ }
+
+}
diff --git a/core/src/org/transdroid/core/service/RssCheckerService.java b/core/src/org/transdroid/core/service/RssCheckerService.java
new file mode 100644
index 00000000..307b8807
--- /dev/null
+++ b/core/src/org/transdroid/core/service/RssCheckerService.java
@@ -0,0 +1,107 @@
+package org.transdroid.core.service;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EService;
+import org.androidannotations.annotations.SystemService;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.ApplicationSettings;
+import org.transdroid.core.app.settings.NotificationSettings;
+import org.transdroid.core.app.settings.RssfeedSetting;
+import org.transdroid.core.gui.log.Log;
+import org.transdroid.core.gui.rss.RssfeedsActivity_;
+import org.transdroid.core.rssparser.Item;
+import org.transdroid.core.rssparser.RssParser;
+import org.transdroid.daemon.util.Collections2;
+
+import android.app.IntentService;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationCompat.Builder;
+
+/**
+ * A background service that checks all user-configured RSS feeds for new items.
+ * @author Eric Kok
+ */
+@EService
+public class RssCheckerService extends IntentService {
+
+ @Bean
+ protected ConnectivityHelper connectivityHelper;
+ @Bean
+ protected NotificationSettings notificationSettings;
+ @Bean
+ protected ApplicationSettings applicationSettings;
+ @SystemService
+ protected NotificationManager notificationManager;
+
+ public RssCheckerService() {
+ super("RssCheckerService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+
+ if (!connectivityHelper.shouldPerformActions() || !notificationSettings.isEnabled()) {
+ Log.d(this,
+ "Skip the RSS checker service, as background data is disabled, the service is disabled or we are not connected.");
+ return;
+ }
+
+ // Check every RSS feed for new items
+ int unread = 0;
+ Set
hasUnread = new LinkedHashSet();
+ for (RssfeedSetting feed : applicationSettings.getRssfeedSettings()) {
+ try {
+
+ Log.d(this, "Try to parse " + feed.getName() + " (" + feed.getUrl() + ")");
+ RssParser parser = new RssParser(feed.getUrl());
+ parser.parse();
+ if (parser.getChannel() == null)
+ continue;
+
+ // Find the last item that is newer than the last viewed date
+ for (Item item : parser.getChannel().getItems()) {
+ if (item.getPubdate() != null && item.getPubdate().before(feed.getLastViewed())) {
+ break;
+ } else {
+ unread++;
+ if (!hasUnread.contains(feed.getName()))
+ hasUnread.add(feed.getName());
+ }
+ }
+
+ Log.d(this, feed.getName() + " has " + (hasUnread.contains(feed.getName()) ? "" : "no ")
+ + "unread items");
+
+ } catch (Exception e) {
+ // Ignore RSS feeds that could not be retrieved or parsed
+ }
+ }
+
+ if (unread == 0) {
+ // No new items; just exit
+ return;
+ }
+
+ // Provide a notification, since there are new RSS items
+ PendingIntent pi = PendingIntent.getActivity(this, 80000, new Intent(this, RssfeedsActivity_.class),
+ Intent.FLAG_ACTIVITY_NEW_TASK);
+ String title = getResources().getQuantityString(R.plurals.rss_service_new, unread, Integer.toString(unread));
+ String forString = Collections2.joinString(hasUnread, ", ");
+ Builder builder = new NotificationCompat.Builder(this).setSmallIcon(R.drawable.ic_stat_notification)
+ .setTicker(title).setContentTitle(title)
+ .setContentText(getString(R.string.rss_service_newfor, forString)).setNumber(unread)
+ .setLights(notificationSettings.getDesiredLedColour(), 600, 1000)
+ .setSound(notificationSettings.getSound()).setAutoCancel(true).setContentIntent(pi);
+ if (notificationSettings.shouldVibrate())
+ builder.setVibrate(notificationSettings.getDefaultVibratePattern());
+ notificationManager.notify(80001, builder.build());
+
+ }
+
+}
diff --git a/core/src/org/transdroid/core/service/ServerCheckerService.java b/core/src/org/transdroid/core/service/ServerCheckerService.java
new file mode 100644
index 00000000..b237a82f
--- /dev/null
+++ b/core/src/org/transdroid/core/service/ServerCheckerService.java
@@ -0,0 +1,183 @@
+package org.transdroid.core.service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.androidannotations.annotations.Bean;
+import org.androidannotations.annotations.EService;
+import org.androidannotations.annotations.SystemService;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.transdroid.core.R;
+import org.transdroid.core.app.settings.ApplicationSettings;
+import org.transdroid.core.app.settings.NotificationSettings;
+import org.transdroid.core.app.settings.ServerSetting;
+import org.transdroid.core.gui.TorrentsActivity_;
+import org.transdroid.core.gui.log.Log;
+import org.transdroid.daemon.IDaemonAdapter;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.task.DaemonTaskResult;
+import org.transdroid.daemon.task.RetrieveTask;
+import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
+import org.transdroid.daemon.util.Collections2;
+
+import android.app.IntentService;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationCompat.Builder;
+import android.support.v4.app.NotificationCompat.InboxStyle;
+
+/**
+ * A background service that checks all user-configured servers (if so desired) for new and finished torrents.
+ * @author Eric Kok
+ */
+@EService
+public class ServerCheckerService extends IntentService {
+
+ @Bean
+ protected ConnectivityHelper connectivityHelper;
+ @Bean
+ protected NotificationSettings notificationSettings;
+ @Bean
+ protected ApplicationSettings applicationSettings;
+ @SystemService
+ protected NotificationManager notificationManager;
+
+ public ServerCheckerService() {
+ super("ServerCheckerService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+
+ if (!connectivityHelper.shouldPerformActions() || !notificationSettings.isEnabled()) {
+ Log.d(this,
+ "Skip the server checker service, as background data is disabled, the service is disabled or we are not connected.");
+ return;
+ }
+
+ int notifyBase = 10000;
+ for (ServerSetting server : applicationSettings.getServerSettings()) {
+
+ // No need to check if the server is not properly configured or none of the two types of notifications are
+ // enabled by the user for this specific server
+ if (server.getType() == null || server.getAddress() == null || server.getAddress().equals("")
+ || !(server.shouldAlarmOnFinishedDownload() || server.shouldAlarmOnNewTorrent()))
+ return;
+
+ // Get the statistics for the last time we checked this server
+ JSONArray lastStats = applicationSettings.getServerLastStats(server);
+
+ // Synchronously retrieve torrents listing
+ IDaemonAdapter adapter = server.createServerAdapter();
+ DaemonTaskResult result = RetrieveTask.create(adapter).execute();
+ if (!(result instanceof RetrieveTaskSuccessResult)) {
+ // Cannot retrieve torrents at this time
+ return;
+ }
+ List retrieved = ((RetrieveTaskSuccessResult) result).getTorrents();
+ Log.d(this, server.getName() + ": Retrieved torrent listing");
+
+ // Check for differences between the last and the current stats
+ JSONArray currentStats = new JSONArray();
+ List newTorrents = new ArrayList();
+ List doneTorrents = new ArrayList();
+ for (Torrent torrent : retrieved) {
+
+ // Remember this torrent for the next time
+ try {
+ currentStats.put(new JSONObject().put("id", torrent.getUniqueID()).put("done",
+ torrent.getPartDone() == 1F));
+ } catch (JSONException e) {
+ // Can't build the JSON object; this should not happen and we can safely ignore it
+ }
+
+ // See if this torrent was done the last time we checked
+ if (lastStats != null) {
+ Boolean wasDone = findLastDoneStat(lastStats, torrent);
+ if (server.shouldAlarmOnNewTorrent() && wasDone == null) {
+ // This torrent wasn't present earlier
+ newTorrents.add(torrent);
+ continue;
+ }
+ if (server.shouldAlarmOnFinishedDownload() && torrent.getPartDone() == 1F && wasDone != null && !wasDone)
+ // This torrent is now done, but wasn't before
+ doneTorrents.add(torrent);
+ }
+
+ }
+
+ // Store the now-current statistics on torrents for the next time we check this server
+ applicationSettings.setServerLastStats(server, currentStats);
+
+ // Notify on new and now-done torrents for this server
+ Log.d(this, server.getName() + ": " + newTorrents.size() + " new torrents, " + doneTorrents.size()
+ + " newly finished torrents.");
+ Intent i = new Intent(this, TorrentsActivity_.class);
+ i.putExtra("org.transdroid.START_SERVER", server.getOrder());
+ // Should start the main activity directly into this server
+ PendingIntent pi = PendingIntent.getActivity(this, notifyBase + server.getOrder(), i,
+ Intent.FLAG_ACTIVITY_NEW_TASK);
+ ArrayList affectedTorrents = new ArrayList(newTorrents.size() + doneTorrents.size());
+ affectedTorrents.addAll(newTorrents);
+ affectedTorrents.addAll(doneTorrents);
+ String title, forString = Collections2.joinString(affectedTorrents, ", ");
+ if (newTorrents.size() > 0 && doneTorrents.size() > 0) {
+ // Note: use the 'one' plural iif 1 new torrent was added and 1 was newly finished
+ title = getResources().getQuantityString(R.plurals.status_service_finished,
+ newTorrents.size() + doneTorrents.size() == 2 ? 1 : 2, Integer.toString(newTorrents.size()),
+ Integer.toString(doneTorrents.size()));
+ } else if (newTorrents.size() > 0) {
+ title = getResources().getQuantityString(R.plurals.status_service_added, newTorrents.size(),
+ Integer.toString(newTorrents.size()));
+ } else if (doneTorrents.size() > 0) {
+ title = getResources().getQuantityString(R.plurals.status_service_finished, doneTorrents.size(),
+ Integer.toString(doneTorrents.size()));
+ } else {
+ // No notification to show
+ continue;
+ }
+
+ // Build the basic notification
+ Builder builder = new NotificationCompat.Builder(this).setSmallIcon(R.drawable.ic_stat_notification)
+ .setTicker(title).setContentTitle(title).setContentText(forString)
+ .setNumber(affectedTorrents.size())
+ .setLights(notificationSettings.getDesiredLedColour(), 600, 1000)
+ .setSound(notificationSettings.getSound()).setAutoCancel(true).setContentIntent(pi);
+ if (notificationSettings.shouldVibrate())
+ builder.setVibrate(notificationSettings.getDefaultVibratePattern());
+
+ // Add at most 5 lines with the affected torrents
+ InboxStyle inbox = new NotificationCompat.InboxStyle(builder);
+ if (affectedTorrents.size() < 6) {
+ for (Torrent affectedTorrent : affectedTorrents) {
+ inbox.addLine(affectedTorrent.getName());
+ }
+ } else {
+ for (int j = 0; j < 4; j++) {
+ inbox.addLine(affectedTorrents.get(j).getName());
+ }
+ inbox.addLine(getString(R.string.status_service_andothers, affectedTorrents.get(5).getName()));
+ }
+ notificationManager.notify(notifyBase + server.getOrder(), inbox.build());
+
+ }
+
+ }
+
+ private Boolean findLastDoneStat(JSONArray lastStats, Torrent torrent) {
+ for (int i = 0; i < lastStats.length(); i++) {
+ try {
+ if (lastStats.getJSONObject(i).getString("id").equals(torrent.getUniqueID()))
+ return lastStats.getJSONObject(i).getBoolean("done");
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/external/ColorPickerPreference/.classpath b/external/ColorPickerPreference/.classpath
new file mode 100644
index 00000000..7bc01d9a
--- /dev/null
+++ b/external/ColorPickerPreference/.classpath
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/external/ColorPickerPreference/.gitattributes b/external/ColorPickerPreference/.gitattributes
new file mode 100644
index 00000000..dfe07704
--- /dev/null
+++ b/external/ColorPickerPreference/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/external/ColorPickerPreference/.gitignore b/external/ColorPickerPreference/.gitignore
new file mode 100644
index 00000000..4b1a60ae
--- /dev/null
+++ b/external/ColorPickerPreference/.gitignore
@@ -0,0 +1,4 @@
+/bin
+/gen
+.classpath
+.project
\ No newline at end of file
diff --git a/external/ColorPickerPreference/.project b/external/ColorPickerPreference/.project
new file mode 100644
index 00000000..b3e7744d
--- /dev/null
+++ b/external/ColorPickerPreference/.project
@@ -0,0 +1,33 @@
+
+
+ ColorPickerPreference
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/external/ColorPickerPreference/AndroidManifest.xml b/external/ColorPickerPreference/AndroidManifest.xml
new file mode 100644
index 00000000..e09215a9
--- /dev/null
+++ b/external/ColorPickerPreference/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/external/ColorPickerPreference/CHANGELOG.rst b/external/ColorPickerPreference/CHANGELOG.rst
new file mode 100644
index 00000000..5342d2ac
--- /dev/null
+++ b/external/ColorPickerPreference/CHANGELOG.rst
@@ -0,0 +1,26 @@
+================================
+ColorPickerPreference Change Log
+================================
+
+2011-02-11 v1.11:
+----------------
+fix: color controls not visible in landscape orientation
+fix: colorPickerDialog constructor was protected
+
+2011-01-25 v1.1:
+----------------
+* new: Alpha Slider is disabled by default
+* new: Alpha Slider can be enabled:
+ * with preference XML using attribute alphaSlider="true"
+ * with function setAlphaSliderEnabled(true)
+* new: defaultValue in preference XML now accepts HEX color code:
+ * #FF00FF, rgb
+ * #FF00FF00, argb
+
+2011-01-20 v1.01:
+-----------------
+fix: sometimes preview color disappear
+
+2011-01-19 v1.0:
+----------------
+release
\ No newline at end of file
diff --git a/external/ColorPickerPreference/LICENSE b/external/ColorPickerPreference/LICENSE
new file mode 100644
index 00000000..da9cd5cb
--- /dev/null
+++ b/external/ColorPickerPreference/LICENSE
@@ -0,0 +1,15 @@
+/*
+ * Copyright (C) 2011 Sergey Margaritov & Daniel Nilsson
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
\ No newline at end of file
diff --git a/external/ColorPickerPreference/README.rst b/external/ColorPickerPreference/README.rst
new file mode 100644
index 00000000..bffa417a
--- /dev/null
+++ b/external/ColorPickerPreference/README.rst
@@ -0,0 +1,47 @@
+=====================
+ColorPickerPreference
+=====================
+
+Generally used classes by Daniel Nilsson.
+ColorPickerPreference class by Sergey Margaritov.
+Packed by Sergey Margaritov.
+
+Features
+========
+
+* Color Area
+* Hue Slider
+* Alpha Slider (disabled by default)
+* Old & New Color
+* Color Preview in Preferences List
+
+Requirements
+============
+
+Tested with APIv7, but maybe will work with early versions
+
+Usage
+=====
+
+You can see some tests inside
+
+::
+
+
+ alphaSlider="true"
+ />
+
+To enable Alpha Slider in your code use function:
+::
+ setAlphaSliderEnabled(boolean enable)
+
+Screens
+=======
+
+* .. image:: https://github.com/attenzione/android-ColorPickerPreference/raw/master/screen_1.png
+
+* .. image:: https://github.com/attenzione/android-ColorPickerPreference/raw/master/screen_2.png
\ No newline at end of file
diff --git a/external/ColorPickerPreference/ant.properties b/external/ColorPickerPreference/ant.properties
new file mode 100644
index 00000000..b0971e89
--- /dev/null
+++ b/external/ColorPickerPreference/ant.properties
@@ -0,0 +1,17 @@
+# This file is used to override default values used by the Ant build system.
+#
+# This file must be checked into Version Control Systems, as it is
+# integral to the build system of your project.
+
+# This file is only used by the Ant script.
+
+# You can use this to override default values such as
+# 'source.dir' for the location of your java source folder and
+# 'out.dir' for the location of your output folder.
+
+# You can also use it define how the release builds are signed by declaring
+# the following properties:
+# 'key.store' for the location of your keystore and
+# 'key.alias' for the name of the key to use.
+# The password will be asked during the build when you use the 'release' target.
+
diff --git a/external/ColorPickerPreference/build.xml b/external/ColorPickerPreference/build.xml
new file mode 100644
index 00000000..c575c550
--- /dev/null
+++ b/external/ColorPickerPreference/build.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/external/ColorPickerPreference/local.properties b/external/ColorPickerPreference/local.properties
new file mode 100644
index 00000000..47f704fe
--- /dev/null
+++ b/external/ColorPickerPreference/local.properties
@@ -0,0 +1,10 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+
+# location of the SDK. This is only used by Ant
+# For customization when using a Version Control System, please read the
+# header note.
+sdk.dir=/home/eric/Dev/android-sdk
diff --git a/external/ColorPickerPreference/proguard-project.txt b/external/ColorPickerPreference/proguard-project.txt
new file mode 100644
index 00000000..f2fe1559
--- /dev/null
+++ b/external/ColorPickerPreference/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/external/ColorPickerPreference/proguard.cfg b/external/ColorPickerPreference/proguard.cfg
new file mode 100644
index 00000000..8ad7d335
--- /dev/null
+++ b/external/ColorPickerPreference/proguard.cfg
@@ -0,0 +1,34 @@
+-optimizationpasses 5
+-dontusemixedcaseclassnames
+-dontskipnonpubliclibraryclasses
+-dontpreverify
+-verbose
+-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
+
+-keep public class * extends android.app.Activity
+-keep public class * extends android.app.Application
+-keep public class * extends android.app.Service
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class com.android.vending.licensing.ILicensingService
+
+-keepclasseswithmembernames class * {
+ native ;
+}
+
+-keepclasseswithmembernames class * {
+ public (android.content.Context, android.util.AttributeSet);
+}
+
+-keepclasseswithmembernames class * {
+ public (android.content.Context, android.util.AttributeSet, int);
+}
+
+-keepclassmembers enum * {
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+-keep class * implements android.os.Parcelable {
+ public static final android.os.Parcelable$Creator *;
+}
diff --git a/external/ColorPickerPreference/project.properties b/external/ColorPickerPreference/project.properties
new file mode 100644
index 00000000..6905bdf0
--- /dev/null
+++ b/external/ColorPickerPreference/project.properties
@@ -0,0 +1,12 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+
+android.library=true
+# Project target.
+target=android-18
diff --git a/external/ColorPickerPreference/res/drawable-hdpi/icon.png b/external/ColorPickerPreference/res/drawable-hdpi/icon.png
new file mode 100644
index 00000000..8074c4c5
Binary files /dev/null and b/external/ColorPickerPreference/res/drawable-hdpi/icon.png differ
diff --git a/external/ColorPickerPreference/res/drawable-ldpi/icon.png b/external/ColorPickerPreference/res/drawable-ldpi/icon.png
new file mode 100644
index 00000000..1095584e
Binary files /dev/null and b/external/ColorPickerPreference/res/drawable-ldpi/icon.png differ
diff --git a/external/ColorPickerPreference/res/drawable-mdpi/icon.png b/external/ColorPickerPreference/res/drawable-mdpi/icon.png
new file mode 100644
index 00000000..a07c69fa
Binary files /dev/null and b/external/ColorPickerPreference/res/drawable-mdpi/icon.png differ
diff --git a/external/ColorPickerPreference/res/layout-land/dialog_color_picker.xml b/external/ColorPickerPreference/res/layout-land/dialog_color_picker.xml
new file mode 100644
index 00000000..cf567016
--- /dev/null
+++ b/external/ColorPickerPreference/res/layout-land/dialog_color_picker.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/external/ColorPickerPreference/res/layout/dialog_color_picker.xml b/external/ColorPickerPreference/res/layout/dialog_color_picker.xml
new file mode 100644
index 00000000..00ee3753
--- /dev/null
+++ b/external/ColorPickerPreference/res/layout/dialog_color_picker.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/external/ColorPickerPreference/res/values/integer.xml b/external/ColorPickerPreference/res/values/integer.xml
new file mode 100644
index 00000000..e3626064
--- /dev/null
+++ b/external/ColorPickerPreference/res/values/integer.xml
@@ -0,0 +1,5 @@
+
+
+ 0xff000000
+ 0xff00ff00
+
\ No newline at end of file
diff --git a/external/ColorPickerPreference/res/values/strings.xml b/external/ColorPickerPreference/res/values/strings.xml
new file mode 100644
index 00000000..251a66f4
--- /dev/null
+++ b/external/ColorPickerPreference/res/values/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ Hello World, Main!
+ ColorPickerPreference
+
+
+ Color Picker
+ Press on Color to apply
+
+
+ Category
+ Color 1
+ black color by default, set by reference
+ Color 2
+ not persistent color\nalpha slider added via code
+ Color 3
+ picker with alpha slider
+ Color 4
+ color set with HEX code in xml
+
diff --git a/external/ColorPickerPreference/res/xml/settings.xml b/external/ColorPickerPreference/res/xml/settings.xml
new file mode 100644
index 00000000..0cd87af9
--- /dev/null
+++ b/external/ColorPickerPreference/res/xml/settings.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/external/ColorPickerPreference/screen_1.png b/external/ColorPickerPreference/screen_1.png
new file mode 100644
index 00000000..4798dae7
Binary files /dev/null and b/external/ColorPickerPreference/screen_1.png differ
diff --git a/external/ColorPickerPreference/screen_2.png b/external/ColorPickerPreference/screen_2.png
new file mode 100644
index 00000000..3bb2d671
Binary files /dev/null and b/external/ColorPickerPreference/screen_2.png differ
diff --git a/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/AlphaPatternDrawable.java b/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/AlphaPatternDrawable.java
new file mode 100644
index 00000000..ff9c3c84
--- /dev/null
+++ b/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/AlphaPatternDrawable.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2010 Daniel Nilsson
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.margaritov.preference.colorpicker;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+/**
+ * This drawable that draws a simple white and gray chessboard pattern.
+ * It's pattern you will often see as a background behind a
+ * partly transparent image in many applications.
+ * @author Daniel Nilsson
+ */
+public class AlphaPatternDrawable extends Drawable {
+
+ private int mRectangleSize = 10;
+
+ private Paint mPaint = new Paint();
+ private Paint mPaintWhite = new Paint();
+ private Paint mPaintGray = new Paint();
+
+ private int numRectanglesHorizontal;
+ private int numRectanglesVertical;
+
+ /**
+ * Bitmap in which the pattern will be cahched.
+ */
+ private Bitmap mBitmap;
+
+ public AlphaPatternDrawable(int rectangleSize) {
+ mRectangleSize = rectangleSize;
+ mPaintWhite.setColor(0xffffffff);
+ mPaintGray.setColor(0xffcbcbcb);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ canvas.drawBitmap(mBitmap, null, getBounds(), mPaint);
+ }
+
+ @Override
+ public int getOpacity() {
+ return 0;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ throw new UnsupportedOperationException("Alpha is not supported by this drawwable.");
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ throw new UnsupportedOperationException("ColorFilter is not supported by this drawwable.");
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+
+ int height = bounds.height();
+ int width = bounds.width();
+
+ numRectanglesHorizontal = (int) Math.ceil((width / mRectangleSize));
+ numRectanglesVertical = (int) Math.ceil(height / mRectangleSize);
+
+ generatePatternBitmap();
+
+ }
+
+ /**
+ * This will generate a bitmap with the pattern
+ * as big as the rectangle we were allow to draw on.
+ * We do this to chache the bitmap so we don't need to
+ * recreate it each time draw() is called since it
+ * takes a few milliseconds.
+ */
+ private void generatePatternBitmap(){
+
+ if(getBounds().width() <= 0 || getBounds().height() <= 0){
+ return;
+ }
+
+ mBitmap = Bitmap.createBitmap(getBounds().width(), getBounds().height(), Config.ARGB_8888);
+ Canvas canvas = new Canvas(mBitmap);
+
+ Rect r = new Rect();
+ boolean verticalStartWhite = true;
+ for (int i = 0; i <= numRectanglesVertical; i++) {
+
+ boolean isWhite = verticalStartWhite;
+ for (int j = 0; j <= numRectanglesHorizontal; j++) {
+
+ r.top = i * mRectangleSize;
+ r.left = j * mRectangleSize;
+ r.bottom = r.top + mRectangleSize;
+ r.right = r.left + mRectangleSize;
+
+ canvas.drawRect(r, isWhite ? mPaintWhite : mPaintGray);
+
+ isWhite = !isWhite;
+ }
+
+ verticalStartWhite = !verticalStartWhite;
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/ColorPickerDialog.java b/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/ColorPickerDialog.java
new file mode 100644
index 00000000..a63d48c3
--- /dev/null
+++ b/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/ColorPickerDialog.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2010 Daniel Nilsson
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.margaritov.preference.colorpicker;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+
+public class ColorPickerDialog
+ extends
+ Dialog
+ implements
+ ColorPickerView.OnColorChangedListener,
+ View.OnClickListener {
+
+ private ColorPickerView mColorPicker;
+
+ private ColorPickerPanelView mOldColor;
+ private ColorPickerPanelView mNewColor;
+
+ private OnColorChangedListener mListener;
+
+ public interface OnColorChangedListener {
+ public void onColorChanged(int color);
+ }
+
+ public ColorPickerDialog(Context context, int initialColor) {
+ super(context);
+
+ init(initialColor);
+ }
+
+ private void init(int color) {
+ // To fight color banding.
+ getWindow().setFormat(PixelFormat.RGBA_8888);
+
+ setUp(color);
+
+ }
+
+ private void setUp(int color) {
+
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ View layout = inflater.inflate(R.layout.dialog_color_picker, null);
+
+ setContentView(layout);
+
+ setTitle(R.string.dialog_color_picker);
+
+ mColorPicker = (ColorPickerView) layout.findViewById(R.id.color_picker_view);
+ mOldColor = (ColorPickerPanelView) layout.findViewById(R.id.old_color_panel);
+ mNewColor = (ColorPickerPanelView) layout.findViewById(R.id.new_color_panel);
+
+ ((LinearLayout) mOldColor.getParent()).setPadding(
+ Math.round(mColorPicker.getDrawingOffset()),
+ 0,
+ Math.round(mColorPicker.getDrawingOffset()),
+ 0
+ );
+
+ mOldColor.setOnClickListener(this);
+ mNewColor.setOnClickListener(this);
+ mColorPicker.setOnColorChangedListener(this);
+ mOldColor.setColor(color);
+ mColorPicker.setColor(color, true);
+
+ }
+
+ @Override
+ public void onColorChanged(int color) {
+
+ mNewColor.setColor(color);
+
+ /*
+ if (mListener != null) {
+ mListener.onColorChanged(color);
+ }
+ */
+
+ }
+
+ public void setAlphaSliderVisible(boolean visible) {
+ mColorPicker.setAlphaSliderVisible(visible);
+ }
+
+ /**
+ * Set a OnColorChangedListener to get notified when the color
+ * selected by the user has changed.
+ * @param listener
+ */
+ public void setOnColorChangedListener(OnColorChangedListener listener){
+ mListener = listener;
+ }
+
+ public int getColor() {
+ return mColorPicker.getColor();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.new_color_panel) {
+ if (mListener != null) {
+ mListener.onColorChanged(mNewColor.getColor());
+ }
+ }
+ dismiss();
+ }
+
+ @Override
+ public Bundle onSaveInstanceState() {
+ Bundle state = super.onSaveInstanceState();
+ state.putInt("old_color", mOldColor.getColor());
+ state.putInt("new_color", mNewColor.getColor());
+ return state;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ mOldColor.setColor(savedInstanceState.getInt("old_color"));
+ mColorPicker.setColor(savedInstanceState.getInt("new_color"), true);
+ }
+}
diff --git a/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/ColorPickerPanelView.java b/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/ColorPickerPanelView.java
new file mode 100644
index 00000000..b0003662
--- /dev/null
+++ b/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/ColorPickerPanelView.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2010 Daniel Nilsson
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.margaritov.preference.colorpicker;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * This class draws a panel which which will be filled with a color which can be set.
+ * It can be used to show the currently selected color which you will get from
+ * the {@link ColorPickerView}.
+ * @author Daniel Nilsson
+ *
+ */
+public class ColorPickerPanelView extends View {
+
+ /**
+ * The width in pixels of the border
+ * surrounding the color panel.
+ */
+ private final static float BORDER_WIDTH_PX = 1;
+
+ private float mDensity = 1f;
+
+ private int mBorderColor = 0xff6E6E6E;
+ private int mColor = 0xff000000;
+
+ private Paint mBorderPaint;
+ private Paint mColorPaint;
+
+ private RectF mDrawingRect;
+ private RectF mColorRect;
+
+ private AlphaPatternDrawable mAlphaPattern;
+
+
+ public ColorPickerPanelView(Context context){
+ this(context, null);
+ }
+
+ public ColorPickerPanelView(Context context, AttributeSet attrs){
+ this(context, attrs, 0);
+ }
+
+ public ColorPickerPanelView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init(){
+ mBorderPaint = new Paint();
+ mColorPaint = new Paint();
+ mDensity = getContext().getResources().getDisplayMetrics().density;
+ }
+
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+
+ final RectF rect = mColorRect;
+
+ if(BORDER_WIDTH_PX > 0){
+ mBorderPaint.setColor(mBorderColor);
+ canvas.drawRect(mDrawingRect, mBorderPaint);
+ }
+
+ if(mAlphaPattern != null){
+ mAlphaPattern.draw(canvas);
+ }
+
+ mColorPaint.setColor(mColor);
+
+ canvas.drawRect(rect, mColorPaint);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ mDrawingRect = new RectF();
+ mDrawingRect.left = getPaddingLeft();
+ mDrawingRect.right = w - getPaddingRight();
+ mDrawingRect.top = getPaddingTop();
+ mDrawingRect.bottom = h - getPaddingBottom();
+
+ setUpColorRect();
+
+ }
+
+ private void setUpColorRect(){
+ final RectF dRect = mDrawingRect;
+
+ float left = dRect.left + BORDER_WIDTH_PX;
+ float top = dRect.top + BORDER_WIDTH_PX;
+ float bottom = dRect.bottom - BORDER_WIDTH_PX;
+ float right = dRect.right - BORDER_WIDTH_PX;
+
+ mColorRect = new RectF(left,top, right, bottom);
+
+ mAlphaPattern = new AlphaPatternDrawable((int)(5 * mDensity));
+
+ mAlphaPattern.setBounds(
+ Math.round(mColorRect.left),
+ Math.round(mColorRect.top),
+ Math.round(mColorRect.right),
+ Math.round(mColorRect.bottom)
+ );
+
+ }
+
+ /**
+ * Set the color that should be shown by this view.
+ * @param color
+ */
+ public void setColor(int color){
+ mColor = color;
+ invalidate();
+ }
+
+ /**
+ * Get the color currently show by this view.
+ * @return
+ */
+ public int getColor(){
+ return mColor;
+ }
+
+ /**
+ * Set the color of the border surrounding the panel.
+ * @param color
+ */
+ public void setBorderColor(int color){
+ mBorderColor = color;
+ invalidate();
+ }
+
+ /**
+ * Get the color of the border surrounding the panel.
+ */
+ public int getBorderColor(){
+ return mBorderColor;
+ }
+
+}
\ No newline at end of file
diff --git a/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/ColorPickerPreference.java b/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/ColorPickerPreference.java
new file mode 100644
index 00000000..8dded67a
--- /dev/null
+++ b/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/ColorPickerPreference.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2011 Sergey Margaritov
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.margaritov.preference.colorpicker;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.preference.Preference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+/**
+ * A preference type that allows a user to choose a time
+ * @author Sergey Margaritov
+ */
+public class ColorPickerPreference
+ extends
+ Preference
+ implements
+ Preference.OnPreferenceClickListener,
+ ColorPickerDialog.OnColorChangedListener {
+
+ View mView;
+ ColorPickerDialog mDialog;
+ private int mValue = Color.BLACK;
+ private float mDensity = 0;
+ private boolean mAlphaSliderEnabled = false;
+
+ public ColorPickerPreference(Context context) {
+ super(context);
+ init(context, null);
+ }
+
+ public ColorPickerPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs);
+ }
+
+ public ColorPickerPreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context, attrs);
+ }
+
+ @Override
+ protected Object onGetDefaultValue(TypedArray a, int index) {
+ return a.getColor(index, Color.BLACK);
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+ onColorChanged(restoreValue ? getPersistedInt(mValue) : (Integer) defaultValue);
+ }
+
+ private void init(Context context, AttributeSet attrs) {
+ mDensity = getContext().getResources().getDisplayMetrics().density;
+ setOnPreferenceClickListener(this);
+ if (attrs != null) {
+ mAlphaSliderEnabled = attrs.getAttributeBooleanValue(null, "alphaSlider", false);
+ }
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+ mView = view;
+ setPreviewColor();
+ }
+
+ private void setPreviewColor() {
+ if (mView == null) return;
+ ImageView iView = new ImageView(getContext());
+ LinearLayout widgetFrameView = ((LinearLayout)mView.findViewById(android.R.id.widget_frame));
+ if (widgetFrameView == null) return;
+ widgetFrameView.setVisibility(View.VISIBLE);
+ widgetFrameView.setPadding(
+ widgetFrameView.getPaddingLeft(),
+ widgetFrameView.getPaddingTop(),
+ (int)(mDensity * 8),
+ widgetFrameView.getPaddingBottom()
+ );
+ // remove already create preview image
+ int count = widgetFrameView.getChildCount();
+ if (count > 0) {
+ widgetFrameView.removeViews(0, count);
+ }
+ widgetFrameView.addView(iView);
+ widgetFrameView.setMinimumWidth(0);
+ iView.setBackgroundDrawable(new AlphaPatternDrawable((int)(5 * mDensity)));
+ iView.setImageBitmap(getPreviewBitmap());
+ }
+
+ private Bitmap getPreviewBitmap() {
+ int d = (int) (mDensity * 31); //30dip
+ int color = mValue;
+ Bitmap bm = Bitmap.createBitmap(d, d, Config.ARGB_8888);
+ int w = bm.getWidth();
+ int h = bm.getHeight();
+ int c = color;
+ for (int i = 0; i < w; i++) {
+ for (int j = i; j < h; j++) {
+ c = (i <= 1 || j <= 1 || i >= w-2 || j >= h-2) ? Color.GRAY : color;
+ bm.setPixel(i, j, c);
+ if (i != j) {
+ bm.setPixel(j, i, c);
+ }
+ }
+ }
+
+ return bm;
+ }
+
+ @Override
+ public void onColorChanged(int color) {
+ if (isPersistent()) {
+ persistInt(color);
+ }
+ mValue = color;
+ setPreviewColor();
+ try {
+ getOnPreferenceChangeListener().onPreferenceChange(this, color);
+ } catch (NullPointerException e) {
+
+ }
+ }
+
+ public boolean onPreferenceClick(Preference preference) {
+ showDialog(null);
+ return false;
+ }
+
+ protected void showDialog(Bundle state) {
+ mDialog = new ColorPickerDialog(getContext(), mValue);
+ mDialog.setOnColorChangedListener(this);
+ if (mAlphaSliderEnabled) {
+ mDialog.setAlphaSliderVisible(true);
+ }
+ if (state != null) {
+ mDialog.onRestoreInstanceState(state);
+ }
+ mDialog.show();
+ }
+
+ /**
+ * Toggle Alpha Slider visibility (by default it's disabled)
+ * @param enable
+ */
+ public void setAlphaSliderEnabled(boolean enable) {
+ mAlphaSliderEnabled = enable;
+ }
+
+ /**
+ * For custom purposes. Not used by ColorPickerPreferrence
+ * @param color
+ * @author Unknown
+ */
+ public static String convertToARGB(int color) {
+ String alpha = Integer.toHexString(Color.alpha(color));
+ String red = Integer.toHexString(Color.red(color));
+ String green = Integer.toHexString(Color.green(color));
+ String blue = Integer.toHexString(Color.blue(color));
+
+ if (alpha.length() == 1) {
+ alpha = "0" + alpha;
+ }
+
+ if (red.length() == 1) {
+ red = "0" + red;
+ }
+
+ if (green.length() == 1) {
+ green = "0" + green;
+ }
+
+ if (blue.length() == 1) {
+ blue = "0" + blue;
+ }
+
+ return "#" + alpha + red + green + blue;
+ }
+
+ /**
+ * For custom purposes. Not used by ColorPickerPreferrence
+ * @param argb
+ * @throws NumberFormatException
+ * @author Unknown
+ */
+ public static int convertToColorInt(String argb) throws NumberFormatException {
+
+ if (argb.startsWith("#")) {
+ argb = argb.replace("#", "");
+ }
+
+ int alpha = -1, red = -1, green = -1, blue = -1;
+
+ if (argb.length() == 8) {
+ alpha = Integer.parseInt(argb.substring(0, 2), 16);
+ red = Integer.parseInt(argb.substring(2, 4), 16);
+ green = Integer.parseInt(argb.substring(4, 6), 16);
+ blue = Integer.parseInt(argb.substring(6, 8), 16);
+ }
+ else if (argb.length() == 6) {
+ alpha = 255;
+ red = Integer.parseInt(argb.substring(0, 2), 16);
+ green = Integer.parseInt(argb.substring(2, 4), 16);
+ blue = Integer.parseInt(argb.substring(4, 6), 16);
+ }
+
+ return Color.argb(alpha, red, green, blue);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ if (mDialog == null || !mDialog.isShowing()) {
+ return superState;
+ }
+
+ final SavedState myState = new SavedState(superState);
+ myState.dialogBundle = mDialog.onSaveInstanceState();
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (state == null || !(state instanceof SavedState)) {
+ // Didn't save state for us in onSaveInstanceState
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ showDialog(myState.dialogBundle);
+ }
+
+ private static class SavedState extends BaseSavedState {
+ Bundle dialogBundle;
+
+ public SavedState(Parcel source) {
+ super(source);
+ dialogBundle = source.readBundle();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeBundle(dialogBundle);
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @SuppressWarnings("unused")
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/ColorPickerView.java b/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/ColorPickerView.java
new file mode 100644
index 00000000..c0ec0531
--- /dev/null
+++ b/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/ColorPickerView.java
@@ -0,0 +1,952 @@
+/*
+ * Copyright (C) 2010 Daniel Nilsson
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.margaritov.preference.colorpicker;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ComposeShader;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.Paint.Align;
+import android.graphics.Paint.Style;
+import android.graphics.Shader.TileMode;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * Displays a color picker to the user and allow them
+ * to select a color. A slider for the alpha channel is
+ * also available. Enable it by setting
+ * setAlphaSliderVisible(boolean) to true.
+ * @author Daniel Nilsson
+ */
+public class ColorPickerView extends View {
+
+ private final static int PANEL_SAT_VAL = 0;
+ private final static int PANEL_HUE = 1;
+ private final static int PANEL_ALPHA = 2;
+
+ /**
+ * The width in pixels of the border
+ * surrounding all color panels.
+ */
+ private final static float BORDER_WIDTH_PX = 1;
+
+ /**
+ * The width in dp of the hue panel.
+ */
+ private float HUE_PANEL_WIDTH = 30f;
+ /**
+ * The height in dp of the alpha panel
+ */
+ private float ALPHA_PANEL_HEIGHT = 20f;
+ /**
+ * The distance in dp between the different
+ * color panels.
+ */
+ private float PANEL_SPACING = 10f;
+ /**
+ * The radius in dp of the color palette tracker circle.
+ */
+ private float PALETTE_CIRCLE_TRACKER_RADIUS = 5f;
+ /**
+ * The dp which the tracker of the hue or alpha panel
+ * will extend outside of its bounds.
+ */
+ private float RECTANGLE_TRACKER_OFFSET = 2f;
+
+
+ private float mDensity = 1f;
+
+ private OnColorChangedListener mListener;
+
+ private Paint mSatValPaint;
+ private Paint mSatValTrackerPaint;
+
+ private Paint mHuePaint;
+ private Paint mHueTrackerPaint;
+
+ private Paint mAlphaPaint;
+ private Paint mAlphaTextPaint;
+
+ private Paint mBorderPaint;
+
+ private Shader mValShader;
+ private Shader mSatShader;
+ private Shader mHueShader;
+ private Shader mAlphaShader;
+
+ private int mAlpha = 0xff;
+ private float mHue = 360f;
+ private float mSat = 0f;
+ private float mVal = 0f;
+
+ private String mAlphaSliderText = "";
+ private int mSliderTrackerColor = 0xff1c1c1c;
+ private int mBorderColor = 0xff6E6E6E;
+ private boolean mShowAlphaPanel = false;
+
+ /*
+ * To remember which panel that has the "focus" when
+ * processing hardware button data.
+ */
+ private int mLastTouchedPanel = PANEL_SAT_VAL;
+
+ /**
+ * Offset from the edge we must have or else
+ * the finger tracker will get clipped when
+ * it is drawn outside of the view.
+ */
+ private float mDrawingOffset;
+
+
+ /*
+ * Distance form the edges of the view
+ * of where we are allowed to draw.
+ */
+ private RectF mDrawingRect;
+
+ private RectF mSatValRect;
+ private RectF mHueRect;
+ private RectF mAlphaRect;
+
+ private AlphaPatternDrawable mAlphaPattern;
+
+ private Point mStartTouchPoint = null;
+
+ public interface OnColorChangedListener {
+ public void onColorChanged(int color);
+ }
+
+ public ColorPickerView(Context context){
+ this(context, null);
+ }
+
+ public ColorPickerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ColorPickerView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init(){
+ mDensity = getContext().getResources().getDisplayMetrics().density;
+ PALETTE_CIRCLE_TRACKER_RADIUS *= mDensity;
+ RECTANGLE_TRACKER_OFFSET *= mDensity;
+ HUE_PANEL_WIDTH *= mDensity;
+ ALPHA_PANEL_HEIGHT *= mDensity;
+ PANEL_SPACING = PANEL_SPACING * mDensity;
+
+ mDrawingOffset = calculateRequiredOffset();
+
+ initPaintTools();
+
+ //Needed for receiving trackball motion events.
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ }
+
+ private void initPaintTools(){
+
+ mSatValPaint = new Paint();
+ mSatValTrackerPaint = new Paint();
+ mHuePaint = new Paint();
+ mHueTrackerPaint = new Paint();
+ mAlphaPaint = new Paint();
+ mAlphaTextPaint = new Paint();
+ mBorderPaint = new Paint();
+
+
+ mSatValTrackerPaint.setStyle(Style.STROKE);
+ mSatValTrackerPaint.setStrokeWidth(2f * mDensity);
+ mSatValTrackerPaint.setAntiAlias(true);
+
+ mHueTrackerPaint.setColor(mSliderTrackerColor);
+ mHueTrackerPaint.setStyle(Style.STROKE);
+ mHueTrackerPaint.setStrokeWidth(2f * mDensity);
+ mHueTrackerPaint.setAntiAlias(true);
+
+ mAlphaTextPaint.setColor(0xff1c1c1c);
+ mAlphaTextPaint.setTextSize(14f * mDensity);
+ mAlphaTextPaint.setAntiAlias(true);
+ mAlphaTextPaint.setTextAlign(Align.CENTER);
+ mAlphaTextPaint.setFakeBoldText(true);
+
+
+ }
+
+ private float calculateRequiredOffset(){
+ float offset = Math.max(PALETTE_CIRCLE_TRACKER_RADIUS, RECTANGLE_TRACKER_OFFSET);
+ offset = Math.max(offset, BORDER_WIDTH_PX * mDensity);
+
+ return offset * 1.5f;
+ }
+
+ private int[] buildHueColorArray(){
+
+ int[] hue = new int[361];
+
+ int count = 0;
+ for(int i = hue.length -1; i >= 0; i--, count++){
+ hue[count] = Color.HSVToColor(new float[]{i, 1f, 1f});
+ }
+
+ return hue;
+ }
+
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+
+ if(mDrawingRect.width() <= 0 || mDrawingRect.height() <= 0) return;
+
+ drawSatValPanel(canvas);
+ drawHuePanel(canvas);
+ drawAlphaPanel(canvas);
+
+ }
+
+ private void drawSatValPanel(Canvas canvas){
+
+ final RectF rect = mSatValRect;
+
+ if(BORDER_WIDTH_PX > 0){
+ mBorderPaint.setColor(mBorderColor);
+ canvas.drawRect(mDrawingRect.left, mDrawingRect.top, rect.right + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint);
+ }
+
+ if (mValShader == null) {
+ mValShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom,
+ 0xffffffff, 0xff000000, TileMode.CLAMP);
+ }
+
+ int rgb = Color.HSVToColor(new float[]{mHue,1f,1f});
+
+ mSatShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top,
+ 0xffffffff, rgb, TileMode.CLAMP);
+ ComposeShader mShader = new ComposeShader(mValShader, mSatShader, PorterDuff.Mode.MULTIPLY);
+ mSatValPaint.setShader(mShader);
+
+ canvas.drawRect(rect, mSatValPaint);
+
+ Point p = satValToPoint(mSat, mVal);
+
+ mSatValTrackerPaint.setColor(0xff000000);
+ canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS - 1f * mDensity, mSatValTrackerPaint);
+
+ mSatValTrackerPaint.setColor(0xffdddddd);
+ canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS, mSatValTrackerPaint);
+
+ }
+
+ private void drawHuePanel(Canvas canvas){
+
+ final RectF rect = mHueRect;
+
+ if(BORDER_WIDTH_PX > 0){
+ mBorderPaint.setColor(mBorderColor);
+ canvas.drawRect(rect.left - BORDER_WIDTH_PX,
+ rect.top - BORDER_WIDTH_PX,
+ rect.right + BORDER_WIDTH_PX,
+ rect.bottom + BORDER_WIDTH_PX,
+ mBorderPaint);
+ }
+
+ if (mHueShader == null) {
+ mHueShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom, buildHueColorArray(), null, TileMode.CLAMP);
+ mHuePaint.setShader(mHueShader);
+ }
+
+ canvas.drawRect(rect, mHuePaint);
+
+ float rectHeight = 4 * mDensity / 2;
+
+ Point p = hueToPoint(mHue);
+
+ RectF r = new RectF();
+ r.left = rect.left - RECTANGLE_TRACKER_OFFSET;
+ r.right = rect.right + RECTANGLE_TRACKER_OFFSET;
+ r.top = p.y - rectHeight;
+ r.bottom = p.y + rectHeight;
+
+
+ canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint);
+
+ }
+
+ private void drawAlphaPanel(Canvas canvas){
+
+ if(!mShowAlphaPanel || mAlphaRect == null || mAlphaPattern == null) return;
+
+ final RectF rect = mAlphaRect;
+
+ if(BORDER_WIDTH_PX > 0){
+ mBorderPaint.setColor(mBorderColor);
+ canvas.drawRect(rect.left - BORDER_WIDTH_PX,
+ rect.top - BORDER_WIDTH_PX,
+ rect.right + BORDER_WIDTH_PX,
+ rect.bottom + BORDER_WIDTH_PX,
+ mBorderPaint);
+ }
+
+
+ mAlphaPattern.draw(canvas);
+
+ float[] hsv = new float[]{mHue,mSat,mVal};
+ int color = Color.HSVToColor(hsv);
+ int acolor = Color.HSVToColor(0, hsv);
+
+ mAlphaShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top,
+ color, acolor, TileMode.CLAMP);
+
+
+ mAlphaPaint.setShader(mAlphaShader);
+
+ canvas.drawRect(rect, mAlphaPaint);
+
+ if(mAlphaSliderText != null && mAlphaSliderText!= ""){
+ canvas.drawText(mAlphaSliderText, rect.centerX(), rect.centerY() + 4 * mDensity, mAlphaTextPaint);
+ }
+
+ float rectWidth = 4 * mDensity / 2;
+
+ Point p = alphaToPoint(mAlpha);
+
+ RectF r = new RectF();
+ r.left = p.x - rectWidth;
+ r.right = p.x + rectWidth;
+ r.top = rect.top - RECTANGLE_TRACKER_OFFSET;
+ r.bottom = rect.bottom + RECTANGLE_TRACKER_OFFSET;
+
+ canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint);
+
+ }
+
+
+ private Point hueToPoint(float hue){
+
+ final RectF rect = mHueRect;
+ final float height = rect.height();
+
+ Point p = new Point();
+
+ p.y = (int) (height - (hue * height / 360f) + rect.top);
+ p.x = (int) rect.left;
+
+ return p;
+ }
+
+ private Point satValToPoint(float sat, float val){
+
+ final RectF rect = mSatValRect;
+ final float height = rect.height();
+ final float width = rect.width();
+
+ Point p = new Point();
+
+ p.x = (int) (sat * width + rect.left);
+ p.y = (int) ((1f - val) * height + rect.top);
+
+ return p;
+ }
+
+ private Point alphaToPoint(int alpha){
+
+ final RectF rect = mAlphaRect;
+ final float width = rect.width();
+
+ Point p = new Point();
+
+ p.x = (int) (width - (alpha * width / 0xff) + rect.left);
+ p.y = (int) rect.top;
+
+ return p;
+
+ }
+
+ private float[] pointToSatVal(float x, float y){
+
+ final RectF rect = mSatValRect;
+ float[] result = new float[2];
+
+ float width = rect.width();
+ float height = rect.height();
+
+ if (x < rect.left){
+ x = 0f;
+ }
+ else if(x > rect.right){
+ x = width;
+ }
+ else{
+ x = x - rect.left;
+ }
+
+ if (y < rect.top){
+ y = 0f;
+ }
+ else if(y > rect.bottom){
+ y = height;
+ }
+ else{
+ y = y - rect.top;
+ }
+
+
+ result[0] = 1.f / width * x;
+ result[1] = 1.f - (1.f / height * y);
+
+ return result;
+ }
+
+ private float pointToHue(float y){
+
+ final RectF rect = mHueRect;
+
+ float height = rect.height();
+
+ if (y < rect.top){
+ y = 0f;
+ }
+ else if(y > rect.bottom){
+ y = height;
+ }
+ else{
+ y = y - rect.top;
+ }
+
+ return 360f - (y * 360f / height);
+ }
+
+ private int pointToAlpha(int x){
+
+ final RectF rect = mAlphaRect;
+ final int width = (int) rect.width();
+
+ if(x < rect.left){
+ x = 0;
+ }
+ else if(x > rect.right){
+ x = width;
+ }
+ else{
+ x = x - (int)rect.left;
+ }
+
+ return 0xff - (x * 0xff / width);
+
+ }
+
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent event) {
+
+ float x = event.getX();
+ float y = event.getY();
+
+ boolean update = false;
+
+
+ if(event.getAction() == MotionEvent.ACTION_MOVE){
+
+ switch(mLastTouchedPanel){
+
+ case PANEL_SAT_VAL:
+
+ float sat, val;
+
+ sat = mSat + x/50f;
+ val = mVal - y/50f;
+
+ if(sat < 0f){
+ sat = 0f;
+ }
+ else if(sat > 1f){
+ sat = 1f;
+ }
+
+ if(val < 0f){
+ val = 0f;
+ }
+ else if(val > 1f){
+ val = 1f;
+ }
+
+ mSat = sat;
+ mVal = val;
+
+ update = true;
+
+ break;
+
+ case PANEL_HUE:
+
+ float hue = mHue - y * 10f;
+
+ if(hue < 0f){
+ hue = 0f;
+ }
+ else if(hue > 360f){
+ hue = 360f;
+ }
+
+ mHue = hue;
+
+ update = true;
+
+ break;
+
+ case PANEL_ALPHA:
+
+ if(!mShowAlphaPanel || mAlphaRect == null){
+ update = false;
+ }
+ else{
+
+ int alpha = (int) (mAlpha - x*10);
+
+ if(alpha < 0){
+ alpha = 0;
+ }
+ else if(alpha > 0xff){
+ alpha = 0xff;
+ }
+
+ mAlpha = alpha;
+
+
+ update = true;
+ }
+
+ break;
+ }
+
+
+ }
+
+
+ if(update){
+
+ if(mListener != null){
+ mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[]{mHue, mSat, mVal}));
+ }
+
+ invalidate();
+ return true;
+ }
+
+
+ return super.onTrackballEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+
+ boolean update = false;
+
+ switch(event.getAction()){
+
+ case MotionEvent.ACTION_DOWN:
+
+ mStartTouchPoint = new Point((int)event.getX(), (int)event.getY());
+
+ update = moveTrackersIfNeeded(event);
+
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+
+ update = moveTrackersIfNeeded(event);
+
+ break;
+
+ case MotionEvent.ACTION_UP:
+
+ mStartTouchPoint = null;
+
+ update = moveTrackersIfNeeded(event);
+
+ break;
+
+ }
+
+ if(update){
+
+ if(mListener != null){
+ mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[]{mHue, mSat, mVal}));
+ }
+
+ invalidate();
+ return true;
+ }
+
+
+ return super.onTouchEvent(event);
+ }
+
+ private boolean moveTrackersIfNeeded(MotionEvent event){
+
+ if(mStartTouchPoint == null) return false;
+
+ boolean update = false;
+
+ int startX = mStartTouchPoint.x;
+ int startY = mStartTouchPoint.y;
+
+
+ if(mHueRect.contains(startX, startY)){
+ mLastTouchedPanel = PANEL_HUE;
+
+ mHue = pointToHue(event.getY());
+
+ update = true;
+ }
+ else if(mSatValRect.contains(startX, startY)){
+
+ mLastTouchedPanel = PANEL_SAT_VAL;
+
+ float[] result = pointToSatVal(event.getX(), event.getY());
+
+ mSat = result[0];
+ mVal = result[1];
+
+ update = true;
+ }
+ else if(mAlphaRect != null && mAlphaRect.contains(startX, startY)){
+
+ mLastTouchedPanel = PANEL_ALPHA;
+
+ mAlpha = pointToAlpha((int)event.getX());
+
+ update = true;
+ }
+
+
+ return update;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+
+ int width = 0;
+ int height = 0;
+
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ int widthAllowed = MeasureSpec.getSize(widthMeasureSpec);
+ int heightAllowed = MeasureSpec.getSize(heightMeasureSpec);
+
+ widthAllowed = chooseWidth(widthMode, widthAllowed);
+ heightAllowed = chooseHeight(heightMode, heightAllowed);
+
+ if(!mShowAlphaPanel){
+
+ height = (int) (widthAllowed - PANEL_SPACING - HUE_PANEL_WIDTH);
+
+ //If calculated height (based on the width) is more than the allowed height.
+ if(height > heightAllowed || getTag().equals("landscape")) {
+ height = heightAllowed;
+ width = (int) (height + PANEL_SPACING + HUE_PANEL_WIDTH);
+ }
+ else{
+ width = widthAllowed;
+ }
+ }
+ else{
+
+ width = (int) (heightAllowed - ALPHA_PANEL_HEIGHT + HUE_PANEL_WIDTH);
+
+ if(width > widthAllowed){
+ width = widthAllowed;
+ height = (int) (widthAllowed - HUE_PANEL_WIDTH + ALPHA_PANEL_HEIGHT);
+ }
+ else{
+ height = heightAllowed;
+ }
+
+ }
+
+ setMeasuredDimension(width, height);
+ }
+
+ private int chooseWidth(int mode, int size){
+ if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) {
+ return size;
+ } else { // (mode == MeasureSpec.UNSPECIFIED)
+ return getPrefferedWidth();
+ }
+ }
+
+ private int chooseHeight(int mode, int size){
+ if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) {
+ return size;
+ } else { // (mode == MeasureSpec.UNSPECIFIED)
+ return getPrefferedHeight();
+ }
+ }
+
+ private int getPrefferedWidth(){
+
+ int width = getPrefferedHeight();
+
+ if(mShowAlphaPanel){
+ width -= (PANEL_SPACING + ALPHA_PANEL_HEIGHT);
+ }
+
+
+ return (int) (width + HUE_PANEL_WIDTH + PANEL_SPACING);
+
+ }
+
+ private int getPrefferedHeight(){
+
+ int height = (int)(200 * mDensity);
+
+ if(mShowAlphaPanel){
+ height += PANEL_SPACING + ALPHA_PANEL_HEIGHT;
+ }
+
+ return height;
+ }
+
+
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ mDrawingRect = new RectF();
+ mDrawingRect.left = mDrawingOffset + getPaddingLeft();
+ mDrawingRect.right = w - mDrawingOffset - getPaddingRight();
+ mDrawingRect.top = mDrawingOffset + getPaddingTop();
+ mDrawingRect.bottom = h - mDrawingOffset - getPaddingBottom();
+
+ setUpSatValRect();
+ setUpHueRect();
+ setUpAlphaRect();
+ }
+
+ private void setUpSatValRect(){
+
+ final RectF dRect = mDrawingRect;
+ float panelSide = dRect.height() - BORDER_WIDTH_PX * 2;
+
+ if(mShowAlphaPanel){
+ panelSide -= PANEL_SPACING + ALPHA_PANEL_HEIGHT;
+ }
+
+ float left = dRect.left + BORDER_WIDTH_PX;
+ float top = dRect.top + BORDER_WIDTH_PX;
+ float bottom = top + panelSide;
+ float right = left + panelSide;
+
+ mSatValRect = new RectF(left,top, right, bottom);
+ }
+
+ private void setUpHueRect(){
+ final RectF dRect = mDrawingRect;
+
+ float left = dRect.right - HUE_PANEL_WIDTH + BORDER_WIDTH_PX;
+ float top = dRect.top + BORDER_WIDTH_PX;
+ float bottom = dRect.bottom - BORDER_WIDTH_PX - (mShowAlphaPanel ? (PANEL_SPACING + ALPHA_PANEL_HEIGHT) : 0);
+ float right = dRect.right - BORDER_WIDTH_PX;
+
+ mHueRect = new RectF(left, top, right, bottom);
+ }
+
+ private void setUpAlphaRect() {
+
+ if(!mShowAlphaPanel) return;
+
+ final RectF dRect = mDrawingRect;
+
+ float left = dRect.left + BORDER_WIDTH_PX;
+ float top = dRect.bottom - ALPHA_PANEL_HEIGHT + BORDER_WIDTH_PX;
+ float bottom = dRect.bottom - BORDER_WIDTH_PX;
+ float right = dRect.right - BORDER_WIDTH_PX;
+
+ mAlphaRect = new RectF(left, top, right, bottom);
+
+ mAlphaPattern = new AlphaPatternDrawable((int) (5 * mDensity));
+ mAlphaPattern.setBounds(
+ Math.round(mAlphaRect.left),
+ Math.round(mAlphaRect.top),
+ Math.round(mAlphaRect.right),
+ Math.round(mAlphaRect.bottom)
+ );
+
+ }
+
+
+ /**
+ * Set a OnColorChangedListener to get notified when the color
+ * selected by the user has changed.
+ * @param listener
+ */
+ public void setOnColorChangedListener(OnColorChangedListener listener){
+ mListener = listener;
+ }
+
+ /**
+ * Set the color of the border surrounding all panels.
+ * @param color
+ */
+ public void setBorderColor(int color){
+ mBorderColor = color;
+ invalidate();
+ }
+
+ /**
+ * Get the color of the border surrounding all panels.
+ */
+ public int getBorderColor(){
+ return mBorderColor;
+ }
+
+ /**
+ * Get the current color this view is showing.
+ * @return the current color.
+ */
+ public int getColor(){
+ return Color.HSVToColor(mAlpha, new float[]{mHue,mSat,mVal});
+ }
+
+ /**
+ * Set the color the view should show.
+ * @param color The color that should be selected.
+ */
+ public void setColor(int color){
+ setColor(color, false);
+ }
+
+ /**
+ * Set the color this view should show.
+ * @param color The color that should be selected.
+ * @param callback If you want to get a callback to
+ * your OnColorChangedListener.
+ */
+ public void setColor(int color, boolean callback){
+
+ int alpha = Color.alpha(color);
+ int red = Color.red(color);
+ int blue = Color.blue(color);
+ int green = Color.green(color);
+
+ float[] hsv = new float[3];
+
+ Color.RGBToHSV(red, green, blue, hsv);
+
+ mAlpha = alpha;
+ mHue = hsv[0];
+ mSat = hsv[1];
+ mVal = hsv[2];
+
+ if(callback && mListener != null){
+ mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[]{mHue, mSat, mVal}));
+ }
+
+ invalidate();
+ }
+
+ /**
+ * Get the drawing offset of the color picker view.
+ * The drawing offset is the distance from the side of
+ * a panel to the side of the view minus the padding.
+ * Useful if you want to have your own panel below showing
+ * the currently selected color and want to align it perfectly.
+ * @return The offset in pixels.
+ */
+ public float getDrawingOffset(){
+ return mDrawingOffset;
+ }
+
+ /**
+ * Set if the user is allowed to adjust the alpha panel. Default is false.
+ * If it is set to false no alpha will be set.
+ * @param visible
+ */
+ public void setAlphaSliderVisible(boolean visible){
+
+ if(mShowAlphaPanel != visible){
+ mShowAlphaPanel = visible;
+
+ /*
+ * Reset all shader to force a recreation.
+ * Otherwise they will not look right after
+ * the size of the view has changed.
+ */
+ mValShader = null;
+ mSatShader = null;
+ mHueShader = null;
+ mAlphaShader = null;;
+
+ requestLayout();
+ }
+
+ }
+
+ public void setSliderTrackerColor(int color){
+ mSliderTrackerColor = color;
+
+ mHueTrackerPaint.setColor(mSliderTrackerColor);
+
+ invalidate();
+ }
+
+ public int getSliderTrackerColor(){
+ return mSliderTrackerColor;
+ }
+
+ /**
+ * Set the text that should be shown in the
+ * alpha slider. Set to null to disable text.
+ * @param res string resource id.
+ */
+ public void setAlphaSliderText(int res){
+ String text = getContext().getString(res);
+ setAlphaSliderText(text);
+ }
+
+ /**
+ * Set the text that should be shown in the
+ * alpha slider. Set to null to disable text.
+ * @param text Text that should be shown.
+ */
+ public void setAlphaSliderText(String text){
+ mAlphaSliderText = text;
+ invalidate();
+ }
+
+ /**
+ * Get the current value of the text
+ * that will be shown in the alpha
+ * slider.
+ * @return
+ */
+ public String getAlphaSliderText(){
+ return mAlphaSliderText;
+ }
+}
\ No newline at end of file
diff --git a/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/Test.java b/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/Test.java
new file mode 100644
index 00000000..e5e167d9
--- /dev/null
+++ b/external/ColorPickerPreference/src/net/margaritov/preference/colorpicker/Test.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2011 Sergey Margaritov
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.margaritov.preference.colorpicker;
+
+import net.margaritov.preference.colorpicker.R;
+
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.PreferenceActivity;
+
+public class Test extends PreferenceActivity {
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.settings);
+ ((ColorPickerPreference)findPreference("color2")).setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ preference.setSummary(ColorPickerPreference.convertToARGB(Integer.valueOf(String.valueOf(newValue))));
+ return true;
+ }
+
+ });
+ ((ColorPickerPreference)findPreference("color2")).setAlphaSliderEnabled(true);
+ }
+}
\ No newline at end of file
diff --git a/external/Crouton/.gitignore b/external/Crouton/.gitignore
new file mode 100644
index 00000000..90705626
--- /dev/null
+++ b/external/Crouton/.gitignore
@@ -0,0 +1,41 @@
+# built application files
+*.apk
+*.ap_
+*.jar
+gen-external-apklibs
+
+# keystore
+*.keystore
+
+# files for the dex VM
+*.dex
+
+# Java class files
+*.class
+
+# generated files
+bin/
+gen/
+target/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Eclipse project files
+.classpath
+.project
+.metadata
+.settings
+
+# IntelliJ files
+.idea
+*.iml
+
+# OSX files
+.DS_Store
+
+#vi swap files
+*.swp
+
+# maven target
+target
diff --git a/external/Crouton/LICENSE b/external/Crouton/LICENSE
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/external/Crouton/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/external/Crouton/README.markdown b/external/Crouton/README.markdown
new file mode 100644
index 00000000..8d4a3d77
--- /dev/null
+++ b/external/Crouton/README.markdown
@@ -0,0 +1,199 @@
+# Crouton
+![Crouton](https://raw.github.com/keyboardsurfer/Crouton/master/sample/res/drawable-xhdpi/ic_launcher.png "Crouton logo")
+
+Context sensitive notifications for Android
+
+## Overview
+
+**Crouton** is a class that can be used by Android developers that feel the need for an **alternative to the Context insensitive [Toast](http://developer.android.com/reference/android/widget/Toast.html)**.
+
+A Crouton will be displayed at the position the developer decides.
+Standard will be the of an application window.
+You can line up multiple Croutons for display, that will be shown one after another.
+
+You can check some features in the Crouton Demo.
+
+
+
+
+
+If you're already using Crouton and just want to download the latest version of the library, follow [this link](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22de.keyboardsurfer.android.widget%22).
+
+### Changelog
+#### Current version: 1.7
+
+####[1.7](https://github.com/keyboardsurfer/Crouton/tree/1.7)
+
+- `Crouton.setOnClickListener(OnClickListener)` has been introduced.
+- Infinite display of Crouton is possible via `Style.setDuration(Style.DURATION_INFINITE)`
+- Via `Crouton.hide(Crouton)` a Crouton can be hidden.
+
+####[1.6](https://github.com/keyboardsurfer/Crouton/tree/1.6)
+
+- Crouton now can be used on any Android device with **API level 4+**.
+- Changes the package name to `de.keyboardsurfer.android.widget`
+- Adds possibility to set a custom width
+- Can now be added to any ViewGroup (@coreform)
+- Integration with TalkBack (@coreform)
+- Adds Accessibility features (@coreform)
+- Fixes bug that got Crouton out of sync with reality (@coreform)
+- New [LifecycleCallback](https://github.com/keyboardsurfer/Crouton/blob/master/library/src/de/keyboardsurfer/android/widget/crouton/LifecycleCallback.java) (@coreform)
+- initializeCroutonView was refactored, to make it easier on the eyes
+- removes redundant initialization within Style.Builder
+- documentation improvments
+
+#### older versions
+
+Please see the `git log`
+
+## Usage
+
+The API is kept as simple as the Toast API:
+
+Create a Crouton for any CharSequence:
+
+ Crouton.makeText(Activity, CharSequence, [Style]).show();
+
+Create a Crouton with a String from your application's resources:
+
+ Crouton.makeText(Activity, int, Style).show();
+
+Further you can attach a Crouton to any view like this:
+
+ Crouton.makeText(Activity, int, Style, int).show();
+
+If you would like a more graphical introduction to Crouton check out [this presentation](https://speakerdeck.com/keyboardsurfer/crouton-devfest-berlin-2012).
+
+##Important!
+
+In your Activity.onDestroy() make sure to call
+
+ Crouton.cancelAllCroutons();
+
+to cancel cancel all scheduled Croutons.
+
+This is a workaround and further description is available in #24.
+
+## Basic Examples
+Currently you can use the three different Style attributes displayed below out of the box:
+
+![Alert](https://github.com/keyboardsurfer/Crouton/raw/master/res/Alert.png "Example of Style.ALERT")
+
+![Confirm](https://github.com/keyboardsurfer/Crouton/raw/master/res/Confirm.png "Example of Style.CONFIRM")
+
+![Info](https://github.com/keyboardsurfer/Crouton/raw/master/res/Info.png "Example of Style.INFO")
+
+## Extension and Modification
+
+The whole design of a Crouton is defined by [Style](https://github.com/keyboardsurfer/Crouton/blob/master/library/src/de/keyboardsurfer/android/widget/crouton/Style.java).
+
+You can use one of the styles Crouton ships with: **Style.ALERT**, **Style.CONFIRM** and **Style.INFO**. Or you can create your own Style.
+
+In general you can modify
+
+- display duration
+- dimension settings
+- options for the text to display
+- custom Views
+- appearance & disappearance Animation
+- displayed Image
+
+Since [Style](https://github.com/keyboardsurfer/Crouton/blob/master/library/src/de/keyboardsurfer/android/widget/crouton/Style.java) is the general entry point for tweaking Croutons, go and see for yourself what can be done with it.
+
+
+## Maven
+
+### From maven central
+
+Crouton is available in the maven central repository.
+
+To use crouton simply add
+
+```xml
+
+ crouton
+ 1.7
+ de.keyboardsurfer.android.widget
+
+```
+
+to your pom.xml
+
+If you also want the sources or javadoc add the respective classifier
+
+```xml
+ sources
+```
+
+or
+
+```xml
+ javadoc
+```
+to the dependency.
+
+If you are referencing a newer version of the Android Support Library in your application, you might want to exclude Crouton's dependency like this:
+
+```xml
+
+ crouton
+ ${crouton.version}
+ de.keyboardsurfer.android.widget
+
+
+ com.google.android
+ support-v4
+
+
+
+```
+
+### DIY
+
+The build requires Maven. Operations are very simple:
+
+* `mvn -f library/pom.xml clean package` will build a `jar` library;
+* `mvn clean package` will build a `jar` library and the sample application `apk`;
+* `mvn -f library/pom.xml clean install` will put Crouton in your local Maven repository.
+
+After putting Crouton in the repository you can add it as a dependency.
+
+```xml
+
+ crouton
+ 1.6
+ de.keyboardsurfer.android.widget
+
+```
+
+## Contribution
+
+### Pull requests welcome
+
+Feel free to contribute to Crouton.
+
+Either you found a bug or have created a new and awesome feature, just create a pull request.
+
+If you want to start to create a new feature or have any other questions regarding Crouton, [file an issue](https://github.com/keyboardsurfer/Crouton/issues/new).
+I'll try to answer as soon as I find the time.
+
+### Formatting
+
+For contributors using Eclipse there's a formatter available at the [download section](https://github.com/downloads/keyboardsurfer/Crouton/Crouton_Eclipseformatter.xml).
+
+In order to reduce merging pains on my end, please use this formatter or format your commit in a way similar to it's example.
+
+If you're using IDEA, the Eclipse Formatter plugin should allow you to use the formatter as well.
+
+## License
+
+* [Apache Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html)
+
+## Attributions
+
+The initial version was written by Benjamin Weiss at [Neofonie Mobile GmbH](http://mobile.neofonie.de).
+
+The name and the idea of [Crouton](https://github.com/keyboardsurfer/Crouton/blob/master/library/src/de/keyboardsurfer/android/widget/crouton/Crouton.java) originates in a [blog article](http://android.cyrilmottier.com/?p=773) by Cyril Mottier.
+
+The Crouton logo has been created by [Marie Schweiz](http://marie-schweiz.de).
diff --git a/external/Crouton/build.gradle b/external/Crouton/build.gradle
new file mode 100644
index 00000000..e69de29b
diff --git a/external/Crouton/library/.classpath b/external/Crouton/library/.classpath
new file mode 100644
index 00000000..7bc01d9a
--- /dev/null
+++ b/external/Crouton/library/.classpath
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/external/Crouton/library/.project b/external/Crouton/library/.project
new file mode 100644
index 00000000..a5d8dde2
--- /dev/null
+++ b/external/Crouton/library/.project
@@ -0,0 +1,33 @@
+
+
+ Crouton
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/external/Crouton/library/AndroidManifest.xml b/external/Crouton/library/AndroidManifest.xml
new file mode 100644
index 00000000..5f62e486
--- /dev/null
+++ b/external/Crouton/library/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/external/Crouton/library/ant.properties b/external/Crouton/library/ant.properties
new file mode 100644
index 00000000..b0971e89
--- /dev/null
+++ b/external/Crouton/library/ant.properties
@@ -0,0 +1,17 @@
+# This file is used to override default values used by the Ant build system.
+#
+# This file must be checked into Version Control Systems, as it is
+# integral to the build system of your project.
+
+# This file is only used by the Ant script.
+
+# You can use this to override default values such as
+# 'source.dir' for the location of your java source folder and
+# 'out.dir' for the location of your output folder.
+
+# You can also use it define how the release builds are signed by declaring
+# the following properties:
+# 'key.store' for the location of your keystore and
+# 'key.alias' for the name of the key to use.
+# The password will be asked during the build when you use the 'release' target.
+
diff --git a/external/Crouton/library/build.xml b/external/Crouton/library/build.xml
new file mode 100644
index 00000000..5ecd4a3a
--- /dev/null
+++ b/external/Crouton/library/build.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/external/Crouton/library/local.properties b/external/Crouton/library/local.properties
new file mode 100644
index 00000000..47f704fe
--- /dev/null
+++ b/external/Crouton/library/local.properties
@@ -0,0 +1,10 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+
+# location of the SDK. This is only used by Ant
+# For customization when using a Version Control System, please read the
+# header note.
+sdk.dir=/home/eric/Dev/android-sdk
diff --git a/external/Crouton/library/pom.xml b/external/Crouton/library/pom.xml
new file mode 100644
index 00000000..864bcce0
--- /dev/null
+++ b/external/Crouton/library/pom.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+ 4.0.0
+
+ Crouton
+ Context sensitive notifications for Android
+ https://github.com/keyboardsurfer/Crouton
+ crouton
+ de.keyboardsurfer.android.widget
+ 1.7
+ jar
+
+
+ UTF-8
+ 4.1.1.4
+ 16
+
+
+
+
+ keyboardsurfer
+ Benjamin Weiss
+
+
+
+
+
+ The Apache Software License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ git@github.com:keyboardsurfer/Crouton.git
+ scm:git:git@github.com:keyboardsurfer/Crouton.git
+ scm:git:git@github.com:keyboardsurfer/Crouton.git
+
+
+
+
+ android
+ ${android.version}
+ com.google.android
+ provided
+
+
+ com.google.android
+ support-v4
+ r11
+
+
+
+
+ src
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 2.9
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ com.jayway.maven.plugins.android.generation2
+ android-maven-plugin
+ 3.5.0
+
+
+
+
+
diff --git a/external/Crouton/library/proguard-project.txt b/external/Crouton/library/proguard-project.txt
new file mode 100644
index 00000000..f2fe1559
--- /dev/null
+++ b/external/Crouton/library/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/external/Crouton/library/project.properties b/external/Crouton/library/project.properties
new file mode 100644
index 00000000..bbe5c21a
--- /dev/null
+++ b/external/Crouton/library/project.properties
@@ -0,0 +1,17 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+
+target=android-18
+android.library=true
+android.library.reference.1=../../JakeWharton-ActionBarSherlock/library
diff --git a/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/Crouton.java b/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/Crouton.java
new file mode 100644
index 00000000..4d9d95b8
--- /dev/null
+++ b/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/Crouton.java
@@ -0,0 +1,825 @@
+/*
+ * Copyright 2012 - 2013 Benjamin Weiss
+ * Copyright 2012 Neofonie Mobile GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.keyboardsurfer.android.widget.crouton;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Shader;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+/*
+ * Based on an article by Cyril Mottier (http://android.cyrilmottier.com/?p=773)
+ */
+
+
+/**
+ * Displays information in a non-invasive context related manner. Like
+ * {@link android.widget.Toast}, but better.
+ *
+ * Important:
+ * Call {@link Crouton#clearCroutonsForActivity(Activity)} within
+ * {@link android.app.Activity#onDestroy()} to avoid {@link Context} leaks.
+ */
+public final class Crouton {
+ private static final int IMAGE_ID = 0x100;
+ private static final int TEXT_ID = 0x101;
+ private final CharSequence text;
+ private final Style style;
+ private final View customView;
+
+ private OnClickListener onClickListener;
+
+ private Activity activity;
+ private ViewGroup viewGroup;
+ private FrameLayout croutonView;
+ private Animation inAnimation;
+ private Animation outAnimation;
+ private LifecycleCallback lifecycleCallback = null;
+
+ /**
+ * Creates the {@link Crouton}.
+ *
+ * @param activity
+ * The {@link Activity} that the {@link Crouton} should be attached
+ * to.
+ * @param text
+ * The text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ */
+ private Crouton(Activity activity, CharSequence text, Style style) {
+ if ((activity == null) || (text == null) || (style == null)) {
+ throw new IllegalArgumentException("Null parameters are not accepted");
+ }
+
+ this.activity = activity;
+ this.viewGroup = null;
+ this.text = text;
+ this.style = style;
+ this.customView = null;
+ }
+
+ /**
+ * Creates the {@link Crouton}.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param text
+ * The text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ * @param viewGroup
+ * The {@link ViewGroup} that this {@link Crouton} should be added to.
+ */
+ private Crouton(Activity activity, CharSequence text, Style style, ViewGroup viewGroup) {
+ if ((activity == null) || (text == null) || (style == null)) {
+ throw new IllegalArgumentException("Null parameters are not accepted");
+ }
+
+ this.activity = activity;
+ this.text = text;
+ this.style = style;
+ this.viewGroup = viewGroup;
+ this.customView = null;
+ }
+
+ /**
+ * Creates the {@link Crouton}.
+ *
+ * @param activity
+ * The {@link Activity} that the {@link Crouton} should be attached
+ * to.
+ * @param customView
+ * The custom {@link View} to display
+ */
+ private Crouton(Activity activity, View customView) {
+ if ((activity == null) || (customView == null)) {
+ throw new IllegalArgumentException("Null parameters are not accepted");
+ }
+
+ this.activity = activity;
+ this.viewGroup = null;
+ this.customView = customView;
+ this.style = new Style.Builder().build();
+ this.text = null;
+ }
+
+ /**
+ * Creates the {@link Crouton}.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param customView
+ * The custom {@link View} to display
+ * @param viewGroup
+ * The {@link ViewGroup} that this {@link Crouton} should be added to.
+ */
+ private Crouton(Activity activity, View customView, ViewGroup viewGroup) {
+ if ((activity == null) || (customView == null)) {
+ throw new IllegalArgumentException("Null parameters are not accepted");
+ }
+
+ this.activity = activity;
+ this.customView = customView;
+ this.viewGroup = viewGroup;
+ this.style = new Style.Builder().build();
+ this.text = null;
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text and style for a given
+ * activity.
+ *
+ * @param activity
+ * The {@link Activity} that the {@link Crouton} should be attached
+ * to.
+ * @param text
+ * The text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ *
+ * @return The created {@link Crouton}.
+ */
+ public static Crouton makeText(Activity activity, CharSequence text, Style style) {
+ return new Crouton(activity, text, style);
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text and style for a given
+ * activity.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param text
+ * The text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ * @param viewGroup
+ * The {@link ViewGroup} that this {@link Crouton} should be added to.
+ *
+ * @return The created {@link Crouton}.
+ */
+ public static Crouton makeText(Activity activity, CharSequence text, Style style, ViewGroup viewGroup) {
+ return new Crouton(activity, text, style, viewGroup);
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text and style for a given
+ * activity.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param text
+ * The text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ * @param viewGroupResId
+ * The resource id of the {@link ViewGroup} that this {@link Crouton} should be added to.
+ *
+ * @return The created {@link Crouton}.
+ */
+ public static Crouton makeText(Activity activity, CharSequence text, Style style, int viewGroupResId) {
+ return new Crouton(activity, text, style, (ViewGroup) activity.findViewById(viewGroupResId));
+ }
+
+
+ /**
+ * Creates a {@link Crouton} with provided text-resource and style for a given
+ * activity.
+ *
+ * @param activity
+ * The {@link Activity} that the {@link Crouton} should be attached
+ * to.
+ * @param textResourceId
+ * The resource id of the text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ *
+ * @return The created {@link Crouton}.
+ */
+ public static Crouton makeText(Activity activity, int textResourceId, Style style) {
+ return makeText(activity, activity.getString(textResourceId), style);
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text-resource and style for a given
+ * activity.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param textResourceId
+ * The resource id of the text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ * @param viewGroup
+ * The {@link ViewGroup} that this {@link Crouton} should be added to.
+ *
+ * @return The created {@link Crouton}.
+ */
+ public static Crouton makeText(Activity activity, int textResourceId, Style style, ViewGroup viewGroup) {
+ return makeText(activity, activity.getString(textResourceId), style, viewGroup);
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text-resource and style for a given
+ * activity.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param textResourceId
+ * The resource id of the text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ * @param viewGroupResId
+ * The resource id of the {@link ViewGroup} that this {@link Crouton} should be added to.
+ *
+ * @return The created {@link Crouton}.
+ */
+ public static Crouton makeText(Activity activity, int textResourceId, Style style, int viewGroupResId) {
+ return makeText(activity, activity.getString(textResourceId), style,
+ (ViewGroup) activity.findViewById(viewGroupResId));
+ }
+
+
+ /**
+ * Creates a {@link Crouton} with provided text-resource and style for a given
+ * activity.
+ *
+ * @param activity
+ * The {@link Activity} that the {@link Crouton} should be attached
+ * to.
+ * @param customView
+ * The custom {@link View} to display
+ *
+ * @return The created {@link Crouton}.
+ */
+ public static Crouton make(Activity activity, View customView) {
+ return new Crouton(activity, customView);
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text-resource and style for a given
+ * activity.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param customView
+ * The custom {@link View} to display
+ * @param viewGroup
+ * The {@link ViewGroup} that this {@link Crouton} should be added to.
+ *
+ * @return The created {@link Crouton}.
+ */
+ public static Crouton make(Activity activity, View customView, ViewGroup viewGroup) {
+ return new Crouton(activity, customView, viewGroup);
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text-resource and style for a given
+ * activity.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param customView
+ * The custom {@link View} to display
+ * @param viewGroupResId
+ * The resource id of the {@link ViewGroup} that this {@link Crouton} should be added to.
+ *
+ * @return The created {@link Crouton}.
+ */
+ public static Crouton make(Activity activity, View customView, int viewGroupResId) {
+ return new Crouton(activity, customView, (ViewGroup) activity.findViewById(viewGroupResId));
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text and style for a given activity
+ * and displays it directly.
+ *
+ * @param activity
+ * The {@link android.app.Activity} that the {@link Crouton} should
+ * be attached to.
+ * @param text
+ * The text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ */
+ public static void showText(Activity activity, CharSequence text, Style style) {
+ makeText(activity, text, style).show();
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text and style for a given activity
+ * and displays it directly.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param text
+ * The text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ * @param viewGroup
+ * The {@link ViewGroup} that this {@link Crouton} should be added to.
+ */
+ public static void showText(Activity activity, CharSequence text, Style style, ViewGroup viewGroup) {
+ makeText(activity, text, style, viewGroup).show();
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text and style for a given activity
+ * and displays it directly.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param text
+ * The text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ * @param viewGroupResId
+ * The resource id of the {@link ViewGroup} that this {@link Crouton} should be added to.
+ */
+ public static void showText(Activity activity, CharSequence text, Style style, int viewGroupResId) {
+ makeText(activity, text, style, (ViewGroup) activity.findViewById(viewGroupResId)).show();
+ }
+
+
+ /**
+ * Creates a {@link Crouton} with provided text and style for a given activity
+ * and displays it directly.
+ *
+ * @param activity
+ * The {@link android.app.Activity} that the {@link Crouton} should
+ * be attached to.
+ * @param customView
+ * The custom {@link View} to display
+ */
+ public static void show(Activity activity, View customView) {
+ make(activity, customView).show();
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text and style for a given activity
+ * and displays it directly.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param customView
+ * The custom {@link View} to display
+ * @param viewGroup
+ * The {@link ViewGroup} that this {@link Crouton} should be added to.
+ */
+ public static void show(Activity activity, View customView, ViewGroup viewGroup) {
+ make(activity, customView, viewGroup).show();
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text and style for a given activity
+ * and displays it directly.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param customView
+ * The custom {@link View} to display
+ * @param viewGroupResId
+ * The resource id of the {@link ViewGroup} that this {@link Crouton} should be added to.
+ */
+ public static void show(Activity activity, View customView, int viewGroupResId) {
+ make(activity, customView, viewGroupResId).show();
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text-resource and style for a given
+ * activity and displays it directly.
+ *
+ * @param activity
+ * The {@link Activity} that the {@link Crouton} should be attached
+ * to.
+ * @param textResourceId
+ * The resource id of the text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ */
+ public static void showText(Activity activity, int textResourceId, Style style) {
+ showText(activity, activity.getString(textResourceId), style);
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text-resource and style for a given
+ * activity and displays it directly.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param textResourceId
+ * The resource id of the text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ * @param viewGroup
+ * The {@link ViewGroup} that this {@link Crouton} should be added to.
+ */
+ public static void showText(Activity activity, int textResourceId, Style style, ViewGroup viewGroup) {
+ showText(activity, activity.getString(textResourceId), style, viewGroup);
+ }
+
+ /**
+ * Creates a {@link Crouton} with provided text-resource and style for a given
+ * activity and displays it directly.
+ *
+ * @param activity
+ * The {@link Activity} that represents the context in which the Crouton should exist.
+ * @param textResourceId
+ * The resource id of the text you want to display.
+ * @param style
+ * The style that this {@link Crouton} should be created with.
+ * @param viewGroupResId
+ * The resource id of the {@link ViewGroup} that this {@link Crouton} should be added to.
+ */
+ public static void showText(Activity activity, int textResourceId, Style style, int viewGroupResId) {
+ showText(activity, activity.getString(textResourceId), style, viewGroupResId);
+ }
+
+ /**
+ * Allows hiding of a previously displayed {@link Crouton}.
+ * @param crouton The {@link Crouton} you want to hide.
+ */
+ public static void hide(Crouton crouton) {
+ Manager.getInstance().removeCrouton(crouton);
+ }
+
+ /**
+ * Cancels all queued {@link Crouton}s. If there is a {@link Crouton}
+ * displayed currently, it will be the last one displayed.
+ */
+ public static void cancelAllCroutons() {
+ Manager.getInstance().clearCroutonQueue();
+ }
+
+ /**
+ * Clears (and removes from {@link Activity}'s content view, if necessary) all
+ * croutons for the provided activity
+ *
+ * @param activity
+ * - The {@link Activity} to clear the croutons for.
+ */
+ public static void clearCroutonsForActivity(Activity activity) {
+ Manager.getInstance().clearCroutonsForActivity(activity);
+ }
+
+ /**
+ * Cancels a {@link Crouton} immediately.
+ */
+ public void cancel() {
+ Manager manager = Manager.getInstance();
+ manager.removeCroutonImmediately(this);
+ }
+
+ /**
+ * Displays the {@link Crouton}. If there's another {@link Crouton} visible at
+ * the time, this {@link Crouton} will be displayed afterwards.
+ */
+ public void show() {
+ Manager.getInstance().add(this);
+ }
+
+ public Animation getInAnimation() {
+ if ((null == this.inAnimation) && (null != this.activity)) {
+ if (getStyle().inAnimationResId > 0) {
+ this.inAnimation = AnimationUtils.loadAnimation(getActivity(), getStyle().inAnimationResId);
+ } else {
+ this.inAnimation = DefaultAnimationsBuilder.buildDefaultSlideInDownAnimation();
+ }
+ }
+
+ return inAnimation;
+ }
+
+ public Animation getOutAnimation() {
+ if ((null == this.outAnimation) && (null != this.activity)) {
+ if (getStyle().outAnimationResId > 0) {
+ this.outAnimation = AnimationUtils.loadAnimation(getActivity(), getStyle().outAnimationResId);
+ } else {
+ this.outAnimation = DefaultAnimationsBuilder.buildDefaultSlideOutUpAnimation();
+ }
+ }
+
+ return outAnimation;
+ }
+
+ /**
+ * @param lifecycleCallback
+ * Callback object for notable events in the life of a Crouton.
+ */
+ public void setLifecycleCallback(LifecycleCallback lifecycleCallback) {
+ this.lifecycleCallback = lifecycleCallback;
+ }
+
+ /**
+ * Convenience method to get the license text for embedding within your application.
+ * @return
+ * The license text.
+ */
+ public String getLicenseText() {
+ return "This application uses the Crouton library.\n\n" +
+ "Copyright 2012 - 2013 Benjamin Weiss \n" +
+ "Copyright 2012 Neofonie Mobile GmbH\n" +
+ "\n" +
+ "Licensed under the Apache License, Version 2.0 (the \"License\");\n" +
+ "you may not use this file except in compliance with the License.\n" +
+ "You may obtain a copy of the License at\n" +
+ "\n" +
+ " http://www.apache.org/licenses/LICENSE-2.0\n" +
+ "\n" +
+ "Unless required by applicable law or agreed to in writing, software\n" +
+ "distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
+ "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
+ "See the License for the specific language governing permissions and\n" +
+ "limitations under the License.";
+ }
+
+ /**
+ * Allows setting of an {@link OnClickListener} directly to a {@link Crouton} without having to use a custom view.
+ * @param onClickListener The {@link OnClickListener} to set.
+ * @return this {@link Crouton}.
+ */
+ public Crouton setOnClickListener(OnClickListener onClickListener){
+ this.onClickListener = onClickListener;
+ return this;
+ }
+
+ /**
+ * @return true
if the {@link Crouton} is being displayed, else
+ * false
.
+ */
+ boolean isShowing() {
+ return (null != activity) && (null != croutonView) && (null != croutonView.getParent());
+ }
+
+ /**
+ * Removes the activity reference this {@link Crouton} is holding
+ */
+ void detachActivity() {
+ activity = null;
+ }
+
+ /**
+ * Removes the viewGroup reference this {@link Crouton} is holding
+ */
+ void detachViewGroup() {
+ viewGroup = null;
+ }
+
+ /**
+ * Removes the lifecycleCallback reference this {@link Crouton} is holding
+ */
+ void detachLifecycleCallback() {
+ lifecycleCallback = null;
+ }
+
+ /**
+ * @return the lifecycleCallback
+ */
+ LifecycleCallback getLifecycleCallback() {
+ return lifecycleCallback;
+ }
+
+ /**
+ * @return the style
+ */
+ Style getStyle() {
+ return style;
+ }
+
+ /**
+ * @return the activity
+ */
+ Activity getActivity() {
+ return activity;
+ }
+
+ /**
+ * @return the viewGroup
+ */
+ ViewGroup getViewGroup() {
+ return viewGroup;
+ }
+
+ /**
+ * @return the text
+ */
+ CharSequence getText() {
+ return text;
+ }
+
+ /**
+ * @return the view
+ */
+ View getView() {
+ // return the custom view if one exists
+ if (null != this.customView) {
+ return this.customView;
+ }
+
+ // if already setup return the view
+ if (null == this.croutonView) {
+ initializeCroutonView();
+ }
+
+ return croutonView;
+ }
+
+ private void initializeCroutonView() {
+ Resources resources = this.activity.getResources();
+
+ this.croutonView = initializeCroutonViewGroup(resources);
+
+ // create content view
+ RelativeLayout contentView = initializeContentView(resources);
+ this.croutonView.addView(contentView);
+ }
+
+ private FrameLayout initializeCroutonViewGroup(Resources resources) {
+ FrameLayout croutonView = new FrameLayout(this.activity);
+
+ if(null != onClickListener)
+ croutonView.setOnClickListener(onClickListener);
+
+ final int height;
+ if (this.style.heightDimensionResId > 0) {
+ height = resources.getDimensionPixelSize(this.style.heightDimensionResId);
+ } else {
+ height = this.style.heightInPixels;
+ }
+
+ final int width;
+ if (this.style.widthDimensionResId > 0) {
+ width = resources.getDimensionPixelSize(this.style.widthDimensionResId);
+ } else {
+ width = this.style.widthInPixels;
+ }
+
+ croutonView.setLayoutParams(
+ new FrameLayout.LayoutParams(width != 0 ? width : FrameLayout.LayoutParams.MATCH_PARENT, height));
+
+ // set background
+ if (this.style.backgroundColorValue != -1) {
+ croutonView.setBackgroundColor(this.style.backgroundColorValue);
+ } else {
+ croutonView.setBackgroundColor(resources.getColor(this.style.backgroundColorResourceId));
+ }
+
+ // set the background drawable if set. This will override the background
+ // color.
+ if (this.style.backgroundDrawableResourceId != 0) {
+ Bitmap background = BitmapFactory.decodeResource(resources, this.style.backgroundDrawableResourceId);
+ BitmapDrawable drawable = new BitmapDrawable(resources, background);
+ if (this.style.isTileEnabled) {
+ drawable.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
+ }
+ croutonView.setBackgroundDrawable(drawable);
+ }
+ return croutonView;
+ }
+
+ private RelativeLayout initializeContentView(final Resources resources) {
+ RelativeLayout contentView = new RelativeLayout(this.activity);
+ contentView.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,
+ RelativeLayout.LayoutParams.WRAP_CONTENT));
+
+ // set padding
+ int padding = this.style.paddingInPixels;
+
+ // if a padding dimension has been set, this will overwrite any padding
+ // in pixels
+ if (this.style.paddingDimensionResId > 0) {
+ padding = resources.getDimensionPixelSize(this.style.paddingDimensionResId);
+ }
+ contentView.setPadding(padding, padding, padding, padding);
+
+ // only setup image if one is requested
+ ImageView image = null;
+ if ((null != this.style.imageDrawable) || (0 != this.style.imageResId)) {
+ image = initializeImageView();
+ contentView.addView(image, image.getLayoutParams());
+ }
+
+ TextView text = initializeTextView(resources);
+
+ RelativeLayout.LayoutParams textParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,
+ RelativeLayout.LayoutParams.WRAP_CONTENT);
+ if (null != image) {
+ textParams.addRule(RelativeLayout.RIGHT_OF, image.getId());
+ }
+ contentView.addView(text, textParams);
+ return contentView;
+ }
+
+ private TextView initializeTextView(final Resources resources) {
+ TextView text = new TextView(this.activity);
+ text.setId(TEXT_ID);
+ text.setText(this.text);
+ text.setTypeface(Typeface.DEFAULT_BOLD);
+ text.setGravity(this.style.gravity);
+
+ // set the text color if set
+ if (this.style.textColorResourceId != 0) {
+ text.setTextColor(resources.getColor(this.style.textColorResourceId));
+ }
+
+ // Set the text size. If the user has set a text size and text
+ // appearance, the text size in the text appearance
+ // will override this.
+ if (this.style.textSize != 0) {
+ text.setTextSize(TypedValue.COMPLEX_UNIT_SP, this.style.textSize);
+ }
+
+ // Setup the shadow if requested
+ if (this.style.textShadowColorResId != 0) {
+ initializeTextViewShadow(resources, text);
+ }
+
+ // Set the text appearance
+ if (this.style.textAppearanceResId != 0) {
+ text.setTextAppearance(this.activity, this.style.textAppearanceResId);
+ }
+ return text;
+ }
+
+ private void initializeTextViewShadow(final Resources resources, final TextView text) {
+ int textShadowColor = resources.getColor(this.style.textShadowColorResId);
+ float textShadowRadius = this.style.textShadowRadius;
+ float textShadowDx = this.style.textShadowDx;
+ float textShadowDy = this.style.textShadowDy;
+ text.setShadowLayer(textShadowRadius, textShadowDx, textShadowDy, textShadowColor);
+ }
+
+ private ImageView initializeImageView() {
+ ImageView image;
+ image = new ImageView(this.activity);
+ image.setId(IMAGE_ID);
+ image.setAdjustViewBounds(true);
+ image.setScaleType(this.style.imageScaleType);
+
+ // set the image drawable if not null
+ if (null != this.style.imageDrawable) {
+ image.setImageDrawable(this.style.imageDrawable);
+ }
+
+ // set the image resource if not 0. This will overwrite the drawable
+ // if both are set
+ if (this.style.imageResId != 0) {
+ image.setImageResource(this.style.imageResId);
+ }
+
+ RelativeLayout.LayoutParams imageParams = new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.WRAP_CONTENT,
+ RelativeLayout.LayoutParams.WRAP_CONTENT);
+ imageParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
+ imageParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
+
+ image.setLayoutParams(imageParams);
+
+ return image;
+ }
+
+ @Override
+ public String toString() {
+ return "Crouton{" +
+ "text=" + text +
+ ", style=" + style +
+ ", customView=" + customView +
+ ", activity=" + activity +
+ ", viewGroup=" + viewGroup +
+ ", croutonView=" + croutonView +
+ ", inAnimation=" + inAnimation +
+ ", outAnimation=" + outAnimation +
+ ", lifecycleCallback=" + lifecycleCallback +
+ '}';
+ }
+}
diff --git a/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/DefaultAnimationsBuilder.java b/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/DefaultAnimationsBuilder.java
new file mode 100644
index 00000000..b1109c86
--- /dev/null
+++ b/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/DefaultAnimationsBuilder.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2012 - 2013 Benjamin Weiss
+ * Copyright 2012 Neofonie Mobile GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.keyboardsurfer.android.widget.crouton;
+
+import android.view.animation.Animation;
+import android.view.animation.TranslateAnimation;
+
+/**
+ * Builds the default animations for showing and hiding a {@link Crouton}.
+ */
+final class DefaultAnimationsBuilder {
+ private static Animation slideInDownAnimation, slideOutUpAnimation;
+
+ protected static final class SlideInDownAnimationParameters {
+ private SlideInDownAnimationParameters() {
+ /* no-op */
+ }
+
+ public static final float FROM_X_DELTA = 0;
+ public static final float TO_X_DELTA = 0;
+ public static final float FROM_Y_DELTA = -50;
+ public static final float TO_Y_DELTA = 0;
+
+ public static final long DURATION = 400;
+ }
+
+ protected static final class SlideOutUpAnimationParameters {
+ private SlideOutUpAnimationParameters() {
+ /* no-op */
+ }
+
+ public static final float FROM_X_DELTA = 0;
+ public static final float TO_X_DELTA = 0;
+ public static final float FROM_Y_DELTA = 0;
+ public static final float TO_Y_DELTA = -50;
+
+ public static final long DURATION = 400;
+ }
+
+ private DefaultAnimationsBuilder() {
+ /* no-op */
+ }
+
+ /**
+ * @return The default Animation for a showing {@link Crouton}.
+ */
+ public static Animation buildDefaultSlideInDownAnimation() {
+ if (null == slideInDownAnimation) {
+ slideInDownAnimation = new TranslateAnimation(SlideInDownAnimationParameters.FROM_X_DELTA,
+ SlideInDownAnimationParameters.TO_X_DELTA,
+ SlideInDownAnimationParameters.FROM_Y_DELTA, SlideInDownAnimationParameters.TO_Y_DELTA);
+ slideInDownAnimation.setDuration(SlideInDownAnimationParameters.DURATION);
+ }
+
+ return slideInDownAnimation;
+ }
+
+ /**
+ * @return The default Animation for a hiding {@link Crouton}.
+ */
+ public static Animation buildDefaultSlideOutUpAnimation() {
+ if (null == slideOutUpAnimation) {
+ slideOutUpAnimation = new TranslateAnimation(SlideOutUpAnimationParameters.FROM_X_DELTA,
+ SlideOutUpAnimationParameters.TO_X_DELTA,
+ SlideOutUpAnimationParameters.FROM_Y_DELTA, SlideOutUpAnimationParameters.TO_Y_DELTA);
+ slideOutUpAnimation.setDuration(SlideOutUpAnimationParameters.DURATION);
+ }
+ return slideOutUpAnimation;
+ }
+}
diff --git a/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/LifecycleCallback.java b/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/LifecycleCallback.java
new file mode 100644
index 00000000..a2ade574
--- /dev/null
+++ b/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/LifecycleCallback.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2012 - 2013 Benjamin Weiss
+ * Copyright 2012 Neofonie Mobile GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.keyboardsurfer.android.widget.crouton;
+
+public interface LifecycleCallback {
+ public void onDisplayed();
+ public void onRemoved();
+ //public void onCeasarDressing();
+}
diff --git a/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/Manager.java b/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/Manager.java
new file mode 100644
index 00000000..0d462941
--- /dev/null
+++ b/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/Manager.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright 2012 - 2013 Benjamin Weiss
+ * Copyright 2012 Neofonie Mobile GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.keyboardsurfer.android.widget.crouton;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.FrameLayout;
+import java.util.Iterator;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+
+/**
+ * Manages the lifecycle of {@link Crouton}s.
+ */
+final class Manager extends Handler {
+ private static final class Messages {
+ private Messages() { /* no-op */
+ }
+
+ public static final int DISPLAY_CROUTON = 0xc2007;
+ public static final int ADD_CROUTON_TO_VIEW = 0xc20074dd;
+ public static final int REMOVE_CROUTON = 0xc2007de1;
+ }
+
+ private static Manager INSTANCE;
+
+ private Queue croutonQueue;
+
+ private Manager() {
+ croutonQueue = new LinkedBlockingQueue();
+ }
+
+ /**
+ * @return The currently used instance of the {@link Manager}.
+ */
+ static synchronized Manager getInstance() {
+ if (null == INSTANCE) {
+ INSTANCE = new Manager();
+ }
+
+ return INSTANCE;
+ }
+
+ /**
+ * Inserts a {@link Crouton} to be displayed.
+ *
+ * @param crouton
+ * The {@link Crouton} to be displayed.
+ */
+ void add(Crouton crouton) {
+ croutonQueue.add(crouton);
+ displayCrouton();
+ }
+
+ /**
+ * Displays the next {@link Crouton} within the queue.
+ */
+ private void displayCrouton() {
+ if (croutonQueue.isEmpty()) {
+ return;
+ }
+
+ // First peek whether the Crouton has an activity.
+ final Crouton currentCrouton = croutonQueue.peek();
+
+ // If the activity is null we poll the Crouton off the queue.
+ if (null == currentCrouton.getActivity()) {
+ croutonQueue.poll();
+ }
+
+ if (!currentCrouton.isShowing()) {
+ // Display the Crouton
+ sendMessage(currentCrouton, Messages.ADD_CROUTON_TO_VIEW);
+ if (null != currentCrouton.getLifecycleCallback()) {
+ currentCrouton.getLifecycleCallback().onDisplayed();
+ }
+ } else {
+ sendMessageDelayed(currentCrouton, Messages.DISPLAY_CROUTON, calculateCroutonDuration(currentCrouton));
+ }
+ }
+
+ private long calculateCroutonDuration(Crouton crouton) {
+ long croutonDuration = crouton.getStyle().durationInMilliseconds;
+ croutonDuration += crouton.getInAnimation().getDuration();
+ croutonDuration += crouton.getOutAnimation().getDuration();
+ return croutonDuration;
+ }
+
+ /**
+ * Sends a {@link Crouton} within a {@link Message}.
+ *
+ * @param crouton
+ * The {@link Crouton} that should be sent.
+ * @param messageId
+ * The {@link Message} id.
+ */
+ private void sendMessage(Crouton crouton, final int messageId) {
+ final Message message = obtainMessage(messageId);
+ message.obj = crouton;
+ sendMessage(message);
+ }
+
+ /**
+ * Sends a {@link Crouton} within a delayed {@link Message}.
+ *
+ * @param crouton
+ * The {@link Crouton} that should be sent.
+ * @param messageId
+ * The {@link Message} id.
+ * @param delay
+ * The delay in milliseconds.
+ */
+ private void sendMessageDelayed(Crouton crouton, final int messageId, final long delay) {
+ Message message = obtainMessage(messageId);
+ message.obj = crouton;
+ sendMessageDelayed(message, delay);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see android.os.Handler#handleMessage(android.os.Message)
+ */
+ @Override
+ public void handleMessage(Message message) {
+ final Crouton crouton = (Crouton) message.obj;
+
+ switch (message.what) {
+ case Messages.DISPLAY_CROUTON: {
+ displayCrouton();
+ break;
+ }
+
+ case Messages.ADD_CROUTON_TO_VIEW: {
+ addCroutonToView(crouton);
+ break;
+ }
+
+ case Messages.REMOVE_CROUTON: {
+ removeCrouton(crouton);
+ if (null != crouton.getLifecycleCallback()) {
+ crouton.getLifecycleCallback().onRemoved();
+ }
+ break;
+ }
+
+ default: {
+ super.handleMessage(message);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Adds a {@link Crouton} to the {@link ViewParent} of it's {@link Activity}.
+ *
+ * @param crouton
+ * The {@link Crouton} that should be added.
+ */
+ private void addCroutonToView(Crouton crouton) {
+ // don't add if it is already showing
+ if (crouton.isShowing()) {
+ return;
+ }
+
+ View croutonView = crouton.getView();
+ if (null == croutonView.getParent()) {
+ ViewGroup.LayoutParams params = croutonView.getLayoutParams();
+ if (null == params) {
+ params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+ // display Crouton in ViewGroup is it has been supplied
+ if (null != crouton.getViewGroup()) {
+ // TODO implement add to last position feature (need to align with how this will be requested for activity)
+ if (crouton.getViewGroup() instanceof FrameLayout) {
+ crouton.getViewGroup().addView(croutonView, params);
+ } else {
+ crouton.getViewGroup().addView(croutonView, 0, params);
+ }
+ } else {
+ Activity activity = crouton.getActivity();
+ if (null == activity || activity.isFinishing()) {
+ return;
+ }
+ activity.addContentView(croutonView, params);
+ }
+ }
+ croutonView.startAnimation(crouton.getInAnimation());
+ announceForAccessibilityCompat(crouton.getActivity(), crouton.getText());
+ if (Style.DURATION_INFINITE != crouton.getStyle().durationInMilliseconds) {
+ sendMessageDelayed(crouton, Messages.REMOVE_CROUTON,
+ crouton.getStyle().durationInMilliseconds + crouton.getInAnimation().getDuration());
+ }
+ }
+
+ /**
+ * Removes the {@link Crouton}'s view after it's display
+ * durationInMilliseconds.
+ *
+ * @param crouton
+ * The {@link Crouton} added to a {@link ViewGroup} and should be
+ * removed.
+ */
+ protected void removeCrouton(Crouton crouton) {
+ View croutonView = crouton.getView();
+ ViewGroup croutonParentView = (ViewGroup) croutonView.getParent();
+
+ if (null != croutonParentView) {
+ croutonView.startAnimation(crouton.getOutAnimation());
+
+ // Remove the Crouton from the queue.
+ Crouton removed = croutonQueue.poll();
+
+ // Remove the crouton from the view's parent.
+ croutonParentView.removeView(croutonView);
+ if (null != removed) {
+ removed.detachActivity();
+ removed.detachViewGroup();
+ if (null != removed.getLifecycleCallback()) {
+ removed.getLifecycleCallback().onRemoved();
+ }
+ removed.detachLifecycleCallback();
+ }
+
+ // Send a message to display the next crouton but delay it by the out
+ // animation duration to make sure it finishes
+ sendMessageDelayed(crouton, Messages.DISPLAY_CROUTON, crouton.getOutAnimation().getDuration());
+ }
+ }
+
+ /**
+ * Removes a {@link Crouton} immediately, even when it's currently being
+ * displayed.
+ *
+ * @param crouton
+ * The {@link Crouton} that should be removed.
+ */
+ void removeCroutonImmediately(Crouton crouton) {
+ // if Crouton has already been displayed then it may not be in the queue (because it was popped).
+ // This ensures the displayed Crouton is removed from its parent immediately, whether another instance
+ // of it exists in the queue or not.
+ // Note: crouton.isShowing() is false here even if it really is showing, as croutonView object in
+ // Crouton seems to be out of sync with reality!
+ if (null != crouton.getActivity() && null != crouton.getView() && null != crouton.getView().getParent()) {
+ ((ViewGroup) crouton.getView().getParent()).removeView(crouton.getView());
+
+ // remove any messages pending for the crouton
+ removeAllMessagesForCrouton(crouton);
+ }
+ // remove any matching croutons from queue
+ if (null != croutonQueue) {
+ final Iterator croutonIterator = croutonQueue.iterator();
+ while (croutonIterator.hasNext()) {
+ final Crouton c = croutonIterator.next();
+ if (c.equals(crouton) && (null != c.getActivity())) {
+ // remove the crouton from the content view
+ if (crouton.isShowing()) {
+ ((ViewGroup) c.getView().getParent()).removeView(c.getView());
+ }
+
+ // remove any messages pending for the crouton
+ removeAllMessagesForCrouton(c);
+
+ // remove the crouton from the queue
+ croutonIterator.remove();
+
+ // we have found our crouton so just break
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes all {@link Crouton}s from the queue.
+ */
+ void clearCroutonQueue() {
+ removeAllMessages();
+
+ if (null != croutonQueue) {
+ // remove any views that may already have been added to the activity's
+ // content view
+ for (Crouton crouton : croutonQueue) {
+ if (crouton.isShowing()) {
+ ((ViewGroup) crouton.getView().getParent()).removeView(crouton.getView());
+ }
+ }
+ croutonQueue.clear();
+ }
+ }
+
+ /**
+ * Removes all {@link Crouton}s for the provided activity. This will remove
+ * crouton from {@link Activity}s content view immediately.
+ */
+ void clearCroutonsForActivity(Activity activity) {
+ if (null != croutonQueue) {
+ Iterator croutonIterator = croutonQueue.iterator();
+ while (croutonIterator.hasNext()) {
+ Crouton crouton = croutonIterator.next();
+ if ((null != crouton.getActivity()) && crouton.getActivity().equals(activity)) {
+ // remove the crouton from the content view
+ if (crouton.isShowing()) {
+ ((ViewGroup) crouton.getView().getParent()).removeView(crouton.getView());
+ }
+
+ removeAllMessagesForCrouton(crouton);
+
+ // remove the crouton from the queue
+ croutonIterator.remove();
+ }
+ }
+ }
+ }
+
+ private void removeAllMessages() {
+ removeMessages(Messages.ADD_CROUTON_TO_VIEW);
+ removeMessages(Messages.DISPLAY_CROUTON);
+ removeMessages(Messages.REMOVE_CROUTON);
+ }
+
+ private void removeAllMessagesForCrouton(Crouton crouton) {
+ removeMessages(Messages.ADD_CROUTON_TO_VIEW, crouton);
+ removeMessages(Messages.DISPLAY_CROUTON, crouton);
+ removeMessages(Messages.REMOVE_CROUTON, crouton);
+
+ }
+
+ /**
+ * Generates and dispatches an SDK-specific spoken announcement.
+ *
+ * For backwards compatibility, we're constructing an event from scratch
+ * using the appropriate event type. If your application only targets SDK
+ * 16+, you can just call View.announceForAccessibility(CharSequence).
+ *
+ *
+ * note: AccessibilityManager is only available from API lvl 4.
+ *
+ * Adapted from https://http://eyes-free.googlecode.com/files/accessibility_codelab_demos_v2_src.zip
+ * via https://github.com/coreform/android-formidable-validation
+ *
+ * @param context
+ * Used to get {@link AccessibilityManager}
+ * @param text
+ * The text to announce.
+ */
+ public static void announceForAccessibilityCompat(Context context, CharSequence text) {
+ if (Build.VERSION.SDK_INT >= 4) {
+ AccessibilityManager accessibilityManager = (AccessibilityManager) context.getSystemService(
+ Context.ACCESSIBILITY_SERVICE);
+ if (!accessibilityManager.isEnabled()) {
+ return;
+ }
+
+ // Prior to SDK 16, announcements could only be made through FOCUSED
+ // events. Jelly Bean (SDK 16) added support for speaking text verbatim
+ // using the ANNOUNCEMENT event type.
+ final int eventType;
+ if (Build.VERSION.SDK_INT < 16) {
+ eventType = AccessibilityEvent.TYPE_VIEW_FOCUSED;
+ } else {
+ eventType = AccessibilityEventCompat.TYPE_ANNOUNCEMENT;
+ }
+
+ // Construct an accessibility event with the minimum recommended
+ // attributes. An event without a class name or package may be dropped.
+ final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+ event.getText().add(text);
+ event.setClassName(Manager.class.getName());
+ event.setPackageName(context.getPackageName());
+
+ // Sends the event directly through the accessibility manager. If your
+ // application only targets SDK 14+, you should just call
+ // getParent().requestSendAccessibilityEvent(this, event);
+ accessibilityManager.sendAccessibilityEvent(event);
+ }
+ }
+}
diff --git a/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/Style.java b/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/Style.java
new file mode 100644
index 00000000..28f2adaa
--- /dev/null
+++ b/external/Crouton/library/src/de/keyboardsurfer/android/widget/crouton/Style.java
@@ -0,0 +1,539 @@
+/*
+ * Copyright 2012 - 2013 Benjamin Weiss
+ * Copyright 2012 Neofonie Mobile GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.keyboardsurfer.android.widget.crouton;
+
+import android.graphics.drawable.Drawable;
+import android.view.Gravity;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.ImageView;
+
+
+/**
+ * The style for a {@link Crouton}.
+ */
+
+public class Style {
+
+ /**
+ * Display a {@link Crouton} for an infinite amount of time or
+ * until {@link de.keyboardsurfer.android.widget.crouton.Crouton#cancel()} has been called.
+ */
+ public static final int DURATION_INFINITE = -1;
+
+ /**
+ * Default style for alerting the user.
+ */
+ public static final Style ALERT;
+ /**
+ * Default style for confirming an action.
+ */
+ public static final Style CONFIRM;
+ /**
+ * Default style for general information.
+ */
+ public static final Style INFO;
+
+ public static final int holoRedLight = 0xffff4444;
+ public static final int holoGreenLight = 0xff99cc00;
+ public static final int holoBlueLight = 0xff33b5e5;
+
+ static {
+ ALERT = new Builder().setDuration(5000).setBackgroundColorValue(holoRedLight).setHeight(LayoutParams.WRAP_CONTENT)
+ .build();
+ CONFIRM = new Builder().setDuration(3000).setBackgroundColorValue(holoGreenLight).setHeight(
+ LayoutParams.WRAP_CONTENT).build();
+ INFO = new Builder().setDuration(3000).setBackgroundColorValue(holoBlueLight).setHeight(LayoutParams.WRAP_CONTENT)
+ .build();
+ }
+
+ /**
+ * The durationInMilliseconds the {@link Crouton} will be displayed in
+ * milliseconds.
+ */
+ final int durationInMilliseconds;
+
+ /**
+ * The resource id of the backgroundResourceId.
+ *
+ * 0 for no backgroundResourceId.
+ */
+ final int backgroundColorResourceId;
+
+ /**
+ * The resource id of the backgroundDrawableResourceId.
+ *
+ * 0 for no backgroundDrawableResourceId.
+ */
+ final int backgroundDrawableResourceId;
+
+ /**
+ * The backgroundColorResourceValue's e.g. 0xffff4444;
+ *
+ * -1 for no value.
+ */
+ final int backgroundColorValue;
+
+ /**
+ * Whether we should isTileEnabled the backgroundResourceId or not.
+ */
+ final boolean isTileEnabled;
+
+ /**
+ * The text colorResourceId's resource id.
+ *
+ * 0 sets the text colorResourceId to the system theme default.
+ */
+ final int textColorResourceId;
+
+ /**
+ * The height of the {@link Crouton} in pixels.
+ */
+ final int heightInPixels;
+
+ /**
+ * Resource ID for the height of the {@link Crouton}.
+ */
+ final int heightDimensionResId;
+
+ /**
+ * The width of the {@link Crouton} in pixels.
+ */
+ final int widthInPixels;
+
+ /**
+ * Resource ID for the width of the {@link Crouton}.
+ */
+ final int widthDimensionResId;
+
+ /**
+ * The text's gravity as provided by {@link Gravity}.
+ */
+ final int gravity;
+
+ /**
+ * An additional image to display in the {@link Crouton}.
+ */
+ final Drawable imageDrawable;
+
+ /**
+ * An additional image to display in the {@link Crouton}.
+ */
+ final int imageResId;
+
+ /**
+ * The {@link ImageView.ScaleType} for the image to display in the
+ * {@link Crouton}.
+ */
+ final ImageView.ScaleType imageScaleType;
+
+ /**
+ * The text size in sp
+ *
+ * 0 sets the text size to the system theme default
+ */
+ final int textSize;
+
+ /**
+ * The text shadow color's resource id
+ */
+ final int textShadowColorResId;
+
+ /**
+ * The text shadow radius
+ */
+ final float textShadowRadius;
+
+ /**
+ * The text shadow vertical offset
+ */
+ final float textShadowDy;
+
+ /**
+ * The text shadow horizontal offset
+ */
+ final float textShadowDx;
+
+ /**
+ * The text appearance resource id for the text.
+ */
+ final int textAppearanceResId;
+
+ /**
+ * The resource id for the in animation
+ */
+ final int inAnimationResId;
+
+ /**
+ * The resource id for the out animation
+ */
+ final int outAnimationResId;
+
+ /**
+ * The padding for the crouton view content in pixels
+ */
+ final int paddingInPixels;
+
+ /**
+ * The resource id for the padding for the view content
+ */
+ final int paddingDimensionResId;
+
+ private Style(final Builder builder) {
+ this.durationInMilliseconds = builder.durationInMilliseconds;
+ this.backgroundColorResourceId = builder.backgroundColorResourceId;
+ this.backgroundDrawableResourceId = builder.backgroundDrawableResourceId;
+ this.isTileEnabled = builder.isTileEnabled;
+ this.textColorResourceId = builder.textColorResourceId;
+ this.heightInPixels = builder.heightInPixels;
+ this.heightDimensionResId = builder.heightDimensionResId;
+ this.widthInPixels = builder.widthInPixels;
+ this.widthDimensionResId = builder.widthDimensionResId;
+ this.gravity = builder.gravity;
+ this.imageDrawable = builder.imageDrawable;
+ this.textSize = builder.textSize;
+ this.textShadowColorResId = builder.textShadowColorResId;
+ this.textShadowRadius = builder.textShadowRadius;
+ this.textShadowDx = builder.textShadowDx;
+ this.textShadowDy = builder.textShadowDy;
+ this.textAppearanceResId = builder.textAppearanceResId;
+ this.inAnimationResId = builder.inAnimationResId;
+ this.outAnimationResId = builder.outAnimationResId;
+ this.imageResId = builder.imageResId;
+ this.imageScaleType = builder.imageScaleType;
+ this.paddingInPixels = builder.paddingInPixels;
+ this.paddingDimensionResId = builder.paddingDimensionResId;
+ this.backgroundColorValue = builder.backgroundColorValue;
+ }
+
+ /**
+ * Builder for the {@link Style} object.
+ */
+ public static class Builder {
+ private int durationInMilliseconds;
+ private int backgroundColorValue;
+ private int backgroundColorResourceId;
+ private int backgroundDrawableResourceId;
+ private boolean isTileEnabled;
+ private int textColorResourceId;
+ private int heightInPixels;
+ private int heightDimensionResId;
+ private int widthInPixels;
+ private int widthDimensionResId;
+ private int gravity;
+ private Drawable imageDrawable;
+ private int textSize;
+ private int textShadowColorResId;
+ private float textShadowRadius;
+ private float textShadowDx;
+ private float textShadowDy;
+ private int textAppearanceResId;
+ private int inAnimationResId;
+ private int outAnimationResId;
+ private int imageResId;
+ private ImageView.ScaleType imageScaleType;
+ private int paddingInPixels;
+ private int paddingDimensionResId;
+
+ public Builder() {
+ durationInMilliseconds = 3000;
+ paddingInPixels = 10;
+ backgroundColorResourceId = android.R.color.holo_blue_light;
+ backgroundDrawableResourceId = 0;
+ backgroundColorValue = -1;
+ isTileEnabled = false;
+ textColorResourceId = android.R.color.white;
+ heightInPixels = LayoutParams.WRAP_CONTENT;
+ widthInPixels = LayoutParams.MATCH_PARENT;
+ gravity = Gravity.CENTER;
+ imageDrawable = null;
+ inAnimationResId = 0;
+ outAnimationResId = 0;
+ imageResId = 0;
+ imageScaleType = ImageView.ScaleType.FIT_XY;
+ }
+
+ /**
+ * Set the durationInMilliseconds option of the {@link Crouton}.
+ *
+ * @param duration
+ * The durationInMilliseconds the crouton will be displayed
+ * {@link Crouton} in milliseconds.
+ * @return the {@link Builder}.
+ */
+ public Builder setDuration(int duration) {
+ this.durationInMilliseconds = duration;
+
+ return this;
+ }
+
+ /**
+ * Set the backgroundColorResourceId option of the {@link Crouton}.
+ *
+ * @param backgroundColorResourceId
+ * The backgroundColorResourceId's resource id.
+ * @return the {@link Builder}.
+ */
+ public Builder setBackgroundColor(int backgroundColorResourceId) {
+ this.backgroundColorResourceId = backgroundColorResourceId;
+
+ return this;
+ }
+
+ /**
+ * Set the backgroundColorResourceValue option of the {@link Crouton}.
+ *
+ * @param backgroundColorValue
+ * The backgroundColorResourceValue's e.g. 0xffff4444;
+ * @return the {@link Builder}.
+ */
+ public Builder setBackgroundColorValue(int backgroundColorValue) {
+ this.backgroundColorValue = backgroundColorValue;
+ return this;
+ }
+
+ /**
+ * Set the backgroundDrawableResourceId option for the {@link Crouton}.
+ *
+ * @param backgroundDrawableResourceId
+ * Resource ID of a backgroundDrawableResourceId image drawable.
+ * @return the {@link Builder}.
+ */
+ public Builder setBackgroundDrawable(int backgroundDrawableResourceId) {
+ this.backgroundDrawableResourceId = backgroundDrawableResourceId;
+
+ return this;
+ }
+
+ /**
+ * Set the heightInPixels option for the {@link Crouton}.
+ *
+ * @param height
+ * The height of the {@link Crouton} in pixel. Can also be
+ * {@link LayoutParams#MATCH_PARENT} or
+ * {@link LayoutParams#WRAP_CONTENT}.
+ * @return the {@link Builder}.
+ */
+ public Builder setHeight(int height) {
+ this.heightInPixels = height;
+
+ return this;
+ }
+
+ /**
+ * Set the resource id for the height option for the {@link Crouton}.
+ *
+ * @param heightDimensionResId
+ * Resource ID of a dimension for the height of the {@link Crouton}.
+ * @return the {@link Builder}.
+ */
+ public Builder setHeightDimensionResId(int heightDimensionResId) {
+ this.heightDimensionResId = heightDimensionResId;
+
+ return this;
+ }
+
+ /**
+ * Set the widthInPixels option for the {@link Crouton}.
+ *
+ * @param width
+ * The width of the {@link Crouton} in pixel. Can also be
+ * {@link LayoutParams#MATCH_PARENT} or
+ * {@link LayoutParams#WRAP_CONTENT}.
+ * @return the {@link Builder}.
+ */
+ public Builder setWidth(int width) {
+ this.widthInPixels = width;
+
+ return this;
+ }
+
+ /**
+ * Set the resource id for the width option for the {@link Crouton}.
+ *
+ * @param widthDimensionResId
+ * Resource ID of a dimension for the width of the {@link Crouton}.
+ * @return the {@link Builder}.
+ */
+ public Builder setWidthDimensionResId(int widthDimensionResId) {
+ this.widthDimensionResId = widthDimensionResId;
+
+ return this;
+ }
+
+ /**
+ * Set the isTileEnabled option for the {@link Crouton}.
+ *
+ * @param isTileEnabled
+ * true
if you want the backgroundResourceId to be
+ * tiled, else false
.
+ * @return the {@link Builder}.
+ */
+ public Builder setTileEnabled(boolean isTileEnabled) {
+ this.isTileEnabled = isTileEnabled;
+
+ return this;
+ }
+
+ /**
+ * Set the textColorResourceId option for the {@link Crouton}.
+ *
+ * @param textColor
+ * The resource id of the text colorResourceId.
+ * @return the {@link Builder}.
+ */
+ public Builder setTextColor(int textColor) {
+ this.textColorResourceId = textColor;
+
+ return this;
+ }
+
+ /**
+ * Set the gravity option for the {@link Crouton}.
+ *
+ * @param gravity
+ * The text's gravity as provided by {@link Gravity}.
+ * @return the {@link Builder}.
+ */
+ public Builder setGravity(int gravity) {
+ this.gravity = gravity;
+
+ return this;
+ }
+
+ /**
+ * Set the image option for the {@link Crouton}.
+ *
+ * @param imageDrawable
+ * An additional image to display in the {@link Crouton}.
+ * @return the {@link Builder}.
+ */
+ public Builder setImageDrawable(Drawable imageDrawable) {
+ this.imageDrawable = imageDrawable;
+
+ return this;
+ }
+
+ /**
+ * Set the image resource option for the {@link Crouton}.
+ *
+ * @param imageResId
+ * An additional image to display in the {@link Crouton}.
+ * @return the {@link Builder}.
+ */
+ public Builder setImageResource(int imageResId) {
+ this.imageResId = imageResId;
+
+ return this;
+ }
+
+ /**
+ * The text size in sp
+ */
+ public Builder setTextSize(int textSize) {
+ this.textSize = textSize;
+ return this;
+ }
+
+ /**
+ * The text shadow color's resource id
+ */
+ public Builder setTextShadowColor(int textShadowColorResId) {
+ this.textShadowColorResId = textShadowColorResId;
+ return this;
+ }
+
+ /**
+ * The text shadow radius
+ */
+ public Builder setTextShadowRadius(float textShadowRadius) {
+ this.textShadowRadius = textShadowRadius;
+ return this;
+ }
+
+ /**
+ * The text shadow horizontal offset
+ */
+ public Builder setTextShadowDx(float textShadowDx) {
+ this.textShadowDx = textShadowDx;
+ return this;
+ }
+
+ /**
+ * The text shadow vertical offset
+ */
+ public Builder setTextShadowDy(float textShadowDy) {
+ this.textShadowDy = textShadowDy;
+ return this;
+ }
+
+ /**
+ * The text appearance resource id for the text.
+ */
+ public Builder setTextAppearance(int textAppearanceResId) {
+ this.textAppearanceResId = textAppearanceResId;
+ return this;
+ }
+
+ /**
+ * The resource id for the in animation
+ */
+ public Builder setInAnimation(int inAnimationResId) {
+ this.inAnimationResId = inAnimationResId;
+ return this;
+ }
+
+ /**
+ * The resource id for the out animation
+ */
+ public Builder setOutAnimation(int outAnimationResId) {
+ this.outAnimationResId = outAnimationResId;
+ return this;
+ }
+
+ /**
+ * The {@link android.widget.ImageView.ScaleType} for the image
+ */
+ public Builder setImageScaleType(ImageView.ScaleType imageScaleType) {
+ this.imageScaleType = imageScaleType;
+ return this;
+ }
+
+ /**
+ * The padding for the crouton view's content in pixels
+ */
+ public Builder setPaddingInPixels(int padding) {
+ this.paddingInPixels = padding;
+ return this;
+ }
+
+ /**
+ * The resource id for the padding for the crouton view's content
+ */
+ public Builder setPaddingDimensionResId(int paddingResId) {
+ this.paddingDimensionResId = paddingResId;
+ return this;
+ }
+
+ /**
+ * @return a configured {@link Style} object.
+ */
+ public Style build() {
+ return new Style(this);
+ }
+ }
+}
diff --git a/external/Crouton/pom.xml b/external/Crouton/pom.xml
new file mode 100644
index 00000000..a56203e5
--- /dev/null
+++ b/external/Crouton/pom.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+ 4.0.0
+
+ Crouton Parent
+ crouton-parent
+ 1.7
+ de.keyboardsurfer.android.widget
+ pom
+
+
+
+ keyboardsurfer
+ Benjamin Weiss
+
+
+
+
+
+ The Apache Software License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ git@github.com:keyboardsurfer/Crouton.git
+ scm:git:git@github.com:keyboardsurfer/Crouton.git
+ scm:git:git@github.com:keyboardsurfer/Crouton.git
+
+
+
+ library
+ sample
+
+
+
+ UTF-8
+ 4.1.1.4
+ 16
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 2.5.1
+
+ 1.6
+ 1.6
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 2.9
+
+
+ com.jayway.maven.plugins.android.generation2
+ android-maven-plugin
+ 3.4.1
+ true
+
+
+ ${android.version.platform}
+
+ true
+ true
+
+
+
+
+
+
+
diff --git a/external/JakeWharton-ActionBarSherlock/library/AndroidManifest.xml b/external/JakeWharton-ActionBarSherlock/library/AndroidManifest.xml
index 7b8a8482..bd9d22d1 100644
--- a/external/JakeWharton-ActionBarSherlock/library/AndroidManifest.xml
+++ b/external/JakeWharton-ActionBarSherlock/library/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/external/JakeWharton-ActionBarSherlock/library/ant.properties b/external/JakeWharton-ActionBarSherlock/library/ant.properties
new file mode 100644
index 00000000..b0971e89
--- /dev/null
+++ b/external/JakeWharton-ActionBarSherlock/library/ant.properties
@@ -0,0 +1,17 @@
+# This file is used to override default values used by the Ant build system.
+#
+# This file must be checked into Version Control Systems, as it is
+# integral to the build system of your project.
+
+# This file is only used by the Ant script.
+
+# You can use this to override default values such as
+# 'source.dir' for the location of your java source folder and
+# 'out.dir' for the location of your output folder.
+
+# You can also use it define how the release builds are signed by declaring
+# the following properties:
+# 'key.store' for the location of your keystore and
+# 'key.alias' for the name of the key to use.
+# The password will be asked during the build when you use the 'release' target.
+
diff --git a/external/JakeWharton-ActionBarSherlock/library/build.xml b/external/JakeWharton-ActionBarSherlock/library/build.xml
new file mode 100644
index 00000000..56bc6529
--- /dev/null
+++ b/external/JakeWharton-ActionBarSherlock/library/build.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/external/JakeWharton-ActionBarSherlock/library/libs/android-support-v4.jar b/external/JakeWharton-ActionBarSherlock/library/libs/android-support-v4.jar
index 99e063b3..65ebaf8d 100644
Binary files a/external/JakeWharton-ActionBarSherlock/library/libs/android-support-v4.jar and b/external/JakeWharton-ActionBarSherlock/library/libs/android-support-v4.jar differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/local.properties b/external/JakeWharton-ActionBarSherlock/library/local.properties
new file mode 100644
index 00000000..47f704fe
--- /dev/null
+++ b/external/JakeWharton-ActionBarSherlock/library/local.properties
@@ -0,0 +1,10 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+
+# location of the SDK. This is only used by Ant
+# For customization when using a Version Control System, please read the
+# header note.
+sdk.dir=/home/eric/Dev/android-sdk
diff --git a/external/JakeWharton-ActionBarSherlock/library/proguard-project.txt b/external/JakeWharton-ActionBarSherlock/library/proguard-project.txt
new file mode 100644
index 00000000..f2fe1559
--- /dev/null
+++ b/external/JakeWharton-ActionBarSherlock/library/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/external/JakeWharton-ActionBarSherlock/library/project.properties b/external/JakeWharton-ActionBarSherlock/library/project.properties
index 616f300c..6905bdf0 100644
--- a/external/JakeWharton-ActionBarSherlock/library/project.properties
+++ b/external/JakeWharton-ActionBarSherlock/library/project.properties
@@ -9,4 +9,4 @@
android.library=true
# Project target.
-target=android-16
+target=android-18
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__dialog_full_holo_dark.9.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__dialog_full_holo_dark.9.png
deleted file mode 100644
index 79e56f52..00000000
Binary files a/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__dialog_full_holo_dark.9.png and /dev/null differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__dialog_full_holo_light.9.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__dialog_full_holo_light.9.png
deleted file mode 100644
index e029f210..00000000
Binary files a/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__dialog_full_holo_light.9.png and /dev/null differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__ic_commit_search_api_holo_dark.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__ic_commit_search_api_holo_dark.png
new file mode 100644
index 00000000..83f36a94
Binary files /dev/null and b/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__ic_commit_search_api_holo_dark.png differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__ic_commit_search_api_holo_light.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__ic_commit_search_api_holo_light.png
new file mode 100644
index 00000000..a3cc21e6
Binary files /dev/null and b/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__ic_commit_search_api_holo_light.png differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__ic_search.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__ic_search.png
index 4be72f10..bf8bd667 100644
Binary files a/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__ic_search.png and b/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__ic_search.png differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__toast_frame.9.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__toast_frame.9.png
new file mode 100644
index 00000000..ad2cb5a9
Binary files /dev/null and b/external/JakeWharton-ActionBarSherlock/library/res/drawable-hdpi/abs__toast_frame.9.png differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__dialog_full_holo_dark.9.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__dialog_full_holo_dark.9.png
deleted file mode 100644
index fb3660ea..00000000
Binary files a/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__dialog_full_holo_dark.9.png and /dev/null differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__dialog_full_holo_light.9.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__dialog_full_holo_light.9.png
deleted file mode 100644
index f18050ea..00000000
Binary files a/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__dialog_full_holo_light.9.png and /dev/null differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__ic_commit_search_api_holo_dark.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__ic_commit_search_api_holo_dark.png
new file mode 100644
index 00000000..844c99c2
Binary files /dev/null and b/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__ic_commit_search_api_holo_dark.png differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__ic_commit_search_api_holo_light.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__ic_commit_search_api_holo_light.png
new file mode 100644
index 00000000..86c170e9
Binary files /dev/null and b/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__ic_commit_search_api_holo_light.png differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__toast_frame.9.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__toast_frame.9.png
new file mode 100644
index 00000000..b9105dee
Binary files /dev/null and b/external/JakeWharton-ActionBarSherlock/library/res/drawable-mdpi/abs__toast_frame.9.png differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__dialog_full_holo_dark.9.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__dialog_full_holo_dark.9.png
deleted file mode 100644
index f4970ad1..00000000
Binary files a/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__dialog_full_holo_dark.9.png and /dev/null differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__dialog_full_holo_light.9.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__dialog_full_holo_light.9.png
deleted file mode 100644
index 172fc3b5..00000000
Binary files a/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__dialog_full_holo_light.9.png and /dev/null differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__ic_commit_search_api_holo_dark.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__ic_commit_search_api_holo_dark.png
new file mode 100644
index 00000000..d8faf900
Binary files /dev/null and b/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__ic_commit_search_api_holo_dark.png differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__ic_commit_search_api_holo_light.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__ic_commit_search_api_holo_light.png
new file mode 100644
index 00000000..e7c7280a
Binary files /dev/null and b/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__ic_commit_search_api_holo_light.png differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__toast_frame.9.png b/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__toast_frame.9.png
new file mode 100644
index 00000000..9f39a774
Binary files /dev/null and b/external/JakeWharton-ActionBarSherlock/library/res/drawable-xhdpi/abs__toast_frame.9.png differ
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/layout-xlarge/abs__screen_action_bar.xml b/external/JakeWharton-ActionBarSherlock/library/res/layout-xlarge/abs__screen_action_bar.xml
index 040df44a..ab4fa28d 100644
--- a/external/JakeWharton-ActionBarSherlock/library/res/layout-xlarge/abs__screen_action_bar.xml
+++ b/external/JakeWharton-ActionBarSherlock/library/res/layout-xlarge/abs__screen_action_bar.xml
@@ -21,6 +21,7 @@ This is an optimized layout for a screen with the Action Bar enabled.
+ android:fitsSystemWindows="true"
+ android:splitMotionEvents="false">
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__action_bar_tab_bar_view.xml b/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__action_bar_tab_bar_view.xml
index 0d51220c..9d8f759b 100644
--- a/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__action_bar_tab_bar_view.xml
+++ b/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__action_bar_tab_bar_view.xml
@@ -1,6 +1,6 @@
-
\ No newline at end of file
+/>
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__dialog_title_holo.xml b/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__dialog_title_holo.xml
deleted file mode 100644
index ab2b0ee6..00000000
--- a/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__dialog_title_holo.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__list_menu_item_layout.xml b/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__list_menu_item_layout.xml
deleted file mode 100644
index 147f36fe..00000000
--- a/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__list_menu_item_layout.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__screen_action_bar.xml b/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__screen_action_bar.xml
index 1fb82fe9..68dc7dfe 100644
--- a/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__screen_action_bar.xml
+++ b/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__screen_action_bar.xml
@@ -22,7 +22,8 @@ This is an optimized layout for a screen with the Action Bar enabled.
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
- android:fitsSystemWindows="true">
+ android:fitsSystemWindows="true"
+ android:splitMotionEvents="false">
+ android:fitsSystemWindows="true"
+ android:splitMotionEvents="false">
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__simple_dropdown_hint.xml b/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__simple_dropdown_hint.xml
index 8fc0eb12..0088e14a 100644
--- a/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__simple_dropdown_hint.xml
+++ b/external/JakeWharton-ActionBarSherlock/library/res/layout/abs__simple_dropdown_hint.xml
@@ -19,7 +19,7 @@
-->
9dip
- 64dip
+ 64dip
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/values-large/abs__dimens.xml b/external/JakeWharton-ActionBarSherlock/library/res/values-large/abs__dimens.xml
deleted file mode 100644
index 63b12f7f..00000000
--- a/external/JakeWharton-ActionBarSherlock/library/res/values-large/abs__dimens.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
- - 55%
-
- - 80%
-
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/values-sw600dp/abs__dimens.xml b/external/JakeWharton-ActionBarSherlock/library/res/values-sw600dp/abs__dimens.xml
index f6785381..175a47fe 100644
--- a/external/JakeWharton-ActionBarSherlock/library/res/values-sw600dp/abs__dimens.xml
+++ b/external/JakeWharton-ActionBarSherlock/library/res/values-sw600dp/abs__dimens.xml
@@ -34,5 +34,5 @@
5
- 64dip
+ 64dip
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/values-v14/abs__themes.xml b/external/JakeWharton-ActionBarSherlock/library/res/values-v14/abs__themes.xml
index 5fac1ab5..75e317b4 100644
--- a/external/JakeWharton-ActionBarSherlock/library/res/values-v14/abs__themes.xml
+++ b/external/JakeWharton-ActionBarSherlock/library/res/values-v14/abs__themes.xml
@@ -26,9 +26,4 @@
- false
- true
-
-
-
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/values/abs__attrs.xml b/external/JakeWharton-ActionBarSherlock/library/res/values/abs__attrs.xml
index 32631ca8..af5873d9 100644
--- a/external/JakeWharton-ActionBarSherlock/library/res/values/abs__attrs.xml
+++ b/external/JakeWharton-ActionBarSherlock/library/res/values/abs__attrs.xml
@@ -181,7 +181,7 @@
-
+
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/values/abs__colors.xml b/external/JakeWharton-ActionBarSherlock/library/res/values/abs__colors.xml
index 625c632f..b189e727 100644
--- a/external/JakeWharton-ActionBarSherlock/library/res/values/abs__colors.xml
+++ b/external/JakeWharton-ActionBarSherlock/library/res/values/abs__colors.xml
@@ -21,7 +21,4 @@
@color/abs__background_holo_dark
#ff4c4c4c
#ffb2b2b2
- @color/abs__bright_foreground_holo_light
- @color/abs__bright_foreground_holo_dark
- #ff33b5e5
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/values/abs__dimens.xml b/external/JakeWharton-ActionBarSherlock/library/res/values/abs__dimens.xml
index 831289e0..9ded2197 100644
--- a/external/JakeWharton-ActionBarSherlock/library/res/values/abs__dimens.xml
+++ b/external/JakeWharton-ActionBarSherlock/library/res/values/abs__dimens.xml
@@ -36,18 +36,6 @@
56dip
-
- 64dip
-
-
- - 65%
-
- - 95%
-
8dip
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/values/abs__strings.xml b/external/JakeWharton-ActionBarSherlock/library/res/values/abs__strings.xml
index 06a2a2af..cf023592 100644
--- a/external/JakeWharton-ActionBarSherlock/library/res/values/abs__strings.xml
+++ b/external/JakeWharton-ActionBarSherlock/library/res/values/abs__strings.xml
@@ -29,10 +29,6 @@
See all...
-
- Select activity
-
- Share with...
Choose an application
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/values/abs__styles.xml b/external/JakeWharton-ActionBarSherlock/library/res/values/abs__styles.xml
index 45a18c18..f86c1b82 100644
--- a/external/JakeWharton-ActionBarSherlock/library/res/values/abs__styles.xml
+++ b/external/JakeWharton-ActionBarSherlock/library/res/values/abs__styles.xml
@@ -302,25 +302,6 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/external/JakeWharton-ActionBarSherlock/library/res/values/abs__themes.xml b/external/JakeWharton-ActionBarSherlock/library/res/values/abs__themes.xml
index 634fa798..6215e048 100644
--- a/external/JakeWharton-ActionBarSherlock/library/res/values/abs__themes.xml
+++ b/external/JakeWharton-ActionBarSherlock/library/res/values/abs__themes.xml
@@ -9,8 +9,6 @@
-
@@ -182,58 +204,4 @@
- false
- true
-
-
-
-
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/android/support/v4/app/Watson.java b/external/JakeWharton-ActionBarSherlock/library/src/android/support/v4/app/Watson.java
index d93de4c6..ff72cdcf 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/android/support/v4/app/Watson.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/android/support/v4/app/Watson.java
@@ -6,15 +6,14 @@ import android.view.Window;
import com.actionbarsherlock.ActionBarSherlock.OnCreatePanelMenuListener;
import com.actionbarsherlock.ActionBarSherlock.OnMenuItemSelectedListener;
import com.actionbarsherlock.ActionBarSherlock.OnPreparePanelListener;
+import com.actionbarsherlock.BuildConfig;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
-
import java.util.ArrayList;
/** I'm in ur package. Stealing ur variables. */
public abstract class Watson extends FragmentActivity implements OnCreatePanelMenuListener, OnPreparePanelListener, OnMenuItemSelectedListener {
- private static final boolean DEBUG = false;
private static final String TAG = "Watson";
/** Fragment interface for menu creation callback. */
@@ -39,11 +38,11 @@ public abstract class Watson extends FragmentActivity implements OnCreatePanelMe
@Override
public boolean onCreatePanelMenu(int featureId, Menu menu) {
- if (DEBUG) Log.d(TAG, "[onCreatePanelMenu] featureId: " + featureId + ", menu: " + menu);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onCreatePanelMenu] featureId: " + featureId + ", menu: " + menu);
if (featureId == Window.FEATURE_OPTIONS_PANEL) {
boolean result = onCreateOptionsMenu(menu);
- if (DEBUG) Log.d(TAG, "[onCreatePanelMenu] activity create result: " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onCreatePanelMenu] activity create result: " + result);
MenuInflater inflater = getSupportMenuInflater();
boolean show = false;
@@ -73,10 +72,10 @@ public abstract class Watson extends FragmentActivity implements OnCreatePanelMe
mCreatedMenus = newMenus;
- if (DEBUG) Log.d(TAG, "[onCreatePanelMenu] fragments create result: " + show);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onCreatePanelMenu] fragments create result: " + show);
result |= show;
- if (DEBUG) Log.d(TAG, "[onCreatePanelMenu] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onCreatePanelMenu] returning " + result);
return result;
}
return false;
@@ -84,11 +83,11 @@ public abstract class Watson extends FragmentActivity implements OnCreatePanelMe
@Override
public boolean onPreparePanel(int featureId, View view, Menu menu) {
- if (DEBUG) Log.d(TAG, "[onPreparePanel] featureId: " + featureId + ", view: " + view + " menu: " + menu);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onPreparePanel] featureId: " + featureId + ", view: " + view + " menu: " + menu);
if (featureId == Window.FEATURE_OPTIONS_PANEL) {
boolean result = onPrepareOptionsMenu(menu);
- if (DEBUG) Log.d(TAG, "[onPreparePanel] activity prepare result: " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onPreparePanel] activity prepare result: " + result);
boolean show = false;
if (mFragments.mAdded != null) {
@@ -101,11 +100,11 @@ public abstract class Watson extends FragmentActivity implements OnCreatePanelMe
}
}
- if (DEBUG) Log.d(TAG, "[onPreparePanel] fragments prepare result: " + show);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onPreparePanel] fragments prepare result: " + show);
result |= show;
result &= menu.hasVisibleItems();
- if (DEBUG) Log.d(TAG, "[onPreparePanel] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onPreparePanel] returning " + result);
return result;
}
return false;
@@ -113,7 +112,7 @@ public abstract class Watson extends FragmentActivity implements OnCreatePanelMe
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
- if (DEBUG) Log.d(TAG, "[onMenuItemSelected] featureId: " + featureId + ", item: " + item);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onMenuItemSelected] featureId: " + featureId + ", item: " + item);
if (featureId == Window.FEATURE_OPTIONS_PANEL) {
if (onOptionsItemSelected(item)) {
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/ActionBarSherlock.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/ActionBarSherlock.java
index ab160f83..6d56f511 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/ActionBarSherlock.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/ActionBarSherlock.java
@@ -1,14 +1,5 @@
package com.actionbarsherlock;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.util.HashMap;
-import java.util.Iterator;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
@@ -27,6 +18,16 @@ import com.actionbarsherlock.view.ActionMode;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
/**
* Helper for implementing the action bar design pattern across all versions
@@ -40,7 +41,6 @@ import com.actionbarsherlock.view.MenuItem;
*/
public abstract class ActionBarSherlock {
protected static final String TAG = "ActionBarSherlock";
- protected static final boolean DEBUG = false;
private static final Class>[] CONSTRUCTOR_ARGS = new Class[] { Activity.class, int.class };
private static final HashMap> IMPLEMENTATIONS =
@@ -122,12 +122,12 @@ public abstract class ActionBarSherlock {
if (!implementationClass.isAnnotationPresent(Implementation.class)) {
throw new IllegalArgumentException("Class " + implementationClass.getSimpleName() + " is not annotated with @Implementation");
} else if (IMPLEMENTATIONS.containsValue(implementationClass)) {
- if (DEBUG) Log.w(TAG, "Class " + implementationClass.getSimpleName() + " already registered");
+ if (BuildConfig.DEBUG) Log.w(TAG, "Class " + implementationClass.getSimpleName() + " already registered");
return;
}
Implementation impl = implementationClass.getAnnotation(Implementation.class);
- if (DEBUG) Log.i(TAG, "Registering " + implementationClass.getSimpleName() + " with qualifier " + impl);
+ if (BuildConfig.DEBUG) Log.i(TAG, "Registering " + implementationClass.getSimpleName() + " with qualifier " + impl);
IMPLEMENTATIONS.put(impl, implementationClass);
}
@@ -223,7 +223,7 @@ public abstract class ActionBarSherlock {
throw new IllegalStateException("No implementations match configuration.");
}
Class extends ActionBarSherlock> impl = impls.values().iterator().next();
- if (DEBUG) Log.i(TAG, "Using implementation: " + impl.getSimpleName());
+ if (BuildConfig.DEBUG) Log.i(TAG, "Using implementation: " + impl.getSimpleName());
try {
Constructor extends ActionBarSherlock> ctor = impl.getConstructor(CONSTRUCTOR_ARGS);
@@ -253,7 +253,7 @@ public abstract class ActionBarSherlock {
protected ActionBarSherlock(Activity activity, int flags) {
- if (DEBUG) Log.d(TAG, "[] activity: " + activity + ", flags: " + flags);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[] activity: " + activity + ", flags: " + flags);
mActivity = activity;
mIsDelegate = (flags & FLAG_DELEGATE) != 0;
@@ -551,7 +551,7 @@ public abstract class ActionBarSherlock {
* @return {@code true} if menu creation should proceed.
*/
protected final boolean callbackCreateOptionsMenu(Menu menu) {
- if (DEBUG) Log.d(TAG, "[callbackCreateOptionsMenu] menu: " + menu);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[callbackCreateOptionsMenu] menu: " + menu);
boolean result = true;
if (mActivity instanceof OnCreatePanelMenuListener) {
@@ -562,7 +562,7 @@ public abstract class ActionBarSherlock {
result = listener.onCreateOptionsMenu(menu);
}
- if (DEBUG) Log.d(TAG, "[callbackCreateOptionsMenu] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[callbackCreateOptionsMenu] returning " + result);
return result;
}
@@ -572,7 +572,7 @@ public abstract class ActionBarSherlock {
* @return {@code true} if menu preparation should proceed.
*/
protected final boolean callbackPrepareOptionsMenu(Menu menu) {
- if (DEBUG) Log.d(TAG, "[callbackPrepareOptionsMenu] menu: " + menu);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[callbackPrepareOptionsMenu] menu: " + menu);
boolean result = true;
if (mActivity instanceof OnPreparePanelListener) {
@@ -583,7 +583,7 @@ public abstract class ActionBarSherlock {
result = listener.onPrepareOptionsMenu(menu);
}
- if (DEBUG) Log.d(TAG, "[callbackPrepareOptionsMenu] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[callbackPrepareOptionsMenu] returning " + result);
return result;
}
@@ -595,7 +595,7 @@ public abstract class ActionBarSherlock {
* @return {@code true} if the item selection was handled in the callback.
*/
protected final boolean callbackOptionsItemSelected(MenuItem item) {
- if (DEBUG) Log.d(TAG, "[callbackOptionsItemSelected] item: " + item.getTitleCondensed());
+ if (BuildConfig.DEBUG) Log.d(TAG, "[callbackOptionsItemSelected] item: " + item.getTitleCondensed());
boolean result = false;
if (mActivity instanceof OnMenuItemSelectedListener) {
@@ -606,7 +606,7 @@ public abstract class ActionBarSherlock {
result = listener.onOptionsItemSelected(item);
}
- if (DEBUG) Log.d(TAG, "[callbackOptionsItemSelected] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[callbackOptionsItemSelected] returning " + result);
return result;
}
@@ -666,7 +666,7 @@ public abstract class ActionBarSherlock {
* @param view The desired content to display.
*/
public void setContentView(View view) {
- if (DEBUG) Log.d(TAG, "[setContentView] view: " + view);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setContentView] view: " + view);
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@@ -698,7 +698,7 @@ public abstract class ActionBarSherlock {
* Change the title associated with this activity.
*/
public void setTitle(int resId) {
- if (DEBUG) Log.d(TAG, "[setTitle] resId: " + resId);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setTitle] resId: " + resId);
setTitle(mActivity.getString(resId));
}
@@ -767,7 +767,7 @@ public abstract class ActionBarSherlock {
* @return Menu inflater instance.
*/
public MenuInflater getMenuInflater() {
- if (DEBUG) Log.d(TAG, "[getMenuInflater]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[getMenuInflater]");
// Make sure that action views can get an appropriate theme.
if (mMenuInflater == null) {
@@ -791,4 +791,9 @@ public abstract class ActionBarSherlock {
* @see ActionMode
*/
public abstract ActionMode startActionMode(ActionMode.Callback callback);
+
+ /**
+ * Ensure that the action bar is attached.
+ */
+ public void ensureActionBar() {}
}
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockActivity.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockActivity.java
index 7b454364..48a92da6 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockActivity.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockActivity.java
@@ -243,6 +243,12 @@ public abstract class SherlockActivity extends Activity implements OnCreatePanel
getSherlock().requestFeature((int)featureId);
}
+ @Override
+ public View findViewById(int id) {
+ getSherlock().ensureActionBar();
+ return super.findViewById(id);
+ }
+
///////////////////////////////////////////////////////////////////////////
// Progress Indication
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockDialogFragment.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockDialogFragment.java
index a7c856bf..c139325c 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockDialogFragment.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockDialogFragment.java
@@ -8,9 +8,9 @@ import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
-import static com.actionbarsherlock.app.SherlockFragmentActivity.OnCreateOptionsMenuListener;
-import static com.actionbarsherlock.app.SherlockFragmentActivity.OnOptionsItemSelectedListener;
-import static com.actionbarsherlock.app.SherlockFragmentActivity.OnPrepareOptionsMenuListener;
+import static android.support.v4.app.Watson.OnCreateOptionsMenuListener;
+import static android.support.v4.app.Watson.OnOptionsItemSelectedListener;
+import static android.support.v4.app.Watson.OnPrepareOptionsMenuListener;
public class SherlockDialogFragment extends DialogFragment implements OnCreateOptionsMenuListener, OnPrepareOptionsMenuListener, OnOptionsItemSelectedListener {
private SherlockFragmentActivity mActivity;
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockExpandableListActivity.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockExpandableListActivity.java
index 078f9b0c..9f4e81d1 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockExpandableListActivity.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockExpandableListActivity.java
@@ -232,6 +232,12 @@ public abstract class SherlockExpandableListActivity extends ExpandableListActiv
getSherlock().requestFeature((int)featureId);
}
+ @Override
+ public View findViewById(int id) {
+ getSherlock().ensureActionBar();
+ return super.findViewById(id);
+ }
+
///////////////////////////////////////////////////////////////////////////
// Progress Indication
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockFragment.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockFragment.java
index 0f24e9c8..0bdd068a 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockFragment.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockFragment.java
@@ -8,9 +8,9 @@ import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
-import static com.actionbarsherlock.app.SherlockFragmentActivity.OnCreateOptionsMenuListener;
-import static com.actionbarsherlock.app.SherlockFragmentActivity.OnOptionsItemSelectedListener;
-import static com.actionbarsherlock.app.SherlockFragmentActivity.OnPrepareOptionsMenuListener;
+import static android.support.v4.app.Watson.OnCreateOptionsMenuListener;
+import static android.support.v4.app.Watson.OnOptionsItemSelectedListener;
+import static android.support.v4.app.Watson.OnPrepareOptionsMenuListener;
public class SherlockFragment extends Fragment implements OnCreateOptionsMenuListener, OnPrepareOptionsMenuListener, OnOptionsItemSelectedListener {
private SherlockFragmentActivity mActivity;
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockFragmentActivity.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockFragmentActivity.java
index 3d092f03..b09f05e4 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockFragmentActivity.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockFragmentActivity.java
@@ -9,6 +9,7 @@ import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import com.actionbarsherlock.ActionBarSherlock;
+import com.actionbarsherlock.BuildConfig;
import com.actionbarsherlock.view.ActionMode;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
@@ -19,7 +20,6 @@ import static com.actionbarsherlock.ActionBarSherlock.OnActionModeStartedListene
/** @see {@link android.support.v4.app.Watson} */
public class SherlockFragmentActivity extends Watson implements OnActionModeStartedListener, OnActionModeFinishedListener {
- private static final boolean DEBUG = false;
private static final String TAG = "SherlockFragmentActivity";
private ActionBarSherlock mSherlock;
@@ -139,33 +139,33 @@ public class SherlockFragmentActivity extends Watson implements OnActionModeStar
///////////////////////////////////////////////////////////////////////////
public MenuInflater getSupportMenuInflater() {
- if (DEBUG) Log.d(TAG, "[getSupportMenuInflater]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[getSupportMenuInflater]");
return getSherlock().getMenuInflater();
}
public void invalidateOptionsMenu() {
- if (DEBUG) Log.d(TAG, "[invalidateOptionsMenu]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[invalidateOptionsMenu]");
getSherlock().dispatchInvalidateOptionsMenu();
}
public void supportInvalidateOptionsMenu() {
- if (DEBUG) Log.d(TAG, "[supportInvalidateOptionsMenu]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[supportInvalidateOptionsMenu]");
invalidateOptionsMenu();
}
@Override
public final boolean onCreatePanelMenu(int featureId, android.view.Menu menu) {
- if (DEBUG) Log.d(TAG, "[onCreatePanelMenu] featureId: " + featureId + ", menu: " + menu);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onCreatePanelMenu] featureId: " + featureId + ", menu: " + menu);
if (featureId == Window.FEATURE_OPTIONS_PANEL && !mIgnoreNativeCreate) {
mIgnoreNativeCreate = true;
boolean result = getSherlock().dispatchCreateOptionsMenu(menu);
mIgnoreNativeCreate = false;
- if (DEBUG) Log.d(TAG, "[onCreatePanelMenu] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onCreatePanelMenu] returning " + result);
return result;
}
return super.onCreatePanelMenu(featureId, menu);
@@ -178,14 +178,14 @@ public class SherlockFragmentActivity extends Watson implements OnActionModeStar
@Override
public final boolean onPreparePanel(int featureId, View view, android.view.Menu menu) {
- if (DEBUG) Log.d(TAG, "[onPreparePanel] featureId: " + featureId + ", view: " + view + ", menu: " + menu);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onPreparePanel] featureId: " + featureId + ", view: " + view + ", menu: " + menu);
if (featureId == Window.FEATURE_OPTIONS_PANEL && !mIgnoreNativePrepare) {
mIgnoreNativePrepare = true;
boolean result = getSherlock().dispatchPrepareOptionsMenu(menu);
mIgnoreNativePrepare = false;
- if (DEBUG) Log.d(TAG, "[onPreparePanel] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onPreparePanel] returning " + result);
return result;
}
return super.onPreparePanel(featureId, view, menu);
@@ -198,14 +198,14 @@ public class SherlockFragmentActivity extends Watson implements OnActionModeStar
@Override
public final boolean onMenuItemSelected(int featureId, android.view.MenuItem item) {
- if (DEBUG) Log.d(TAG, "[onMenuItemSelected] featureId: " + featureId + ", item: " + item);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onMenuItemSelected] featureId: " + featureId + ", item: " + item);
if (featureId == Window.FEATURE_OPTIONS_PANEL && !mIgnoreNativeSelected) {
mIgnoreNativeSelected = true;
boolean result = getSherlock().dispatchOptionsItemSelected(item);
mIgnoreNativeSelected = false;
- if (DEBUG) Log.d(TAG, "[onMenuItemSelected] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[onMenuItemSelected] returning " + result);
return result;
}
return super.onMenuItemSelected(featureId, item);
@@ -276,6 +276,12 @@ public class SherlockFragmentActivity extends Watson implements OnActionModeStar
getSherlock().requestFeature((int)featureId);
}
+ @Override
+ public View findViewById(int id) {
+ getSherlock().ensureActionBar();
+ return super.findViewById(id);
+ }
+
///////////////////////////////////////////////////////////////////////////
// Progress Indication
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockListActivity.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockListActivity.java
index aba6d85e..7ced7812 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockListActivity.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockListActivity.java
@@ -243,6 +243,12 @@ public abstract class SherlockListActivity extends ListActivity implements OnCre
getSherlock().requestFeature((int)featureId);
}
+ @Override
+ public View findViewById(int id) {
+ getSherlock().ensureActionBar();
+ return super.findViewById(id);
+ }
+
///////////////////////////////////////////////////////////////////////////
// Progress Indication
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockListFragment.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockListFragment.java
index 13ca3c49..7c4d0951 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockListFragment.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockListFragment.java
@@ -8,9 +8,9 @@ import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
-import static com.actionbarsherlock.app.SherlockFragmentActivity.OnCreateOptionsMenuListener;
-import static com.actionbarsherlock.app.SherlockFragmentActivity.OnOptionsItemSelectedListener;
-import static com.actionbarsherlock.app.SherlockFragmentActivity.OnPrepareOptionsMenuListener;
+import static android.support.v4.app.Watson.OnCreateOptionsMenuListener;
+import static android.support.v4.app.Watson.OnOptionsItemSelectedListener;
+import static android.support.v4.app.Watson.OnPrepareOptionsMenuListener;
public class SherlockListFragment extends ListFragment implements OnCreateOptionsMenuListener, OnPrepareOptionsMenuListener, OnOptionsItemSelectedListener {
private SherlockFragmentActivity mActivity;
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockPreferenceActivity.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockPreferenceActivity.java
index bee72cb2..85c6ab21 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockPreferenceActivity.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/app/SherlockPreferenceActivity.java
@@ -243,6 +243,12 @@ public abstract class SherlockPreferenceActivity extends PreferenceActivity impl
getSherlock().requestFeature((int)featureId);
}
+ @Override
+ public View findViewById(int id) {
+ getSherlock().ensureActionBar();
+ return super.findViewById(id);
+ }
+
///////////////////////////////////////////////////////////////////////////
// Progress Indication
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/ActionBarSherlockCompat.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/ActionBarSherlockCompat.java
index 5e69275c..421c94bd 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/ActionBarSherlockCompat.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/ActionBarSherlockCompat.java
@@ -1,11 +1,5 @@
package com.actionbarsherlock.internal;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static com.actionbarsherlock.internal.ResourcesCompat.getResources_getBoolean;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import org.xmlpull.v1.XmlPullParser;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
@@ -28,9 +22,8 @@ import android.view.Window;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
-import android.widget.FrameLayout;
-import android.widget.TextView;
import com.actionbarsherlock.ActionBarSherlock;
+import com.actionbarsherlock.BuildConfig;
import com.actionbarsherlock.R;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.internal.app.ActionBarImpl;
@@ -46,6 +39,13 @@ import com.actionbarsherlock.internal.widget.IcsProgressBar;
import com.actionbarsherlock.view.ActionMode;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import org.xmlpull.v1.XmlPullParser;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static com.actionbarsherlock.internal.ResourcesCompat.getResources_getBoolean;
@ActionBarSherlock.Implementation(api = 7)
public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBuilder.Callback, com.actionbarsherlock.view.Window.Callback, MenuPresenter.Callback, android.view.MenuItem.OnMenuItemClickListener {
@@ -108,13 +108,6 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
/** Parent view in which the context action bar is displayed. */
private ActionBarContextView mActionModeView;
- /** Title view used with dialogs. */
- private TextView mTitleView;
- /** Current activity title. */
- private CharSequence mTitle = null;
- /** Whether or not this "activity" is floating (i.e., a dialog) */
- private boolean mIsFloating;
-
///////////////////////////////////////////////////////////////////////////
@@ -123,14 +116,14 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public ActionBar getActionBar() {
- if (DEBUG) Log.d(TAG, "[getActionBar]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[getActionBar]");
initActionBar();
return aActionBar;
}
private void initActionBar() {
- if (DEBUG) Log.d(TAG, "[initActionBar]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[initActionBar]");
// Initializing the window decor can change window feature flags.
// Make sure that we have the correct set before performing the test below.
@@ -157,14 +150,14 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void setTitle(CharSequence title) {
- if (DEBUG) Log.d(TAG, "[setTitle] title: " + title);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setTitle] title: " + title);
dispatchTitleChanged(title, 0);
}
@Override
public ActionMode startActionMode(ActionMode.Callback callback) {
- if (DEBUG) Log.d(TAG, "[startActionMode] callback: " + callback);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[startActionMode] callback: " + callback);
if (mActionMode != null) {
mActionMode.finish();
@@ -215,7 +208,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void dispatchConfigurationChanged(Configuration newConfig) {
- if (DEBUG) Log.d(TAG, "[dispatchConfigurationChanged] newConfig: " + newConfig);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchConfigurationChanged] newConfig: " + newConfig);
if (aActionBar != null) {
aActionBar.onConfigurationChanged(newConfig);
@@ -224,7 +217,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void dispatchPostResume() {
- if (DEBUG) Log.d(TAG, "[dispatchPostResume]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchPostResume]");
if (aActionBar != null) {
aActionBar.setShowHideAnimationEnabled(true);
@@ -233,7 +226,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void dispatchPause() {
- if (DEBUG) Log.d(TAG, "[dispatchPause]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchPause]");
if (wActionBar != null && wActionBar.isOverflowMenuShowing()) {
wActionBar.hideOverflowMenu();
@@ -242,7 +235,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void dispatchStop() {
- if (DEBUG) Log.d(TAG, "[dispatchStop]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchStop]");
if (aActionBar != null) {
aActionBar.setShowHideAnimationEnabled(false);
@@ -251,7 +244,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void dispatchInvalidateOptionsMenu() {
- if (DEBUG) Log.d(TAG, "[dispatchInvalidateOptionsMenu]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchInvalidateOptionsMenu]");
Bundle savedActionViewStates = null;
if (mMenu != null) {
@@ -275,7 +268,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public boolean dispatchOpenOptionsMenu() {
- if (DEBUG) Log.d(TAG, "[dispatchOpenOptionsMenu]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchOpenOptionsMenu]");
if (!isReservingOverflow()) {
return false;
@@ -286,7 +279,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public boolean dispatchCloseOptionsMenu() {
- if (DEBUG) Log.d(TAG, "[dispatchCloseOptionsMenu]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchCloseOptionsMenu]");
if (!isReservingOverflow()) {
return false;
@@ -300,7 +293,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void dispatchPostCreate(Bundle savedInstanceState) {
- if (DEBUG) Log.d(TAG, "[dispatchOnPostCreate]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchOnPostCreate]");
if (mIsDelegate) {
mIsTitleReady = true;
@@ -313,7 +306,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public boolean dispatchCreateOptionsMenu(android.view.Menu menu) {
- if (DEBUG) {
+ if (BuildConfig.DEBUG) {
Log.d(TAG, "[dispatchCreateOptionsMenu] android.view.Menu: " + menu);
Log.d(TAG, "[dispatchCreateOptionsMenu] returning true");
}
@@ -322,7 +315,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public boolean dispatchPrepareOptionsMenu(android.view.Menu menu) {
- if (DEBUG) Log.d(TAG, "[dispatchPrepareOptionsMenu] android.view.Menu: " + menu);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchPrepareOptionsMenu] android.view.Menu: " + menu);
if (mActionMode != null) {
return false;
@@ -348,7 +341,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
}
boolean result = mMenu.bindNativeOverflow(menu, this, mNativeItemMap);
- if (DEBUG) Log.d(TAG, "[dispatchPrepareOptionsMenu] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchPrepareOptionsMenu] returning " + result);
return result;
}
@@ -359,7 +352,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public boolean dispatchMenuOpened(int featureId, android.view.Menu menu) {
- if (DEBUG) Log.d(TAG, "[dispatchMenuOpened] featureId: " + featureId + ", menu: " + menu);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchMenuOpened] featureId: " + featureId + ", menu: " + menu);
if (featureId == Window.FEATURE_ACTION_BAR || featureId == Window.FEATURE_OPTIONS_PANEL) {
if (aActionBar != null) {
@@ -373,7 +366,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void dispatchPanelClosed(int featureId, android.view.Menu menu){
- if (DEBUG) Log.d(TAG, "[dispatchPanelClosed] featureId: " + featureId + ", menu: " + menu);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchPanelClosed] featureId: " + featureId + ", menu: " + menu);
if (featureId == Window.FEATURE_ACTION_BAR || featureId == Window.FEATURE_OPTIONS_PANEL) {
if (aActionBar != null) {
@@ -384,22 +377,16 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void dispatchTitleChanged(CharSequence title, int color) {
- if (DEBUG) Log.d(TAG, "[dispatchTitleChanged] title: " + title + ", color: " + color);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchTitleChanged] title: " + title + ", color: " + color);
- if (!mIsDelegate || mIsTitleReady) {
- if (mTitleView != null) {
- mTitleView.setText(title);
- } else if (wActionBar != null) {
- wActionBar.setWindowTitle(title);
- }
+ if ((!mIsDelegate || mIsTitleReady) && (wActionBar != null)) {
+ wActionBar.setWindowTitle(title);
}
-
- mTitle = title;
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
- if (DEBUG) Log.d(TAG, "[dispatchKeyEvent] event: " + event);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchKeyEvent] event: " + event);
final int keyCode = event.getKeyCode();
@@ -412,7 +399,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
if (action == KeyEvent.ACTION_UP) {
mActionMode.finish();
}
- if (DEBUG) Log.d(TAG, "[dispatchKeyEvent] returning true");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchKeyEvent] returning true");
return true;
}
@@ -421,12 +408,12 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
if (action == KeyEvent.ACTION_UP) {
wActionBar.collapseActionView();
}
- if (DEBUG) Log.d(TAG, "[dispatchKeyEvent] returning true");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchKeyEvent] returning true");
return true;
}
}
- if (DEBUG) Log.d(TAG, "[dispatchKeyEvent] returning false");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchKeyEvent] returning false");
return false;
}
@@ -596,7 +583,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public boolean onMenuItemClick(android.view.MenuItem item) {
- if (DEBUG) Log.d(TAG, "[mNativeItemListener.onMenuItemClick] item: " + item);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[mNativeItemListener.onMenuItemClick] item: " + item);
final MenuItemImpl sherlockItem = mNativeItemMap.get(item);
if (sherlockItem != null) {
@@ -620,7 +607,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void setProgressBarVisibility(boolean visible) {
- if (DEBUG) Log.d(TAG, "[setProgressBarVisibility] visible: " + visible);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setProgressBarVisibility] visible: " + visible);
setFeatureInt(Window.FEATURE_PROGRESS, visible ? Window.PROGRESS_VISIBILITY_ON :
Window.PROGRESS_VISIBILITY_OFF);
@@ -628,7 +615,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void setProgressBarIndeterminateVisibility(boolean visible) {
- if (DEBUG) Log.d(TAG, "[setProgressBarIndeterminateVisibility] visible: " + visible);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setProgressBarIndeterminateVisibility] visible: " + visible);
setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
visible ? Window.PROGRESS_VISIBILITY_ON : Window.PROGRESS_VISIBILITY_OFF);
@@ -636,7 +623,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void setProgressBarIndeterminate(boolean indeterminate) {
- if (DEBUG) Log.d(TAG, "[setProgressBarIndeterminate] indeterminate: " + indeterminate);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setProgressBarIndeterminate] indeterminate: " + indeterminate);
setFeatureInt(Window.FEATURE_PROGRESS,
indeterminate ? Window.PROGRESS_INDETERMINATE_ON : Window.PROGRESS_INDETERMINATE_OFF);
@@ -644,14 +631,14 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void setProgress(int progress) {
- if (DEBUG) Log.d(TAG, "[setProgress] progress: " + progress);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setProgress] progress: " + progress);
setFeatureInt(Window.FEATURE_PROGRESS, progress + Window.PROGRESS_START);
}
@Override
public void setSecondaryProgress(int secondaryProgress) {
- if (DEBUG) Log.d(TAG, "[setSecondaryProgress] secondaryProgress: " + secondaryProgress);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setSecondaryProgress] secondaryProgress: " + secondaryProgress);
setFeatureInt(Window.FEATURE_PROGRESS,
secondaryProgress + Window.PROGRESS_SECONDARY_START);
@@ -790,23 +777,23 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
///////////////////////////////////////////////////////////////////////////
private int getFeatures() {
- if (DEBUG) Log.d(TAG, "[getFeatures] returning " + mFeatures);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[getFeatures] returning " + mFeatures);
return mFeatures;
}
@Override
public boolean hasFeature(int featureId) {
- if (DEBUG) Log.d(TAG, "[hasFeature] featureId: " + featureId);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[hasFeature] featureId: " + featureId);
boolean result = (mFeatures & (1 << featureId)) != 0;
- if (DEBUG) Log.d(TAG, "[hasFeature] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[hasFeature] returning " + result);
return result;
}
@Override
public boolean requestFeature(int featureId) {
- if (DEBUG) Log.d(TAG, "[requestFeature] featureId: " + featureId);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[requestFeature] featureId: " + featureId);
if (mContentParent != null) {
throw new AndroidRuntimeException("requestFeature() must be called before adding content");
@@ -829,21 +816,21 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void setUiOptions(int uiOptions) {
- if (DEBUG) Log.d(TAG, "[setUiOptions] uiOptions: " + uiOptions);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setUiOptions] uiOptions: " + uiOptions);
mUiOptions = uiOptions;
}
@Override
public void setUiOptions(int uiOptions, int mask) {
- if (DEBUG) Log.d(TAG, "[setUiOptions] uiOptions: " + uiOptions + ", mask: " + mask);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setUiOptions] uiOptions: " + uiOptions + ", mask: " + mask);
mUiOptions = (mUiOptions & ~mask) | (uiOptions & mask);
}
@Override
public void setContentView(int layoutResId) {
- if (DEBUG) Log.d(TAG, "[setContentView] layoutResId: " + layoutResId);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setContentView] layoutResId: " + layoutResId);
if (mContentParent == null) {
installDecor();
@@ -862,7 +849,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
- if (DEBUG) Log.d(TAG, "[setContentView] view: " + view + ", params: " + params);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setContentView] view: " + view + ", params: " + params);
if (mContentParent == null) {
installDecor();
@@ -881,7 +868,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
@Override
public void addContentView(View view, ViewGroup.LayoutParams params) {
- if (DEBUG) Log.d(TAG, "[addContentView] view: " + view + ", params: " + params);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[addContentView] view: " + view + ", params: " + params);
if (mContentParent == null) {
installDecor();
@@ -892,7 +879,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
}
private void installDecor() {
- if (DEBUG) Log.d(TAG, "[installDecor]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[installDecor]");
if (mDecor == null) {
mDecor = (ViewGroup)mActivity.getWindow().getDecorView().findViewById(android.R.id.content);
@@ -921,84 +908,70 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
}
}
- mTitleView = (TextView)mDecor.findViewById(android.R.id.title);
- if (mTitleView != null) {
- if (hasFeature(Window.FEATURE_NO_TITLE)) {
- mTitleView.setVisibility(View.GONE);
- if (mContentParent instanceof FrameLayout) {
- ((FrameLayout)mContentParent).setForeground(null);
- }
- } else {
- mTitleView.setText(mTitle);
+ wActionBar = (ActionBarView)mDecor.findViewById(R.id.abs__action_bar);
+ if (wActionBar != null) {
+ wActionBar.setWindowCallback(this);
+ if (wActionBar.getTitle() == null) {
+ wActionBar.setWindowTitle(mActivity.getTitle());
+ }
+ if (hasFeature(Window.FEATURE_PROGRESS)) {
+ wActionBar.initProgress();
+ }
+ if (hasFeature(Window.FEATURE_INDETERMINATE_PROGRESS)) {
+ wActionBar.initIndeterminateProgress();
}
- } else {
- wActionBar = (ActionBarView)mDecor.findViewById(R.id.abs__action_bar);
- if (wActionBar != null) {
- wActionBar.setWindowCallback(this);
- if (wActionBar.getTitle() == null) {
- wActionBar.setWindowTitle(mActivity.getTitle());
- }
- if (hasFeature(Window.FEATURE_PROGRESS)) {
- wActionBar.initProgress();
- }
- if (hasFeature(Window.FEATURE_INDETERMINATE_PROGRESS)) {
- wActionBar.initIndeterminateProgress();
- }
- //Since we don't require onCreate dispatching, parse for uiOptions here
- int uiOptions = loadUiOptionsFromManifest(mActivity);
- if (uiOptions != 0) {
- mUiOptions = uiOptions;
- }
+ //Since we don't require onCreate dispatching, parse for uiOptions here
+ int uiOptions = loadUiOptionsFromManifest(mActivity);
+ if (uiOptions != 0) {
+ mUiOptions = uiOptions;
+ }
- boolean splitActionBar = false;
- final boolean splitWhenNarrow = (mUiOptions & ActivityInfo.UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW) != 0;
- if (splitWhenNarrow) {
- splitActionBar = getResources_getBoolean(mActivity, R.bool.abs__split_action_bar_is_narrow);
- } else {
- splitActionBar = mActivity.getTheme()
- .obtainStyledAttributes(R.styleable.SherlockTheme)
- .getBoolean(R.styleable.SherlockTheme_windowSplitActionBar, false);
- }
- final ActionBarContainer splitView = (ActionBarContainer)mDecor.findViewById(R.id.abs__split_action_bar);
- if (splitView != null) {
- wActionBar.setSplitView(splitView);
- wActionBar.setSplitActionBar(splitActionBar);
- wActionBar.setSplitWhenNarrow(splitWhenNarrow);
-
- mActionModeView = (ActionBarContextView)mDecor.findViewById(R.id.abs__action_context_bar);
- mActionModeView.setSplitView(splitView);
- mActionModeView.setSplitActionBar(splitActionBar);
- mActionModeView.setSplitWhenNarrow(splitWhenNarrow);
- } else if (splitActionBar) {
- Log.e(TAG, "Requested split action bar with incompatible window decor! Ignoring request.");
- }
+ boolean splitActionBar = false;
+ final boolean splitWhenNarrow = (mUiOptions & ActivityInfo.UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW) != 0;
+ if (splitWhenNarrow) {
+ splitActionBar = getResources_getBoolean(mActivity, R.bool.abs__split_action_bar_is_narrow);
+ } else {
+ splitActionBar = mActivity.getTheme()
+ .obtainStyledAttributes(R.styleable.SherlockTheme)
+ .getBoolean(R.styleable.SherlockTheme_windowSplitActionBar, false);
+ }
+ final ActionBarContainer splitView = (ActionBarContainer)mDecor.findViewById(R.id.abs__split_action_bar);
+ if (splitView != null) {
+ wActionBar.setSplitView(splitView);
+ wActionBar.setSplitActionBar(splitActionBar);
+ wActionBar.setSplitWhenNarrow(splitWhenNarrow);
+
+ mActionModeView = (ActionBarContextView)mDecor.findViewById(R.id.abs__action_context_bar);
+ mActionModeView.setSplitView(splitView);
+ mActionModeView.setSplitActionBar(splitActionBar);
+ mActionModeView.setSplitWhenNarrow(splitWhenNarrow);
+ } else if (splitActionBar) {
+ Log.e(TAG, "Requested split action bar with incompatible window decor! Ignoring request.");
+ }
- // Post the panel invalidate for later; avoid application onCreateOptionsMenu
- // being called in the middle of onCreate or similar.
- mDecor.post(new Runnable() {
- @Override
- public void run() {
- //Invalidate if the panel menu hasn't been created before this.
- if (!mIsDestroyed && !mActivity.isFinishing() && mMenu == null) {
- dispatchInvalidateOptionsMenu();
- }
+ // Post the panel invalidate for later; avoid application onCreateOptionsMenu
+ // being called in the middle of onCreate or similar.
+ mDecor.post(new Runnable() {
+ @Override
+ public void run() {
+ //Invalidate if the panel menu hasn't been created before this.
+ if (!mIsDestroyed && !mActivity.isFinishing() && mMenu == null) {
+ dispatchInvalidateOptionsMenu();
}
- });
- }
+ }
+ });
}
}
}
private ViewGroup generateLayout() {
- if (DEBUG) Log.d(TAG, "[generateLayout]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[generateLayout]");
// Apply data from current theme.
TypedArray a = mActivity.getTheme().obtainStyledAttributes(R.styleable.SherlockTheme);
- mIsFloating = a.getBoolean(R.styleable.SherlockTheme_android_windowIsFloating, false);
-
if (!a.hasValue(R.styleable.SherlockTheme_windowActionBar)) {
throw new IllegalStateException("You must use Theme.Sherlock, Theme.Sherlock.Light, Theme.Sherlock.Light.DarkActionBar, or a derivative.");
}
@@ -1022,18 +995,10 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
int layoutResource;
if (!hasFeature(Window.FEATURE_NO_TITLE)) {
- if (mIsFloating) {
- //Trash original dialog LinearLayout
- mDecor = (ViewGroup)mDecor.getParent();
- mDecor.removeAllViews();
-
- layoutResource = R.layout.abs__dialog_title_holo;
+ if (hasFeature(Window.FEATURE_ACTION_BAR_OVERLAY)) {
+ layoutResource = R.layout.abs__screen_action_bar_overlay;
} else {
- if (hasFeature(Window.FEATURE_ACTION_BAR_OVERLAY)) {
- layoutResource = R.layout.abs__screen_action_bar_overlay;
- } else {
- layoutResource = R.layout.abs__screen_action_bar;
- }
+ layoutResource = R.layout.abs__screen_action_bar;
}
} else if (hasFeature(Window.FEATURE_ACTION_MODE_OVERLAY) && !hasFeature(Window.FEATURE_NO_TITLE)) {
layoutResource = R.layout.abs__screen_simple_overlay_action_mode;
@@ -1041,7 +1006,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
layoutResource = R.layout.abs__screen_simple;
}
- if (DEBUG) Log.d(TAG, "[generateLayout] using screen XML " + mActivity.getResources().getString(layoutResource));
+ if (BuildConfig.DEBUG) Log.d(TAG, "[generateLayout] using screen XML " + mActivity.getResources().getString(layoutResource));
View in = mActivity.getLayoutInflater().inflate(layoutResource, null);
mDecor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
@@ -1086,7 +1051,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
int uiOptions = 0;
try {
final String thisPackage = activity.getClass().getName();
- if (DEBUG) Log.i(TAG, "Parsing AndroidManifest.xml for " + thisPackage);
+ if (BuildConfig.DEBUG) Log.i(TAG, "Parsing AndroidManifest.xml for " + thisPackage);
final String packageName = activity.getApplicationInfo().packageName;
final AssetManager am = activity.createPackageContext(packageName, 0).getAssets();
@@ -1099,10 +1064,10 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
if ("application".equals(name)) {
//Check if the has the attribute
- if (DEBUG) Log.d(TAG, "Got ");
+ if (BuildConfig.DEBUG) Log.d(TAG, "Got ");
for (int i = xml.getAttributeCount() - 1; i >= 0; i--) {
- if (DEBUG) Log.d(TAG, xml.getAttributeName(i) + ": " + xml.getAttributeValue(i));
+ if (BuildConfig.DEBUG) Log.d(TAG, xml.getAttributeName(i) + ": " + xml.getAttributeValue(i));
if ("uiOptions".equals(xml.getAttributeName(i))) {
uiOptions = xml.getAttributeIntValue(i, 0);
@@ -1111,13 +1076,13 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
}
} else if ("activity".equals(name)) {
//Check if the is us and has the attribute
- if (DEBUG) Log.d(TAG, "Got ");
+ if (BuildConfig.DEBUG) Log.d(TAG, "Got ");
Integer activityUiOptions = null;
String activityPackage = null;
boolean isOurActivity = false;
for (int i = xml.getAttributeCount() - 1; i >= 0; i--) {
- if (DEBUG) Log.d(TAG, xml.getAttributeName(i) + ": " + xml.getAttributeValue(i));
+ if (BuildConfig.DEBUG) Log.d(TAG, xml.getAttributeName(i) + ": " + xml.getAttributeValue(i));
//We need both uiOptions and name attributes
String attrName = xml.getAttributeName(i);
@@ -1149,7 +1114,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
} catch (Exception e) {
e.printStackTrace();
}
- if (DEBUG) Log.i(TAG, "Returning " + Integer.toHexString(uiOptions));
+ if (BuildConfig.DEBUG) Log.i(TAG, "Returning " + Integer.toHexString(uiOptions));
return uiOptions;
}
@@ -1200,4 +1165,13 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu
mActionMode = null;
}
}
+
+ @Override
+ public void ensureActionBar() {
+ if (BuildConfig.DEBUG) Log.d(TAG, "[ensureActionBar]");
+
+ if (mDecor == null) {
+ initActionBar();
+ }
+ }
}
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/ActionBarSherlockNative.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/ActionBarSherlockNative.java
index 0824d384..4f67b9fa 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/ActionBarSherlockNative.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/ActionBarSherlockNative.java
@@ -1,19 +1,22 @@
package com.actionbarsherlock.internal;
-import com.actionbarsherlock.ActionBarSherlock;
-import com.actionbarsherlock.app.ActionBar;
-import com.actionbarsherlock.internal.app.ActionBarWrapper;
-import com.actionbarsherlock.internal.view.menu.MenuWrapper;
-import com.actionbarsherlock.view.ActionMode;
-import com.actionbarsherlock.view.MenuInflater;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import android.view.View;
-import android.view.Window;
import android.view.ViewGroup.LayoutParams;
+import android.view.Window;
+import com.actionbarsherlock.ActionBarSherlock;
+import com.actionbarsherlock.BuildConfig;
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.internal.app.ActionBarWrapper;
+import com.actionbarsherlock.internal.view.menu.MenuItemWrapper;
+import com.actionbarsherlock.internal.view.menu.MenuWrapper;
+import com.actionbarsherlock.view.ActionMode;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
@ActionBarSherlock.Implementation(api = 14)
public class ActionBarSherlockNative extends ActionBarSherlock {
@@ -28,7 +31,7 @@ public class ActionBarSherlockNative extends ActionBarSherlock {
@Override
public ActionBar getActionBar() {
- if (DEBUG) Log.d(TAG, "[getActionBar]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[getActionBar]");
initActionBar();
return mActionBar;
@@ -44,77 +47,89 @@ public class ActionBarSherlockNative extends ActionBarSherlock {
@Override
public void dispatchInvalidateOptionsMenu() {
- if (DEBUG) Log.d(TAG, "[dispatchInvalidateOptionsMenu]");
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchInvalidateOptionsMenu]");
mActivity.getWindow().invalidatePanelMenu(Window.FEATURE_OPTIONS_PANEL);
+
+ if (mMenu != null) mMenu.invalidate();
}
@Override
public boolean dispatchCreateOptionsMenu(android.view.Menu menu) {
- if (DEBUG) Log.d(TAG, "[dispatchCreateOptionsMenu] menu: " + menu);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchCreateOptionsMenu] menu: " + menu);
if (mMenu == null || menu != mMenu.unwrap()) {
mMenu = new MenuWrapper(menu);
}
final boolean result = callbackCreateOptionsMenu(mMenu);
- if (DEBUG) Log.d(TAG, "[dispatchCreateOptionsMenu] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchCreateOptionsMenu] returning " + result);
return result;
}
@Override
public boolean dispatchPrepareOptionsMenu(android.view.Menu menu) {
- if (DEBUG) Log.d(TAG, "[dispatchPrepareOptionsMenu] menu: " + menu);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchPrepareOptionsMenu] menu: " + menu);
final boolean result = callbackPrepareOptionsMenu(mMenu);
- if (DEBUG) Log.d(TAG, "[dispatchPrepareOptionsMenu] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchPrepareOptionsMenu] returning " + result);
return result;
}
@Override
public boolean dispatchOptionsItemSelected(android.view.MenuItem item) {
- if (DEBUG) Log.d(TAG, "[dispatchOptionsItemSelected] item: " + item.getTitleCondensed());
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchOptionsItemSelected] item: " + item.getTitleCondensed());
- final boolean result = callbackOptionsItemSelected(mMenu.findItem(item));
- if (DEBUG) Log.d(TAG, "[dispatchOptionsItemSelected] returning " + result);
+ MenuItem wrapped;
+ if (mMenu == null) {
+ if (item.getItemId() != android.R.id.home) {
+ throw new IllegalStateException("Non-home action item clicked before onCreateOptionsMenu with ID " + item.getItemId());
+ }
+ // Create a throw-away wrapper for now.
+ wrapped = new MenuItemWrapper(item);
+ } else {
+ wrapped = mMenu.findItem(item);
+ }
+ final boolean result = callbackOptionsItemSelected(wrapped);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[dispatchOptionsItemSelected] returning " + result);
return result;
}
@Override
public boolean hasFeature(int feature) {
- if (DEBUG) Log.d(TAG, "[hasFeature] feature: " + feature);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[hasFeature] feature: " + feature);
final boolean result = mActivity.getWindow().hasFeature(feature);
- if (DEBUG) Log.d(TAG, "[hasFeature] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[hasFeature] returning " + result);
return result;
}
@Override
public boolean requestFeature(int featureId) {
- if (DEBUG) Log.d(TAG, "[requestFeature] featureId: " + featureId);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[requestFeature] featureId: " + featureId);
final boolean result = mActivity.getWindow().requestFeature(featureId);
- if (DEBUG) Log.d(TAG, "[requestFeature] returning " + result);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[requestFeature] returning " + result);
return result;
}
@Override
public void setUiOptions(int uiOptions) {
- if (DEBUG) Log.d(TAG, "[setUiOptions] uiOptions: " + uiOptions);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setUiOptions] uiOptions: " + uiOptions);
mActivity.getWindow().setUiOptions(uiOptions);
}
@Override
public void setUiOptions(int uiOptions, int mask) {
- if (DEBUG) Log.d(TAG, "[setUiOptions] uiOptions: " + uiOptions + ", mask: " + mask);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setUiOptions] uiOptions: " + uiOptions + ", mask: " + mask);
mActivity.getWindow().setUiOptions(uiOptions, mask);
}
@Override
public void setContentView(int layoutResId) {
- if (DEBUG) Log.d(TAG, "[setContentView] layoutResId: " + layoutResId);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setContentView] layoutResId: " + layoutResId);
mActivity.getWindow().setContentView(layoutResId);
initActionBar();
@@ -122,7 +137,7 @@ public class ActionBarSherlockNative extends ActionBarSherlock {
@Override
public void setContentView(View view, LayoutParams params) {
- if (DEBUG) Log.d(TAG, "[setContentView] view: " + view + ", params: " + params);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setContentView] view: " + view + ", params: " + params);
mActivity.getWindow().setContentView(view, params);
initActionBar();
@@ -130,7 +145,7 @@ public class ActionBarSherlockNative extends ActionBarSherlock {
@Override
public void addContentView(View view, LayoutParams params) {
- if (DEBUG) Log.d(TAG, "[addContentView] view: " + view + ", params: " + params);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[addContentView] view: " + view + ", params: " + params);
mActivity.getWindow().addContentView(view, params);
initActionBar();
@@ -138,42 +153,42 @@ public class ActionBarSherlockNative extends ActionBarSherlock {
@Override
public void setTitle(CharSequence title) {
- if (DEBUG) Log.d(TAG, "[setTitle] title: " + title);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setTitle] title: " + title);
mActivity.getWindow().setTitle(title);
}
@Override
public void setProgressBarVisibility(boolean visible) {
- if (DEBUG) Log.d(TAG, "[setProgressBarVisibility] visible: " + visible);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setProgressBarVisibility] visible: " + visible);
mActivity.setProgressBarVisibility(visible);
}
@Override
public void setProgressBarIndeterminateVisibility(boolean visible) {
- if (DEBUG) Log.d(TAG, "[setProgressBarIndeterminateVisibility] visible: " + visible);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setProgressBarIndeterminateVisibility] visible: " + visible);
mActivity.setProgressBarIndeterminateVisibility(visible);
}
@Override
public void setProgressBarIndeterminate(boolean indeterminate) {
- if (DEBUG) Log.d(TAG, "[setProgressBarIndeterminate] indeterminate: " + indeterminate);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setProgressBarIndeterminate] indeterminate: " + indeterminate);
mActivity.setProgressBarIndeterminate(indeterminate);
}
@Override
public void setProgress(int progress) {
- if (DEBUG) Log.d(TAG, "[setProgress] progress: " + progress);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setProgress] progress: " + progress);
mActivity.setProgress(progress);
}
@Override
public void setSecondaryProgress(int secondaryProgress) {
- if (DEBUG) Log.d(TAG, "[setSecondaryProgress] secondaryProgress: " + secondaryProgress);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[setSecondaryProgress] secondaryProgress: " + secondaryProgress);
mActivity.setSecondaryProgress(secondaryProgress);
}
@@ -194,7 +209,7 @@ public class ActionBarSherlockNative extends ActionBarSherlock {
@Override
public ActionMode startActionMode(com.actionbarsherlock.view.ActionMode.Callback callback) {
- if (DEBUG) Log.d(TAG, "[startActionMode] callback: " + callback);
+ if (BuildConfig.DEBUG) Log.d(TAG, "[startActionMode] callback: " + callback);
if (mActionMode != null) {
mActionMode.finish();
@@ -288,6 +303,7 @@ public class ActionBarSherlockNative extends ActionBarSherlock {
@Override
public void invalidate() {
mActionMode.invalidate();
+ if (mMenu != null) mMenu.invalidate();
}
@Override
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/ResourcesCompat.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/ResourcesCompat.java
index 8e1efe8c..55a86a87 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/ResourcesCompat.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/ResourcesCompat.java
@@ -1,11 +1,19 @@
package com.actionbarsherlock.internal;
+import android.app.Activity;
import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.XmlResourceParser;
import android.os.Build;
import android.util.DisplayMetrics;
+import android.util.Log;
+import com.actionbarsherlock.BuildConfig;
import com.actionbarsherlock.R;
+import org.xmlpull.v1.XmlPullParser;
public final class ResourcesCompat {
+ private static final String TAG = "ResourcesCompat";
+
//No instances
private ResourcesCompat() {}
@@ -92,4 +100,83 @@ public final class ResourcesCompat {
throw new IllegalArgumentException("Unknown integer resource ID " + id);
}
+
+ /**
+ * Attempt to programmatically load the logo from the manifest file of an
+ * activity by using an XML pull parser. This should allow us to read the
+ * logo attribute regardless of the platform it is being run on.
+ *
+ * @param activity Activity instance.
+ * @return Logo resource ID.
+ */
+ public static int loadLogoFromManifest(Activity activity) {
+ int logo = 0;
+ try {
+ final String thisPackage = activity.getClass().getName();
+ if (BuildConfig.DEBUG) Log.i(TAG, "Parsing AndroidManifest.xml for " + thisPackage);
+
+ final String packageName = activity.getApplicationInfo().packageName;
+ final AssetManager am = activity.createPackageContext(packageName, 0).getAssets();
+ final XmlResourceParser xml = am.openXmlResourceParser("AndroidManifest.xml");
+
+ int eventType = xml.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = xml.getName();
+
+ if ("application".equals(name)) {
+ //Check if the has the attribute
+ if (BuildConfig.DEBUG) Log.d(TAG, "Got ");
+
+ for (int i = xml.getAttributeCount() - 1; i >= 0; i--) {
+ if (BuildConfig.DEBUG) Log.d(TAG, xml.getAttributeName(i) + ": " + xml.getAttributeValue(i));
+
+ if ("logo".equals(xml.getAttributeName(i))) {
+ logo = xml.getAttributeResourceValue(i, 0);
+ break; //out of for loop
+ }
+ }
+ } else if ("activity".equals(name)) {
+ //Check if the is us and has the attribute
+ if (BuildConfig.DEBUG) Log.d(TAG, "Got ");
+ Integer activityLogo = null;
+ String activityPackage = null;
+ boolean isOurActivity = false;
+
+ for (int i = xml.getAttributeCount() - 1; i >= 0; i--) {
+ if (BuildConfig.DEBUG) Log.d(TAG, xml.getAttributeName(i) + ": " + xml.getAttributeValue(i));
+
+ //We need both uiOptions and name attributes
+ String attrName = xml.getAttributeName(i);
+ if ("logo".equals(attrName)) {
+ activityLogo = xml.getAttributeResourceValue(i, 0);
+ } else if ("name".equals(attrName)) {
+ activityPackage = ActionBarSherlockCompat.cleanActivityName(packageName, xml.getAttributeValue(i));
+ if (!thisPackage.equals(activityPackage)) {
+ break; //on to the next
+ }
+ isOurActivity = true;
+ }
+
+ //Make sure we have both attributes before processing
+ if ((activityLogo != null) && (activityPackage != null)) {
+ //Our activity, logo specified, override with our value
+ logo = activityLogo.intValue();
+ }
+ }
+ if (isOurActivity) {
+ //If we matched our activity but it had no logo don't
+ //do any more processing of the manifest
+ break;
+ }
+ }
+ }
+ eventType = xml.nextToken();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ if (BuildConfig.DEBUG) Log.i(TAG, "Returning " + Integer.toHexString(logo));
+ return logo;
+ }
}
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/app/ActionBarImpl.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/app/ActionBarImpl.java
index d022a246..81f7d800 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/app/ActionBarImpl.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/app/ActionBarImpl.java
@@ -170,7 +170,12 @@ public class ActionBarImpl extends ActionBar {
// Older apps get the home button interaction enabled by default.
// Newer apps need to enable it explicitly.
- setHomeButtonEnabled(mContext.getApplicationInfo().targetSdkVersion < 14);
+ boolean homeButtonEnabled = mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.ICE_CREAM_SANDWICH;
+
+ // If the homeAsUp display option is set, always enable the home button.
+ homeButtonEnabled |= (mActionView.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != 0;
+
+ setHomeButtonEnabled(homeButtonEnabled);
setHasEmbeddedTabs(getResources_getBoolean(mContext,
R.bool.abs__action_bar_embed_tabs));
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/app/ActionBarWrapper.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/app/ActionBarWrapper.java
index 840cb3d2..fe479d0b 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/app/ActionBarWrapper.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/app/ActionBarWrapper.java
@@ -26,6 +26,10 @@ public class ActionBarWrapper extends ActionBar implements android.app.ActionBar
mActionBar = activity.getActionBar();
if (mActionBar != null) {
mActionBar.addOnMenuVisibilityListener(this);
+
+ // Fixes issue #746
+ int displayOptions = mActionBar.getDisplayOptions();
+ mActionBar.setHomeButtonEnabled((displayOptions & DISPLAY_HOME_AS_UP) != 0);
}
}
@@ -132,11 +136,19 @@ public class ActionBarWrapper extends ActionBar implements android.app.ActionBar
@Override
public void setDisplayOptions(int options) {
mActionBar.setDisplayOptions(options);
+
+ // Fixes issue #746
+ mActionBar.setHomeButtonEnabled((options & DISPLAY_HOME_AS_UP) != 0);
}
@Override
public void setDisplayOptions(int options, int mask) {
mActionBar.setDisplayOptions(options, mask);
+
+ // Fixes issue #746
+ if ((mask & DISPLAY_HOME_AS_UP) != 0) {
+ mActionBar.setHomeButtonEnabled((options & DISPLAY_HOME_AS_UP) != 0);
+ }
}
@Override
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/nineoldandroids/animation/ValueAnimator.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/nineoldandroids/animation/ValueAnimator.java
index d8a12c68..7f0f93ef 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/nineoldandroids/animation/ValueAnimator.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/nineoldandroids/animation/ValueAnimator.java
@@ -582,6 +582,7 @@ public class ValueAnimator extends Animator {
* active animations to process.
*/
@Override
+ @SuppressWarnings("fallthrough")
public void handleMessage(Message msg) {
boolean callAgain = true;
ArrayList animations = sAnimations.get();
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/view/menu/ActionMenuItemView.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/view/menu/ActionMenuItemView.java
index dcb50f36..7d36c3b2 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/view/menu/ActionMenuItemView.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/view/menu/ActionMenuItemView.java
@@ -37,6 +37,7 @@ import com.actionbarsherlock.R;
import com.actionbarsherlock.internal.view.View_HasStateListenerSupport;
import com.actionbarsherlock.internal.view.View_OnAttachStateChangeListener;
import com.actionbarsherlock.internal.widget.CapitalizingButton;
+import com.actionbarsherlock.internal.widget.IcsToast;
import static com.actionbarsherlock.internal.ResourcesCompat.getResources_getBoolean;
@@ -263,7 +264,7 @@ public class ActionMenuItemView extends LinearLayout
final int midy = screenPos[1] + height / 2;
final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
- Toast cheatSheet = Toast.makeText(context, mItemData.getTitle(), Toast.LENGTH_SHORT);
+ Toast cheatSheet = IcsToast.makeText(context, mItemData.getTitle(), IcsToast.LENGTH_SHORT);
if (midy < displayFrame.height()) {
// Show along the top; follow action buttons
cheatSheet.setGravity(Gravity.TOP | Gravity.RIGHT,
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/view/menu/MenuBuilder.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/view/menu/MenuBuilder.java
index 10c74203..51bacd7b 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/view/menu/MenuBuilder.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/view/menu/MenuBuilder.java
@@ -354,11 +354,11 @@ public class MenuBuilder implements Menu {
SparseArray viewStates = states.getSparseParcelableArray(
getActionViewStatesKey());
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB && viewStates == null) {
- // Fixes Issue #652 with sdk <= 2.3.6
- return;
- }
-
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB && viewStates == null) {
+ //Fixes Issue #652 with sdk <= 2.3.6
+ return;
+ }
+
final int itemCount = size();
for (int i = 0; i < itemCount; i++) {
final MenuItem item = getItem(i);
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/view/menu/MenuWrapper.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/view/menu/MenuWrapper.java
index 3d4dd42f..4eb8d09f 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/view/menu/MenuWrapper.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/view/menu/MenuWrapper.java
@@ -94,11 +94,18 @@ public class MenuWrapper implements Menu {
@Override
public void removeItem(int id) {
+ mNativeMap.remove(mNativeMenu.findItem(id));
mNativeMenu.removeItem(id);
}
@Override
public void removeGroup(int groupId) {
+ for (int i = 0; i < mNativeMenu.size(); i++) {
+ final android.view.MenuItem item = mNativeMenu.getItem(i);
+ if (item.getGroupId() == groupId) {
+ mNativeMap.remove(item);
+ }
+ }
mNativeMenu.removeGroup(groupId);
}
@@ -108,6 +115,20 @@ public class MenuWrapper implements Menu {
mNativeMenu.clear();
}
+ public void invalidate() {
+ if (mNativeMap.isEmpty()) return;
+
+ final WeakHashMap menuMapCopy = new WeakHashMap(mNativeMap.size());
+
+ for (int i = 0; i < mNativeMenu.size(); i++) {
+ final android.view.MenuItem item = mNativeMenu.getItem(i);
+ menuMapCopy.put(item, mNativeMap.get(item));
+ }
+
+ mNativeMap.clear();
+ mNativeMap.putAll(menuMapCopy);
+ }
+
@Override
public void setGroupCheckable(int group, boolean checkable, boolean exclusive) {
mNativeMenu.setGroupCheckable(group, checkable, exclusive);
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/ActionBarContainer.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/ActionBarContainer.java
index 1d9c68b3..0889825c 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/ActionBarContainer.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/ActionBarContainer.java
@@ -18,7 +18,6 @@ package com.actionbarsherlock.internal.widget;
import android.content.Context;
import android.content.res.TypedArray;
-import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
@@ -27,7 +26,6 @@ import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-
import com.actionbarsherlock.R;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.internal.nineoldandroids.widget.NineFrameLayout;
@@ -65,12 +63,7 @@ public class ActionBarContainer extends NineFrameLayout {
//Fix for issue #379
if (mStackedBackground instanceof ColorDrawable && Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
- Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
- Canvas c = new Canvas(bitmap);
- mStackedBackground.draw(c);
- int color = bitmap.getPixel(0, 0);
- bitmap.recycle();
- mStackedBackground = new IcsColorDrawable(color);
+ mStackedBackground = new IcsColorDrawable((ColorDrawable) mStackedBackground);
}
if (getId() == R.id.abs__split_action_bar) {
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/ActionBarView.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/ActionBarView.java
index 4636de17..61e55b0a 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/ActionBarView.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/ActionBarView.java
@@ -16,16 +16,14 @@
package com.actionbarsherlock.internal.widget;
-import org.xmlpull.v1.XmlPullParser;
+import com.actionbarsherlock.internal.ResourcesCompat;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
@@ -49,7 +47,6 @@ import android.widget.TextView;
import com.actionbarsherlock.R;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.ActionBar.OnNavigationListener;
-import com.actionbarsherlock.internal.ActionBarSherlockCompat;
import com.actionbarsherlock.internal.view.menu.ActionMenuItem;
import com.actionbarsherlock.internal.view.menu.ActionMenuPresenter;
import com.actionbarsherlock.internal.view.menu.ActionMenuView;
@@ -70,7 +67,6 @@ import static com.actionbarsherlock.internal.ResourcesCompat.getResources_getBoo
*/
public class ActionBarView extends AbsActionBarView {
private static final String TAG = "ActionBarView";
- private static final boolean DEBUG = false;
/**
* Display options applied by default
@@ -190,7 +186,7 @@ public class ActionBarView extends AbsActionBarView {
if (context instanceof Activity) {
//Even though native methods existed in API 9 and 10 they don't work
//so just parse the manifest to look for the logo pre-Honeycomb
- final int resId = loadLogoFromManifest((Activity) context);
+ final int resId = ResourcesCompat.loadLogoFromManifest((Activity) context);
if (resId != 0) {
mLogo = context.getResources().getDrawable(resId);
}
@@ -265,85 +261,6 @@ public class ActionBarView extends AbsActionBarView {
mHomeLayout.setFocusable(true);
}
- /**
- * Attempt to programmatically load the logo from the manifest file of an
- * activity by using an XML pull parser. This should allow us to read the
- * logo attribute regardless of the platform it is being run on.
- *
- * @param activity Activity instance.
- * @return Logo resource ID.
- */
- private static int loadLogoFromManifest(Activity activity) {
- int logo = 0;
- try {
- final String thisPackage = activity.getClass().getName();
- if (DEBUG) Log.i(TAG, "Parsing AndroidManifest.xml for " + thisPackage);
-
- final String packageName = activity.getApplicationInfo().packageName;
- final AssetManager am = activity.createPackageContext(packageName, 0).getAssets();
- final XmlResourceParser xml = am.openXmlResourceParser("AndroidManifest.xml");
-
- int eventType = xml.getEventType();
- while (eventType != XmlPullParser.END_DOCUMENT) {
- if (eventType == XmlPullParser.START_TAG) {
- String name = xml.getName();
-
- if ("application".equals(name)) {
- //Check if the has the attribute
- if (DEBUG) Log.d(TAG, "Got ");
-
- for (int i = xml.getAttributeCount() - 1; i >= 0; i--) {
- if (DEBUG) Log.d(TAG, xml.getAttributeName(i) + ": " + xml.getAttributeValue(i));
-
- if ("logo".equals(xml.getAttributeName(i))) {
- logo = xml.getAttributeResourceValue(i, 0);
- break; //out of for loop
- }
- }
- } else if ("activity".equals(name)) {
- //Check if the is us and has the attribute
- if (DEBUG) Log.d(TAG, "Got ");
- Integer activityLogo = null;
- String activityPackage = null;
- boolean isOurActivity = false;
-
- for (int i = xml.getAttributeCount() - 1; i >= 0; i--) {
- if (DEBUG) Log.d(TAG, xml.getAttributeName(i) + ": " + xml.getAttributeValue(i));
-
- //We need both uiOptions and name attributes
- String attrName = xml.getAttributeName(i);
- if ("logo".equals(attrName)) {
- activityLogo = xml.getAttributeResourceValue(i, 0);
- } else if ("name".equals(attrName)) {
- activityPackage = ActionBarSherlockCompat.cleanActivityName(packageName, xml.getAttributeValue(i));
- if (!thisPackage.equals(activityPackage)) {
- break; //on to the next
- }
- isOurActivity = true;
- }
-
- //Make sure we have both attributes before processing
- if ((activityLogo != null) && (activityPackage != null)) {
- //Our activity, logo specified, override with our value
- logo = activityLogo.intValue();
- }
- }
- if (isOurActivity) {
- //If we matched our activity but it had no logo don't
- //do any more processing of the manifest
- break;
- }
- }
- }
- eventType = xml.nextToken();
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- if (DEBUG) Log.i(TAG, "Returning " + Integer.toHexString(logo));
- return logo;
- }
-
/*
* Must be public so we can dispatch pre-2.2 via ActionBarImpl.
*/
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/CapitalizingButton.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/CapitalizingButton.java
index fa3698f3..6a134a16 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/CapitalizingButton.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/CapitalizingButton.java
@@ -29,7 +29,12 @@ public class CapitalizingButton extends Button {
public void setTextCompat(CharSequence text) {
if (SANS_ICE_CREAM && mAllCaps && text != null) {
if (IS_GINGERBREAD) {
- setText(text.toString().toUpperCase(Locale.ROOT));
+ try {
+ setText(text.toString().toUpperCase(Locale.ROOT));
+ } catch (NoSuchFieldError e) {
+ //Some manufacturer broke Locale.ROOT. See #572.
+ setText(text.toString().toUpperCase());
+ }
} else {
setText(text.toString().toUpperCase());
}
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/FakeDialogPhoneWindow.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/FakeDialogPhoneWindow.java
deleted file mode 100644
index ad1b4f0a..00000000
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/FakeDialogPhoneWindow.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.actionbarsherlock.internal.widget;
-
-import static android.view.View.MeasureSpec.EXACTLY;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.TypedValue;
-import android.widget.LinearLayout;
-import com.actionbarsherlock.R;
-
-public class FakeDialogPhoneWindow extends LinearLayout {
- final TypedValue mMinWidthMajor = new TypedValue();
- final TypedValue mMinWidthMinor = new TypedValue();
-
- public FakeDialogPhoneWindow(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SherlockTheme);
-
- a.getValue(R.styleable.SherlockTheme_windowMinWidthMajor, mMinWidthMajor);
- a.getValue(R.styleable.SherlockTheme_windowMinWidthMinor, mMinWidthMinor);
-
- a.recycle();
- }
-
- /* Stolen from PhoneWindow */
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
- final boolean isPortrait = metrics.widthPixels < metrics.heightPixels;
-
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- int width = getMeasuredWidth();
- boolean measure = false;
-
- widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY);
-
- final TypedValue tv = isPortrait ? mMinWidthMinor : mMinWidthMajor;
-
- if (tv.type != TypedValue.TYPE_NULL) {
- final int min;
- if (tv.type == TypedValue.TYPE_DIMENSION) {
- min = (int)tv.getDimension(metrics);
- } else if (tv.type == TypedValue.TYPE_FRACTION) {
- min = (int)tv.getFraction(metrics.widthPixels, metrics.widthPixels);
- } else {
- min = 0;
- }
-
- if (width < min) {
- widthMeasureSpec = MeasureSpec.makeMeasureSpec(min, EXACTLY);
- measure = true;
- }
- }
-
- // TODO: Support height?
-
- if (measure) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
-}
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsColorDrawable.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsColorDrawable.java
index a78b3f71..3e022e63 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsColorDrawable.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsColorDrawable.java
@@ -1,8 +1,10 @@
package com.actionbarsherlock.internal.widget;
+import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
+import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
/**
@@ -12,6 +14,14 @@ public class IcsColorDrawable extends Drawable {
private int color;
private final Paint paint = new Paint();
+ public IcsColorDrawable(ColorDrawable drawable) {
+ Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(bitmap);
+ drawable.draw(c);
+ this.color = bitmap.getPixel(0, 0);
+ bitmap.recycle();
+ }
+
public IcsColorDrawable(int color) {
this.color = color;
}
@@ -26,7 +36,7 @@ public class IcsColorDrawable extends Drawable {
@Override
public void setAlpha(int alpha) {
if (alpha != (color >>> 24)) {
- color = (color & 0x00FFFFFF) & (alpha << 24);
+ color = (color & 0x00FFFFFF) | (alpha << 24);
invalidateSelf();
}
}
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsLinearLayout.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsLinearLayout.java
index 4947c41d..b7c6ff31 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsLinearLayout.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsLinearLayout.java
@@ -3,10 +3,11 @@ package com.actionbarsherlock.internal.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
+import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
+import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
-import android.widget.LinearLayout;
import com.actionbarsherlock.internal.nineoldandroids.widget.NineLinearLayout;
@@ -20,14 +21,12 @@ import com.actionbarsherlock.internal.nineoldandroids.widget.NineLinearLayout;
public class IcsLinearLayout extends NineLinearLayout {
private static final int[] R_styleable_LinearLayout = new int[] {
/* 0 */ android.R.attr.divider,
- /* 1 */ android.R.attr.measureWithLargestChild,
/* 2 */ android.R.attr.showDividers,
/* 3 */ android.R.attr.dividerPadding,
};
private static final int LinearLayout_divider = 0;
- private static final int LinearLayout_measureWithLargestChild = 1;
- private static final int LinearLayout_showDividers = 2;
- private static final int LinearLayout_dividerPadding = 3;
+ private static final int LinearLayout_showDividers = 1;
+ private static final int LinearLayout_dividerPadding = 2;
/**
* Don't show any dividers.
@@ -53,8 +52,6 @@ public class IcsLinearLayout extends NineLinearLayout {
private int mShowDividers;
private int mDividerPadding;
- private boolean mUseLargestChild;
-
public IcsLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -63,7 +60,6 @@ public class IcsLinearLayout extends NineLinearLayout {
setDividerDrawable(a.getDrawable(/*com.android.internal.R.styleable.*/LinearLayout_divider));
mShowDividers = a.getInt(/*com.android.internal.R.styleable.*/LinearLayout_showDividers, SHOW_DIVIDER_NONE);
mDividerPadding = a.getDimensionPixelSize(/*com.android.internal.R.styleable.*/LinearLayout_dividerPadding, 0);
- mUseLargestChild = a.getBoolean(/*com.android.internal.R.styleable.*/LinearLayout_measureWithLargestChild, false);
a.recycle();
}
@@ -100,6 +96,12 @@ public class IcsLinearLayout extends NineLinearLayout {
if (divider == mDivider) {
return;
}
+
+ //Fix for issue #379
+ if (divider instanceof ColorDrawable && Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ divider = new IcsColorDrawable((ColorDrawable) divider);
+ }
+
mDivider = divider;
if (divider != null) {
mDividerWidth = divider.getIntrinsicWidth();
@@ -275,136 +277,4 @@ public class IcsLinearLayout extends NineLinearLayout {
}
return false;
}
-
- /**
- * When true, all children with a weight will be considered having
- * the minimum size of the largest child. If false, all children are
- * measured normally.
- *
- * @return True to measure children with a weight using the minimum
- * size of the largest child, false otherwise.
- *
- * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild
- */
- public boolean isMeasureWithLargestChildEnabled() {
- return mUseLargestChild;
- }
-
- /**
- * When set to true, all children with a weight will be considered having
- * the minimum size of the largest child. If false, all children are
- * measured normally.
- *
- * Disabled by default.
- *
- * @param enabled True to measure children with a weight using the
- * minimum size of the largest child, false otherwise.
- *
- * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild
- */
- public void setMeasureWithLargestChildEnabled(boolean enabled) {
- mUseLargestChild = enabled;
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- if (mUseLargestChild) {
- final int orientation = getOrientation();
- switch (orientation) {
- case HORIZONTAL:
- useLargestChildHorizontal();
- break;
-
- case VERTICAL:
- useLargestChildVertical();
- break;
- }
- }
- }
-
- private void useLargestChildHorizontal() {
- final int childCount = getChildCount();
-
- // Find largest child width
- int largestChildWidth = 0;
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
- largestChildWidth = Math.max(child.getMeasuredWidth(), largestChildWidth);
- }
-
- int totalWidth = 0;
- // Re-measure childs
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
-
- if (child == null || child.getVisibility() == View.GONE) {
- continue;
- }
-
- final LinearLayout.LayoutParams lp =
- (LinearLayout.LayoutParams) child.getLayoutParams();
-
- float childExtra = lp.weight;
- if (childExtra > 0) {
- child.measure(
- MeasureSpec.makeMeasureSpec(largestChildWidth,
- MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(),
- MeasureSpec.EXACTLY));
- totalWidth += largestChildWidth;
-
- } else {
- totalWidth += child.getMeasuredWidth();
- }
-
- totalWidth += lp.leftMargin + lp.rightMargin;
- }
-
- totalWidth += getPaddingLeft() + getPaddingRight();
- setMeasuredDimension(totalWidth, getMeasuredHeight());
- }
-
- private void useLargestChildVertical() {
- final int childCount = getChildCount();
-
- // Find largest child width
- int largestChildHeight = 0;
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
- largestChildHeight = Math.max(child.getMeasuredHeight(), largestChildHeight);
- }
-
- int totalHeight = 0;
- // Re-measure childs
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
-
- if (child == null || child.getVisibility() == View.GONE) {
- continue;
- }
-
- final LinearLayout.LayoutParams lp =
- (LinearLayout.LayoutParams) child.getLayoutParams();
-
- float childExtra = lp.weight;
- if (childExtra > 0) {
- child.measure(
- MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
- MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(largestChildHeight,
- MeasureSpec.EXACTLY));
- totalHeight += largestChildHeight;
-
- } else {
- totalHeight += child.getMeasuredHeight();
- }
-
- totalHeight += lp.leftMargin + lp.rightMargin;
- }
-
- totalHeight += getPaddingLeft() + getPaddingRight();
- setMeasuredDimension(getMeasuredWidth(), totalHeight);
- }
}
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsListPopupWindow.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsListPopupWindow.java
index d13c6cea..5da83189 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsListPopupWindow.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsListPopupWindow.java
@@ -258,6 +258,23 @@ public class IcsListPopupWindow {
mPopup.setInputMethodMode(mode);
}
+ /**
+ * Set the selected position of the list.
+ * Only valid when {@link #isShowing()} == {@code true}.
+ *
+ * @param position List position to set as selected.
+ */
+ public void setSelection(int position) {
+ DropDownListView list = mDropDownList;
+ if (isShowing() && list != null) {
+ list.mListSelectionHidden = false;
+ list.setSelection(position);
+ if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) {
+ list.setItemChecked(position, true);
+ }
+ }
+ }
+
public void clearListSelection() {
final DropDownListView list = mDropDownList;
if (list != null) {
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsToast.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsToast.java
new file mode 100644
index 00000000..042648b2
--- /dev/null
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/IcsToast.java
@@ -0,0 +1,60 @@
+
+package com.actionbarsherlock.internal.widget;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.Gravity;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.actionbarsherlock.R;
+
+public class IcsToast extends Toast {
+ public static final int LENGTH_LONG = Toast.LENGTH_LONG;
+ public static final int LENGTH_SHORT = Toast.LENGTH_SHORT;
+ private static final String TAG = "Toast";
+
+ public static Toast makeText(Context context, CharSequence s, int duration) {
+ if (VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH) {
+ return Toast.makeText(context, s, duration);
+ }
+ IcsToast toast = new IcsToast(context);
+ toast.setDuration(duration);
+ TextView view = new TextView(context);
+ view.setText(s);
+ // Original AOSP using reference on @android:color/bright_foreground_dark
+ // bright_foreground_dark - reference on @android:color/background_light
+ // background_light - 0xffffffff
+ view.setTextColor(0xffffffff);
+ view.setGravity(Gravity.CENTER);
+ view.setBackgroundResource(R.drawable.abs__toast_frame);
+ toast.setView(view);
+ return toast;
+ }
+
+ public static Toast makeText(Context context, int resId, int duration) {
+ return makeText(context, context.getResources().getString(resId), duration);
+ }
+
+ public IcsToast(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void setText(CharSequence s) {
+ if (VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH) {
+ super.setText(s);
+ return;
+ }
+ if (getView() == null) {
+ return;
+ }
+ try {
+ ((TextView) getView()).setText(s);
+ } catch (ClassCastException e) {
+ Log.e(TAG, "This Toast was not created with IcsToast.makeText", e);
+ }
+ }
+}
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/ScrollingTabContainerView.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/ScrollingTabContainerView.java
index 48fb5d8b..eb178e0d 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/ScrollingTabContainerView.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/ScrollingTabContainerView.java
@@ -186,7 +186,7 @@ public class ScrollingTabContainerView extends NineHorizontalScrollView
}
private IcsLinearLayout createTabLayout() {
- final IcsLinearLayout tabLayout = (IcsLinearLayout) LayoutInflater.from(getContext())
+ final TabsLinearLayout tabLayout = (TabsLinearLayout) LayoutInflater.from(getContext())
.inflate(R.layout.abs__action_bar_tab_bar_view, null);
tabLayout.setMeasureWithLargestChildEnabled(true);
tabLayout.setLayoutParams(new LinearLayout.LayoutParams(
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/TabsLinearLayout.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/TabsLinearLayout.java
new file mode 100644
index 00000000..03d09b1f
--- /dev/null
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/internal/widget/TabsLinearLayout.java
@@ -0,0 +1,113 @@
+package com.actionbarsherlock.internal.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+
+public class TabsLinearLayout extends IcsLinearLayout {
+ private static final int[] R_styleable_LinearLayout = new int[] {
+ /* 0 */ android.R.attr.measureWithLargestChild,
+ };
+ private static final int LinearLayout_measureWithLargestChild = 0;
+
+ private boolean mUseLargestChild;
+
+ public TabsLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, /*com.android.internal.R.styleable.*/R_styleable_LinearLayout);
+ mUseLargestChild = a.getBoolean(/*com.android.internal.R.styleable.*/LinearLayout_measureWithLargestChild, false);
+
+ a.recycle();
+ }
+
+ /**
+ * When true, all children with a weight will be considered having
+ * the minimum size of the largest child. If false, all children are
+ * measured normally.
+ *
+ * @return True to measure children with a weight using the minimum
+ * size of the largest child, false otherwise.
+ *
+ * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild
+ */
+ public boolean isMeasureWithLargestChildEnabled() {
+ return mUseLargestChild;
+ }
+
+ /**
+ * When set to true, all children with a weight will be considered having
+ * the minimum size of the largest child. If false, all children are
+ * measured normally.
+ *
+ * Disabled by default.
+ *
+ * @param enabled True to measure children with a weight using the
+ * minimum size of the largest child, false otherwise.
+ *
+ * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild
+ */
+ public void setMeasureWithLargestChildEnabled(boolean enabled) {
+ mUseLargestChild = enabled;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ final int childCount = getChildCount();
+ if (childCount <= 2) return;
+
+ final int mode = MeasureSpec.getMode(widthMeasureSpec);
+ if (mUseLargestChild && mode == MeasureSpec.UNSPECIFIED) {
+ final int orientation = getOrientation();
+ if (orientation == HORIZONTAL) {
+ useLargestChildHorizontal();
+ }
+ }
+ }
+
+ private void useLargestChildHorizontal() {
+ final int childCount = getChildCount();
+
+ // Find largest child width
+ int largestChildWidth = 0;
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ largestChildWidth = Math.max(child.getMeasuredWidth(), largestChildWidth);
+ }
+
+ int totalWidth = 0;
+ // Re-measure childs
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ if (child == null || child.getVisibility() == View.GONE) {
+ continue;
+ }
+
+ final LinearLayout.LayoutParams lp =
+ (LinearLayout.LayoutParams) child.getLayoutParams();
+
+ float childExtra = lp.weight;
+ if (childExtra > 0) {
+ child.measure(
+ MeasureSpec.makeMeasureSpec(largestChildWidth,
+ MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(),
+ MeasureSpec.EXACTLY));
+ totalWidth += largestChildWidth;
+
+ } else {
+ totalWidth += child.getMeasuredWidth();
+ }
+
+ totalWidth += lp.leftMargin + lp.rightMargin;
+ }
+
+ totalWidth += getPaddingLeft() + getPaddingRight();
+ setMeasuredDimension(totalWidth, getMeasuredHeight());
+ }
+}
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/widget/ActivityChooserView.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/widget/ActivityChooserView.java
index e19ea9e9..7eb7330e 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/widget/ActivityChooserView.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/widget/ActivityChooserView.java
@@ -395,7 +395,11 @@ class ActivityChooserView extends ViewGroup implements ActivityChooserModelClien
super.onAttachedToWindow();
ActivityChooserModel dataModel = mAdapter.getDataModel();
if (dataModel != null) {
- dataModel.registerObserver(mModelDataSetOberver);
+ try {
+ dataModel.registerObserver(mModelDataSetOberver);
+ } catch (IllegalStateException e) {
+ // Related to #557.
+ }
}
mIsAttachedToWindow = true;
}
@@ -522,6 +526,9 @@ class ActivityChooserView extends ViewGroup implements ActivityChooserModelClien
mDefaultActionButtonContentDescription, label);
mDefaultActivityButton.setContentDescription(contentDescription);
}
+
+ // Work-around for #415.
+ mAdapter.setShowDefaultActivity(false, false);
} else {
mDefaultActivityButton.setVisibility(View.GONE);
}
@@ -644,7 +651,8 @@ class ActivityChooserView extends ViewGroup implements ActivityChooserModelClien
private int mMaxActivityCount = MAX_ACTIVITY_COUNT_DEFAULT;
- private boolean mShowDefaultActivity;
+ // Work-around for #415.
+ private boolean mShowDefaultActivity = true;
private boolean mHighlightDefaultActivity;
@@ -661,7 +669,11 @@ class ActivityChooserView extends ViewGroup implements ActivityChooserModelClien
}
mDataModel = dataModel;
if (dataModel != null && isShown()) {
- dataModel.registerObserver(mModelDataSetOberver);
+ try {
+ dataModel.registerObserver(mModelDataSetOberver);
+ } catch (IllegalStateException e) {
+ // Related to #557.
+ }
}
notifyDataSetChanged();
}
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/widget/SearchView.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/widget/SearchView.java
index c9e7897d..fb831964 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/widget/SearchView.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/widget/SearchView.java
@@ -1623,8 +1623,8 @@ public class SearchView extends LinearLayout implements CollapsibleActionView {
private void forceSuggestionQuery() {
try {
- Method before = SearchAutoComplete.class.getMethod("doBeforeTextChanged");
- Method after = SearchAutoComplete.class.getMethod("doAfterTextChanged");
+ Method before = AutoCompleteTextView.class.getDeclaredMethod("doBeforeTextChanged");
+ Method after = AutoCompleteTextView.class.getDeclaredMethod("doAfterTextChanged");
before.setAccessible(true);
after.setAccessible(true);
before.invoke(mQueryTextView);
diff --git a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/widget/SuggestionsAdapter.java b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/widget/SuggestionsAdapter.java
index bd5cbd71..82d4f0c4 100644
--- a/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/widget/SuggestionsAdapter.java
+++ b/external/JakeWharton-ActionBarSherlock/library/src/com/actionbarsherlock/widget/SuggestionsAdapter.java
@@ -66,6 +66,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListene
static final int REFINE_ALL = 2;
private SearchManager mSearchManager;
+ private SearchableInfo mSearchable;
private SearchView mSearchView;
private Context mProviderContext;
private WeakHashMap mOutsideDrawablesCache;
@@ -94,12 +95,13 @@ class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListene
//private static final long DELETE_KEY_POST_DELAY = 500L;
public SuggestionsAdapter(Context context, SearchView searchView,
- SearchableInfo mSearchable, WeakHashMap outsideDrawablesCache) {
+ SearchableInfo searchable, WeakHashMap outsideDrawablesCache) {
super(context,
R.layout.abs__search_dropdown_item_icons_2line,
null, // no initial cursor
true); // auto-requery
mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
+ mSearchable = searchable;
mProviderContext = mContext;
mSearchView = searchView;
@@ -199,25 +201,48 @@ class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListene
}
public Cursor getSuggestions(String query, int limit) {
+ if (mSearchable == null) {
+ return null;
+ }
+
+ String authority = mSearchable.getSuggestAuthority();
+ if (authority == null) {
+ return null;
+ }
+
Uri.Builder uriBuilder = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority)
.query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
.fragment(""); // TODO: Remove, workaround for a bug in Uri.writeToParcel()
+ // if content path provided, insert it now
+ final String contentPath = mSearchable.getSuggestPath();
+ if (contentPath != null) {
+ uriBuilder.appendEncodedPath(contentPath);
+ }
+
// append standard suggestion query path
uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
+ // get the query selection, may be null
+ String selection = mSearchable.getSuggestSelection();
// inject query, either as selection args or inline
- uriBuilder.appendPath(query);
+ String[] selArgs = null;
+ if (selection != null) { // use selection if provided
+ selArgs = new String[] { query };
+ } else { // no selection, use REST pattern
+ uriBuilder.appendPath(query);
+ }
if (limit > 0) {
- uriBuilder.appendQueryParameter(SearchManager.SUGGEST_PARAMETER_LIMIT, String.valueOf(limit));
+ uriBuilder.appendQueryParameter("limit", String.valueOf(limit));
}
Uri uri = uriBuilder.build();
// finally, make the query
- return mContext.getContentResolver().query(uri, null, null, null, null);
+ return mContext.getContentResolver().query(uri, null, selection, selArgs, null);
}
public void close() {
diff --git a/full/.classpath b/full/.classpath
new file mode 100644
index 00000000..7bc01d9a
--- /dev/null
+++ b/full/.classpath
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/full/.project b/full/.project
new file mode 100644
index 00000000..a91e6ae4
--- /dev/null
+++ b/full/.project
@@ -0,0 +1,33 @@
+
+
+ Transdroid Full
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/full/AndroidManifest.xml b/full/AndroidManifest.xml
new file mode 100644
index 00000000..c3a1ecbc
--- /dev/null
+++ b/full/AndroidManifest.xml
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/full/ant.properties b/full/ant.properties
new file mode 100644
index 00000000..73de9ff3
--- /dev/null
+++ b/full/ant.properties
@@ -0,0 +1,2 @@
+key.store=/home/eric/Dev/erickok.keystore
+key.alias=transdroid
diff --git a/full/build.xml b/full/build.xml
new file mode 100644
index 00000000..d107dd47
--- /dev/null
+++ b/full/build.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/full/local.properties b/full/local.properties
new file mode 100644
index 00000000..47f704fe
--- /dev/null
+++ b/full/local.properties
@@ -0,0 +1,10 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+
+# location of the SDK. This is only used by Ant
+# For customization when using a Version Control System, please read the
+# header note.
+sdk.dir=/home/eric/Dev/android-sdk
diff --git a/full/proguard-project.txt b/full/proguard-project.txt
new file mode 100644
index 00000000..f2fe1559
--- /dev/null
+++ b/full/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/full/project.properties b/full/project.properties
new file mode 100644
index 00000000..4bc32995
--- /dev/null
+++ b/full/project.properties
@@ -0,0 +1,15 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-18
+android.library.reference.1=../core
diff --git a/full/res/values/strings.xml b/full/res/values/strings.xml
new file mode 100644
index 00000000..a0681c47
--- /dev/null
+++ b/full/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ Transdroid
+
+
\ No newline at end of file
diff --git a/lib/.classpath b/lib/.classpath
index c927cda8..4970aef8 100644
--- a/lib/.classpath
+++ b/lib/.classpath
@@ -2,6 +2,6 @@
-
+
diff --git a/lib/src/org/transdroid/daemon/AlphanumComparator.java b/lib/src/org/transdroid/daemon/AlphanumComparator.java
new file mode 100644
index 00000000..d4db86dc
--- /dev/null
+++ b/lib/src/org/transdroid/daemon/AlphanumComparator.java
@@ -0,0 +1,107 @@
+package org.transdroid.daemon;
+
+/*
+ * The Alphanum Algorithm is an improved sorting algorithm for strings
+ * containing numbers. Instead of sorting numbers in ASCII order like
+ * a standard sort, this algorithm sorts numbers in numeric order.
+ *
+ * The Alphanum Algorithm is discussed at http://www.DaveKoelle.com
+ *
+ * This is an updated version with enhancements made by Daniel Migowski,
+ * Andre Bogus, and David Koelle
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+import java.util.Comparator;
+
+public class AlphanumComparator implements Comparator {
+
+ private final boolean isDigit(char ch) {
+ return ch >= 48 && ch <= 57;
+ }
+
+ /** Length of string is passed in for improved efficiency (only need to calculate it once) **/
+ private final String getChunk(String s, int slength, int marker) {
+ StringBuilder chunk = new StringBuilder();
+ char c = s.charAt(marker);
+ chunk.append(c);
+ marker++;
+ if (isDigit(c)) {
+ while (marker < slength) {
+ c = s.charAt(marker);
+ if (!isDigit(c))
+ break;
+ chunk.append(c);
+ marker++;
+ }
+ } else {
+ while (marker < slength) {
+ c = s.charAt(marker);
+ if (isDigit(c))
+ break;
+ chunk.append(c);
+ marker++;
+ }
+ }
+ return chunk.toString();
+ }
+
+ public int compare(String o1, String o2) {
+ if (!(o1 instanceof String) || !(o2 instanceof String)) {
+ return 0;
+ }
+ String s1 = (String) o1;
+ String s2 = (String) o2;
+
+ int thisMarker = 0;
+ int thatMarker = 0;
+ int s1Length = s1.length();
+ int s2Length = s2.length();
+
+ while (thisMarker < s1Length && thatMarker < s2Length) {
+ String thisChunk = getChunk(s1, s1Length, thisMarker);
+ thisMarker += thisChunk.length();
+
+ String thatChunk = getChunk(s2, s2Length, thatMarker);
+ thatMarker += thatChunk.length();
+
+ // If both chunks contain numeric characters, sort them numerically
+ int result = 0;
+ if (isDigit(thisChunk.charAt(0)) && isDigit(thatChunk.charAt(0))) {
+ // Simple chunk comparison by length.
+ int thisChunkLength = thisChunk.length();
+ result = thisChunkLength - thatChunk.length();
+ // If equal, the first different number counts
+ if (result == 0) {
+ for (int i = 0; i < thisChunkLength; i++) {
+ result = thisChunk.charAt(i) - thatChunk.charAt(i);
+ if (result != 0) {
+ return result;
+ }
+ }
+ }
+ } else {
+ result = thisChunk.compareTo(thatChunk);
+ }
+
+ if (result != 0)
+ return result;
+ }
+
+ return s1Length - s2Length;
+ }
+
+}
\ No newline at end of file
diff --git a/lib/src/org/transdroid/daemon/BitComet/BitCometAdapter.java b/lib/src/org/transdroid/daemon/BitComet/BitCometAdapter.java
index 732a4e8a..d5426cf0 100644
--- a/lib/src/org/transdroid/daemon/BitComet/BitCometAdapter.java
+++ b/lib/src/org/transdroid/daemon/BitComet/BitCometAdapter.java
@@ -253,7 +253,7 @@ public class BitCometAdapter implements IDaemonAdapter {
// Read HTTP response
java.io.InputStream instream = entity.getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
instream.close();
// Return raw result
@@ -288,7 +288,7 @@ public class BitCometAdapter implements IDaemonAdapter {
// Read BitComet response
java.io.InputStream instream = entity.getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
instream.close();
int idx = result.indexOf("save_path' value='")+18;
@@ -307,7 +307,7 @@ public class BitCometAdapter implements IDaemonAdapter {
if (entity != null) {
// Check BitComet response
instream = entity.getContent();
- result = HttpHelper.ConvertStreamToString(instream);
+ result = HttpHelper.convertStreamToString(instream);
instream.close();
if (result.indexOf("failed!") > 0) throw new Exception("Adding torrent file failed");
}
@@ -340,7 +340,7 @@ public class BitCometAdapter implements IDaemonAdapter {
// Read BitComet response
java.io.InputStream instream = entity.getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
instream.close();
int idx = result.indexOf("save_path' value='")+18;
@@ -364,7 +364,7 @@ public class BitCometAdapter implements IDaemonAdapter {
if (entity != null) {
// Check BitComet response
instream = entity.getContent();
- result = HttpHelper.ConvertStreamToString(instream);
+ result = HttpHelper.convertStreamToString(instream);
instream.close();
if (result.indexOf("failed!") > 0) {
throw new Exception("Adding URL failed");
@@ -480,7 +480,8 @@ public class BitCometAdapter implements IDaemonAdapter {
comment,
dateAdded,
null,
- null));
+ null,
+ settings.getType()));
}
}
}
@@ -557,7 +558,8 @@ public class BitCometAdapter implements IDaemonAdapter {
label,
dateAdded,
null,
- null)); // Not supported in the web interface
+ null, // Not supported in the web interface
+ settings.getType()));
id++; // Stop/start/etc. requests are made by ID, which is the order number in the returned XML list :-S
diff --git a/lib/src/org/transdroid/daemon/Bitflu/BitfluAdapter.java b/lib/src/org/transdroid/daemon/Bitflu/BitfluAdapter.java
index 14e618bd..6e762fa2 100644
--- a/lib/src/org/transdroid/daemon/Bitflu/BitfluAdapter.java
+++ b/lib/src/org/transdroid/daemon/Bitflu/BitfluAdapter.java
@@ -155,7 +155,7 @@ public class BitfluAdapter implements IDaemonAdapter {
// Read JSON response
InputStream instream = response.getEntity().getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
int httpstatus = response.getStatusLine().getStatusCode();
if(httpstatus != 200) {
@@ -214,7 +214,8 @@ public class BitfluAdapter implements IDaemonAdapter {
null, // label
null, // Not available
null, // Not available
- null)); // Not available
+ null, // Not available
+ settings.getType()));
}
}
// Return the list
diff --git a/lib/src/org/transdroid/daemon/BuffaloNas/BuffaloNasAdapter.java b/lib/src/org/transdroid/daemon/BuffaloNas/BuffaloNasAdapter.java
index dbe6bea8..98d30df8 100644
--- a/lib/src/org/transdroid/daemon/BuffaloNas/BuffaloNasAdapter.java
+++ b/lib/src/org/transdroid/daemon/BuffaloNas/BuffaloNasAdapter.java
@@ -178,7 +178,7 @@ public class BuffaloNasAdapter implements IDaemonAdapter {
// Read JSON response
java.io.InputStream instream = entity.getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
instream.close();
// Return raw result
@@ -281,7 +281,8 @@ public class BuffaloNasAdapter implements IDaemonAdapter {
null,
null,
null,
- null));
+ null,
+ settings.getType()));
}
// Return the list
diff --git a/lib/src/org/transdroid/daemon/DLinkRouterBT/DLinkRouterBTAdapter.java b/lib/src/org/transdroid/daemon/DLinkRouterBT/DLinkRouterBTAdapter.java
index 521333b7..e504767e 100644
--- a/lib/src/org/transdroid/daemon/DLinkRouterBT/DLinkRouterBTAdapter.java
+++ b/lib/src/org/transdroid/daemon/DLinkRouterBT/DLinkRouterBTAdapter.java
@@ -273,7 +273,7 @@ public class DLinkRouterBTAdapter implements IDaemonAdapter {
// Read JSON response
java.io.InputStream instream = entity.getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
JSONObject json = new JSONObject(result);
instream.close();
@@ -376,7 +376,8 @@ public class DLinkRouterBTAdapter implements IDaemonAdapter {
null,
null,
null,
- null);
+ null,
+ settings.getType());
torrents.add(new_t);
}
diff --git a/lib/src/org/transdroid/daemon/Daemon.java b/lib/src/org/transdroid/daemon/Daemon.java
index 7579ea07..847175f2 100644
--- a/lib/src/org/transdroid/daemon/Daemon.java
+++ b/lib/src/org/transdroid/daemon/Daemon.java
@@ -59,6 +59,11 @@ public enum Daemon {
return new DelugeAdapter(settings);
}
},
+ Dummy {
+ public IDaemonAdapter createAdapter(DaemonSettings settings) {
+ return new DummyAdapter(settings);
+ }
+ },
DLinkRouterBT {
public IDaemonAdapter createAdapter(DaemonSettings settings) {
return new DLinkRouterBTAdapter(settings);
@@ -111,6 +116,47 @@ public enum Daemon {
};
public abstract IDaemonAdapter createAdapter(DaemonSettings settings);
+
+ /**
+ * Returns the code as used in preferences matching the given daemon type
+ * @return A string of the form 'daemon_' that represents the daemon's enum value
+ */
+ public static String toCode(Daemon type) {
+ if (type == null)
+ return null;
+ switch (type) {
+ case BitComet:
+ return "daemon_bitcomet";
+ case Bitflu:
+ return "daemon_bitflue";
+ case BitTorrent:
+ return "daemon_bittorrent";
+ case BuffaloNas:
+ return "daemon_buffalonas";
+ case Deluge:
+ return "daemon_deluge";
+ case DLinkRouterBT:
+ return "daemon_dlinkrouterbt";
+ case Dummy:
+ return "daemon_dummy";
+ case KTorrent:
+ return "daemon_ktorrent";
+ case qBittorrent:
+ return "daemon_qbittorrent";
+ case rTorrent:
+ return "daemon_rtorrent";
+ case Tfb4rt:
+ return "daemon_tfb4rt";
+ case Transmission:
+ return "daemon_transmission";
+ case uTorrent:
+ return "daemon_utorrent";
+ case Vuze:
+ return "daemon_vuze";
+ default:
+ return null;
+ }
+ }
/**
* Returns the daemon enum type based on the code used in the user preferences.
@@ -121,6 +167,9 @@ public enum Daemon {
if (daemonCode == null) {
return null;
}
+ if (daemonCode.equals("daemon_bitcomet")) {
+ return BitComet;
+ }
if (daemonCode.equals("daemon_bitflu")) {
return Bitflu;
}
@@ -136,6 +185,9 @@ public enum Daemon {
if (daemonCode.equals("daemon_dlinkrouterbt")) {
return DLinkRouterBT;
}
+ if (daemonCode.equals("daemon_dummy")) {
+ return Dummy;
+ }
if (daemonCode.equals("daemon_ktorrent")) {
return KTorrent;
}
@@ -160,9 +212,6 @@ public enum Daemon {
if (daemonCode.equals("daemon_vuze")) {
return Vuze;
}
- if (daemonCode.equals("daemon_bitcomet")) {
- return BitComet;
- }
return null;
}
@@ -178,6 +227,7 @@ public enum Daemon {
case BuffaloNas:
return 8080;
case DLinkRouterBT:
+ case Dummy:
case rTorrent:
case Tfb4rt:
case BitComet:
@@ -201,19 +251,19 @@ public enum Daemon {
}
public static boolean supportsStats(Daemon type) {
- return type == Transmission || type == Bitflu;
+ return type == Transmission || type == Bitflu || type == Dummy;
}
public static boolean supportsAvailability(Daemon type) {
- return type == uTorrent || type == BitTorrent || type == DLinkRouterBT || type == Transmission || type == Vuze || type == BuffaloNas;
+ return type == uTorrent || type == BitTorrent || type == DLinkRouterBT || type == Transmission || type == Vuze || type == BuffaloNas || type == Dummy;
}
public static boolean supportsFileListing(Daemon type) {
- return type == Synology || type == Transmission || type == uTorrent || type == BitTorrent || type == KTorrent || type == Deluge || type == rTorrent || type == Vuze || type == DLinkRouterBT || type == Bitflu || type == qBittorrent || type == BuffaloNas || type == BitComet;
+ return type == Synology || type == Transmission || type == uTorrent || type == BitTorrent || type == KTorrent || type == Deluge || type == rTorrent || type == Vuze || type == DLinkRouterBT || type == Bitflu || type == qBittorrent || type == BuffaloNas || type == BitComet || type == Dummy;
}
public static boolean supportsFineDetails(Daemon type) {
- return type == uTorrent || type == BitTorrent || type == Daemon.Transmission || type == Deluge || type == rTorrent || type == qBittorrent;
+ return type == uTorrent || type == BitTorrent || type == Daemon.Transmission || type == Deluge || type == rTorrent || type == qBittorrent || type == Dummy;
}
public static boolean needsManualPathSpecified(Daemon type) {
@@ -221,23 +271,23 @@ public enum Daemon {
}
public static boolean supportsFilePaths(Daemon type) {
- return type == uTorrent || type == BitTorrent || type == Vuze || type == Deluge || type == Transmission || type == rTorrent || type == KTorrent || type == BuffaloNas;
+ return type == uTorrent || type == BitTorrent || type == Vuze || type == Deluge || type == Transmission || type == rTorrent || type == KTorrent || type == BuffaloNas || type == Dummy;
}
public static boolean supportsStoppingStarting(Daemon type) {
- return type == uTorrent || type == rTorrent || type == BitTorrent || type == BitComet;
+ return type == uTorrent || type == rTorrent || type == BitTorrent || type == BitComet || type == Dummy;
}
public static boolean supportsForcedStarting(Daemon type) {
- return type == uTorrent || type == BitTorrent;
+ return type == uTorrent || type == BitTorrent || type == Dummy;
}
public static boolean supportsCustomFolder(Daemon type) {
- return type == rTorrent || type == Tfb4rt || type == Bitflu || type == Deluge || type == Transmission;
+ return type == rTorrent || type == Tfb4rt || type == Bitflu || type == Deluge || type == Transmission || type == Dummy;
}
public static boolean supportsSetTransferRates(Daemon type) {
- return type == Deluge || type == Transmission || type == uTorrent || type == BitTorrent || type == Deluge || type == rTorrent || type == Vuze || type == BuffaloNas || type == BitComet;
+ return type == Deluge || type == Transmission || type == uTorrent || type == BitTorrent || type == Deluge || type == rTorrent || type == Vuze || type == BuffaloNas || type == BitComet || type == Dummy;
}
public static boolean supportsAddByFile(Daemon type) {
@@ -246,39 +296,39 @@ public enum Daemon {
}
public static boolean supportsAddByMagnetUrl(Daemon type) {
- return type == uTorrent || type == BitTorrent || type == Transmission || type == Synology || type == Deluge || type == Bitflu || type == KTorrent || type == rTorrent || type == qBittorrent || type == BitComet;
+ return type == uTorrent || type == BitTorrent || type == Transmission || type == Synology || type == Deluge || type == Bitflu || type == KTorrent || type == rTorrent || type == qBittorrent || type == BitComet || type == Dummy;
}
public static boolean supportsRemoveWithData(Daemon type) {
- return type == uTorrent || type == Vuze || type == Transmission || type == Deluge || type == BitTorrent || type == Tfb4rt || type == DLinkRouterBT || type == Bitflu || type == qBittorrent || type == BuffaloNas || type == BitComet || type == rTorrent;
+ return type == uTorrent || type == Vuze || type == Transmission || type == Deluge || type == BitTorrent || type == Tfb4rt || type == DLinkRouterBT || type == Bitflu || type == qBittorrent || type == BuffaloNas || type == BitComet || type == rTorrent || type == Dummy;
}
public static boolean supportsFilePrioritySetting(Daemon type) {
- return type == BitTorrent || type == uTorrent || type == Transmission || type == KTorrent || type == rTorrent || type == Vuze || type == Deluge || type == qBittorrent;
+ return type == BitTorrent || type == uTorrent || type == Transmission || type == KTorrent || type == rTorrent || type == Vuze || type == Deluge || type == qBittorrent || type == Dummy;
}
public static boolean supportsDateAdded(Daemon type) {
- return type == Vuze || type == Transmission || type == rTorrent || type == Bitflu || type == BitComet;
+ return type == Vuze || type == Transmission || type == rTorrent || type == Bitflu || type == BitComet || type == Dummy;
}
public static boolean supportsLabels(Daemon type) {
- return type == uTorrent || type == BitTorrent || type == Deluge || type == BitComet || type == rTorrent; // || type == Vuze
+ return type == uTorrent || type == BitTorrent || type == Deluge || type == BitComet || type == rTorrent || type == Dummy; // || type == Vuze
}
public static boolean supportsSetLabel(Daemon type) {
- return type == uTorrent || type == BitTorrent || type == rTorrent;
+ return type == uTorrent || type == BitTorrent || type == rTorrent || type == Dummy;
}
public static boolean supportsSetDownloadLocation(Daemon type) {
- return type == Transmission || type == Deluge;
+ return type == Transmission || type == Deluge || type == Dummy;
}
public static boolean supportsSetAlternativeMode(Daemon type) {
- return type == Transmission;
+ return type == Transmission || type == Dummy;
}
public static boolean supportsSetTrackers(Daemon type) {
- return type == uTorrent || type == BitTorrent || type == Deluge;
+ return type == uTorrent || type == BitTorrent || type == Deluge || type == Dummy;
}
public static boolean supportsExtraPassword(Daemon type) {
diff --git a/lib/src/org/transdroid/daemon/Deluge/DelugeAdapter.java b/lib/src/org/transdroid/daemon/Deluge/DelugeAdapter.java
index 8a7fa785..d861c4f4 100644
--- a/lib/src/org/transdroid/daemon/Deluge/DelugeAdapter.java
+++ b/lib/src/org/transdroid/daemon/Deluge/DelugeAdapter.java
@@ -182,7 +182,7 @@ public class DelugeAdapter implements IDaemonAdapter {
// Read JSON response
InputStream instream = response.getEntity().getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
// If the upload succeeded, add the torrent file on the server
// For this we need the file name, which is now send as a JSON object like:
@@ -477,7 +477,7 @@ public class DelugeAdapter implements IDaemonAdapter {
// Still no session cookie?
if (sessionCookie == null) {
// Set error message and cancel the action that was requested
- throw new DaemonException(ExceptionType.AuthenticationFailure, "Password error? Server time difference? No (valid) cookie in response and JSON was: " + HttpHelper.ConvertStreamToString(instream));
+ throw new DaemonException(ExceptionType.AuthenticationFailure, "Password error? Server time difference? No (valid) cookie in response and JSON was: " + HttpHelper.convertStreamToString(instream));
}
}
@@ -509,7 +509,7 @@ public class DelugeAdapter implements IDaemonAdapter {
// Read JSON response
InputStream instream = entity.getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
JSONObject json = new JSONObject(result);
instream.close();
@@ -593,8 +593,9 @@ public class DelugeAdapter implements IDaemonAdapter {
0f, // Not available
tor.has(RPC_LABEL)? tor.getString(RPC_LABEL): null,
tor.has(RPC_TIMEADDED)? new Date(tor.getInt(RPC_TIMEADDED) * 1000L): null,
- null,
- tor.getString(RPC_MESSAGE))); // Not available
+ null, // Not available
+ tor.getString(RPC_MESSAGE),
+ settings.getType()));
}
}
diff --git a/lib/src/org/transdroid/daemon/DummyAdapter.java b/lib/src/org/transdroid/daemon/DummyAdapter.java
new file mode 100644
index 00000000..e52b133b
--- /dev/null
+++ b/lib/src/org/transdroid/daemon/DummyAdapter.java
@@ -0,0 +1,301 @@
+/*
+ * This file is part of Transdroid
+ *
+ * Transdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Transdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Transdroid. If not, see .
+ *
+ */
+package org.transdroid.daemon;
+
+import java.io.File;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+
+import org.transdroid.daemon.DaemonException.ExceptionType;
+import org.transdroid.daemon.task.AddByFileTask;
+import org.transdroid.daemon.task.AddByMagnetUrlTask;
+import org.transdroid.daemon.task.AddByUrlTask;
+import org.transdroid.daemon.task.DaemonTask;
+import org.transdroid.daemon.task.DaemonTaskFailureResult;
+import org.transdroid.daemon.task.DaemonTaskResult;
+import org.transdroid.daemon.task.DaemonTaskSuccessResult;
+import org.transdroid.daemon.task.GetFileListTask;
+import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
+import org.transdroid.daemon.task.GetStatsTask;
+import org.transdroid.daemon.task.GetStatsTaskSuccessResult;
+import org.transdroid.daemon.task.GetTorrentDetailsTask;
+import org.transdroid.daemon.task.GetTorrentDetailsTaskSuccessResult;
+import org.transdroid.daemon.task.RetrieveTask;
+import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
+import org.transdroid.daemon.task.SetAlternativeModeTask;
+import org.transdroid.daemon.task.SetDownloadLocationTask;
+import org.transdroid.daemon.task.SetFilePriorityTask;
+import org.transdroid.daemon.task.SetLabelTask;
+import org.transdroid.daemon.task.SetTrackersTask;
+import org.transdroid.daemon.util.DLog;
+
+import android.net.Uri;
+
+/**
+ * A dummy adapter that does not communicate with some server, but maintains a local list of dummy data (reset every
+ * time it is recreated) to simplify testing.
+ * @author erickok
+ */
+public class DummyAdapter implements IDaemonAdapter {
+
+ private static final String LOG_NAME = "Dummy daemon";
+
+ private DaemonSettings settings;
+ private List dummyTorrents;
+ private List dummyLabels;
+ private boolean alternativeModeEnabled = false;
+ private List trackersList = new ArrayList(Arrays.asList("udp://tracker.com/announce:80",
+ "https://torrents.org/announce:443"));
+
+ /**
+ * Initialises a dummy adapter with some dummy data that may be manipulated.
+ */
+ public DummyAdapter(DaemonSettings settings) {
+ this.settings = settings;
+ this.dummyTorrents = new ArrayList();
+ this.dummyLabels = new ArrayList();
+ String[] names = new String[] { "Documentary ", "Book ", "CD Image ", "Mix tape ", "App " };
+ String[] labels = new String[] { "docs", "books", "isos", "music", "software" };
+ TorrentStatus[] statuses = new TorrentStatus[] { TorrentStatus.Seeding, TorrentStatus.Downloading,
+ TorrentStatus.Paused, TorrentStatus.Queued, TorrentStatus.Downloading, TorrentStatus.Seeding,
+ TorrentStatus.Error };
+ Random random = new Random();
+ for (int i = 1; i < 26; i++) {
+ String name = names[i % names.length] + Integer.toString(i);
+ TorrentStatus status = statuses[i % statuses.length];
+ int peersGetting = status == TorrentStatus.Downloading ? i * random.nextInt(16) : 0;
+ int peersSending = status == TorrentStatus.Downloading ? i * random.nextInt(16) : 0;
+ long size = (long) (1024D * 1024D * 1024D * i * random.nextDouble());
+ long left = status == TorrentStatus.Downloading ? (long) (size * random.nextDouble()) : 0;
+ int rateDownload = status == TorrentStatus.Downloading ? (int) (1024D * 100D * i * random.nextDouble())
+ : 0;
+ int rateUpload = status == TorrentStatus.Downloading || status == TorrentStatus.Seeding ?
+ (int) (1024D * 100D * i * random.nextDouble()) : 0;
+ this.dummyTorrents.add(
+ new Torrent(
+ i,
+ "torrent_" + i,
+ name,
+ status,
+ "/downloads/" + name.replace(" ", "_"),
+ rateDownload,
+ rateUpload,
+ peersGetting,
+ peersSending,
+ peersGetting + peersSending, // Total connections
+ (peersGetting + peersSending) * 2, // Twice the total connections
+ (int) (status == TorrentStatus.Downloading?
+ left / rateDownload: 0), // Eta
+ size - left,
+ (long)((double)(size - left) * 3D * random.nextDouble()), // Up to 3 times the amount downloaded
+ size,
+ (float)(size - left) / size, // Part done
+ 1F, // Always 100% available
+ labels[i % labels.length],
+ new Date(System.currentTimeMillis() - (7 * 24 * 60 * 60 * 1000)), // Last week
+ null,
+ status == TorrentStatus.Error?
+ "Dummy error": null,
+ settings.getType()));
+ }
+ for (String label : labels) {
+ dummyLabels.add(new Label(label, 5));
+ }
+ }
+
+ @Override
+ public DaemonTaskResult executeTask(DaemonTask task) {
+
+ try {
+ switch (task.getMethod()) {
+ case Retrieve:
+
+ return new RetrieveTaskSuccessResult((RetrieveTask) task, dummyTorrents, dummyLabels);
+
+ case GetTorrentDetails:
+
+ return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, new TorrentDetails(
+ trackersList,
+ task.getTargetTorrent().getStatusCode() == TorrentStatus.Error ?
+ Arrays.asList("Trackers not working.", "Files not available.") : null));
+
+ case GetFileList:
+
+ Torrent t = task.getTargetTorrent();
+ List dummyFiles = new ArrayList();
+ Priority priorities[] = new Priority[] { Priority.Normal, Priority.Normal, Priority.High, Priority.Low,
+ Priority.Normal };
+ for (int i = 1; i < 16; i++) {
+ String fileName = "file_" + i + ".ext";
+ // Every file has equal part in the total size
+ long size = t.getTotalSize() / 25;
+ long done = t.getDownloadedEver() / 25;
+ Priority priority = priorities[i % priorities.length];
+ dummyFiles.add(new TorrentFile("file_" + i, t.getName() + " file " + i, fileName, t
+ .getLocationDir() + "/" + fileName, size, done, priority));
+ }
+ return new GetFileListTaskSuccessResult((GetFileListTask) task, dummyFiles);
+
+ case GetStats:
+
+ return new GetStatsTaskSuccessResult((GetStatsTask) task, alternativeModeEnabled, 1024L * 1024L *
+ 1024L * 100);
+
+ case AddByFile:
+
+ String file = ((AddByFileTask) task).getFile();
+ DLog.d(LOG_NAME, "Adding torrent " + file);
+ File upload = new File(URI.create(file));
+ dummyTorrents.add(new Torrent(0, "torrent_file", upload.getName(), TorrentStatus.Queued, "/downloads/"
+ + file, 0, 0, 0, 0, 0, 0, -1, 0, 0, 1024 * 1024 * 1000, 0, 1F, "isos", new Date(), null, null,
+ settings.getType()));
+ return new DaemonTaskSuccessResult(task);
+
+ case AddByUrl:
+
+ String url = ((AddByUrlTask) task).getUrl();
+ DLog.d(LOG_NAME, "Adding torrent " + url);
+ if (url == null || url.equals(""))
+ throw new DaemonException(DaemonException.ExceptionType.ParsingFailed, "No url specified");
+ Uri uri = Uri.parse(url);
+ dummyTorrents.add(new Torrent(0, "torrent_byurl", uri.getLastPathSegment(), TorrentStatus.Queued,
+ "/downloads/" + uri.getLastPathSegment(), 0, 0, 0, 0, 0, 0, -1, 0, 0, 1024 * 1024 * 1000, 0,
+ 1F, "music", new Date(), null, null, settings.getType()));
+ return new DaemonTaskSuccessResult(task);
+
+ case AddByMagnetUrl:
+
+ String magnet = ((AddByMagnetUrlTask) task).getUrl();
+ DLog.d(LOG_NAME, "Adding torrent " + magnet);
+ Uri magnetUri = Uri.parse(magnet);
+ dummyTorrents.add(new Torrent(0, "torrent_magnet", magnetUri.getLastPathSegment(),
+ TorrentStatus.Queued, "/downloads/" + magnetUri.getLastPathSegment(), 0, 0, 0, 0, 0, 0, -1, 0,
+ 0, 1024 * 1024 * 1000, 0, 1F, "books", new Date(), null, null, settings.getType()));
+ return new DaemonTaskSuccessResult(task);
+
+ case Remove:
+
+ dummyTorrents.remove(task.getTargetTorrent());
+ return new DaemonTaskSuccessResult(task);
+
+ case Pause:
+
+ task.getTargetTorrent().mimicPause();
+ return new DaemonTaskSuccessResult(task);
+
+ case PauseAll:
+
+ for (Torrent torrent: dummyTorrents) {
+ torrent.mimicPause();
+ }
+ return new DaemonTaskSuccessResult(task);
+
+ case Resume:
+
+ task.getTargetTorrent().mimicPause();
+ return new DaemonTaskSuccessResult(task);
+
+ case ResumeAll:
+
+ for (Torrent torrent: dummyTorrents) {
+ torrent.mimicResume();
+ }
+ return new DaemonTaskSuccessResult(task);
+
+ case Stop:
+
+ task.getTargetTorrent().mimicStop();
+ return new DaemonTaskSuccessResult(task);
+
+ case StopAll:
+
+ for (Torrent torrent: dummyTorrents) {
+ torrent.mimicStop();
+ }
+ return new DaemonTaskSuccessResult(task);
+
+ case Start:
+
+ task.getTargetTorrent().mimicStart();
+ return new DaemonTaskSuccessResult(task);
+
+ case StartAll:
+
+ for (Torrent torrent: dummyTorrents) {
+ torrent.mimicStart();
+ }
+ return new DaemonTaskSuccessResult(task);
+
+ case SetFilePriorities:
+
+ SetFilePriorityTask prioTask = (SetFilePriorityTask) task;
+ for (TorrentFile forFile : prioTask.getForFiles()) {
+ forFile.mimicPriority(prioTask.getNewPriority());
+ }
+ return new DaemonTaskSuccessResult(task);
+
+ case SetTransferRates:
+
+ // No action, as the result in not visible anyway
+ return new DaemonTaskSuccessResult(task);
+
+ case SetLabel:
+
+ SetLabelTask labelTask = (SetLabelTask) task;
+ task.getTargetTorrent().mimicNewLabel(labelTask.getNewLabel());
+ return new DaemonTaskSuccessResult(task);
+
+ case SetTrackers:
+
+ trackersList = new ArrayList(((SetTrackersTask)task).getNewTrackers());
+ return new DaemonTaskSuccessResult(task);
+
+ case SetDownloadLocation:
+
+ task.getTargetTorrent().mimicNewLocation(((SetDownloadLocationTask) task).getNewLocation());
+ return new DaemonTaskSuccessResult(task);
+
+ case SetAlternativeMode:
+
+ alternativeModeEnabled = ((SetAlternativeModeTask) task).isAlternativeModeEnabled();
+ return new DaemonTaskSuccessResult(task);
+
+ default:
+ return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported,
+ task.getMethod() + " is not supported by " + getType()));
+ }
+ } catch (DaemonException e) {
+ return new DaemonTaskFailureResult(task, e);
+ }
+ }
+
+ @Override
+ public Daemon getType() {
+ return settings.getType();
+ }
+
+ @Override
+ public DaemonSettings getSettings() {
+ return this.settings;
+ }
+
+}
diff --git a/lib/src/org/transdroid/daemon/Ktorrent/KtorrentAdapter.java b/lib/src/org/transdroid/daemon/Ktorrent/KtorrentAdapter.java
index f5627e6c..6e3a3191 100644
--- a/lib/src/org/transdroid/daemon/Ktorrent/KtorrentAdapter.java
+++ b/lib/src/org/transdroid/daemon/Ktorrent/KtorrentAdapter.java
@@ -293,7 +293,7 @@ public class KtorrentAdapter implements IDaemonAdapter {
HttpGet httpget = new HttpGet(buildWebUIUrl() + RPC_URL_CHALLENGE);
HttpResponse response = httpclient.execute(httpget);
InputStream instream = response.getEntity().getContent();
- String challengeString = HttpHelper.ConvertStreamToString(instream).replaceAll("\\<.*?>","").trim();
+ String challengeString = HttpHelper.convertStreamToString(instream).replaceAll("\\<.*?>","").trim();
instream.close();
// Challenge string should be something like TncpX3TB8uZ0h8eqztZ6
if (challengeString == null || challengeString.length() != 20) {
@@ -358,7 +358,7 @@ public class KtorrentAdapter implements IDaemonAdapter {
// Read response (a successful action always returned '1')
InputStream instream = response.getEntity().getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
instream.close();
if (result.contains(RPC_SUCCESS)) {
return true;
@@ -407,7 +407,7 @@ public class KtorrentAdapter implements IDaemonAdapter {
// Read response (a successful action always returned '1')
InputStream instream = response.getEntity().getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
instream.close();
if (result.equals("")) {
return true;
diff --git a/lib/src/org/transdroid/daemon/Ktorrent/StatsParser.java b/lib/src/org/transdroid/daemon/Ktorrent/StatsParser.java
index fb55a519..b6dd76f7 100644
--- a/lib/src/org/transdroid/daemon/Ktorrent/StatsParser.java
+++ b/lib/src/org/transdroid/daemon/Ktorrent/StatsParser.java
@@ -5,6 +5,7 @@ import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
+import org.transdroid.daemon.Daemon;
import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentStatus;
@@ -85,7 +86,8 @@ public class StatsParser {
null, // Not supported in the web interface
null, // Not supported in the web interface
null, // Not supported in the web interface
- null)); // Not supported in the web interface
+ null, // Not supported in the web interface
+ Daemon.KTorrent));
id++; // Stop/start/etc. requests are made by ID, which is the order number in the returned XML list :-S
} else if (next == XmlPullParser.START_TAG && name.equals("torrent")){
diff --git a/lib/src/org/transdroid/daemon/OS.java b/lib/src/org/transdroid/daemon/OS.java
index a7f150b2..9bde578a 100644
--- a/lib/src/org/transdroid/daemon/OS.java
+++ b/lib/src/org/transdroid/daemon/OS.java
@@ -12,6 +12,21 @@ public enum OS {
@Override public String getPathSeperator() { return "/"; }
};
+ public static String toCode(OS os) {
+ if (os == null)
+ return null;
+ switch (os) {
+ case Windows:
+ return "type_windows";
+ case Mac:
+ return "type_mac";
+ case Linux:
+ return "type_linux";
+ default:
+ return null;
+ }
+ }
+
public static OS fromCode(String osCode) {
if (osCode == null) {
return null;
diff --git a/lib/src/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java b/lib/src/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java
index 0a7b4fda..9c1e0839 100644
--- a/lib/src/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java
+++ b/lib/src/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java
@@ -306,7 +306,7 @@ public class QbittorrentAdapter implements IDaemonAdapter {
// Read JSON response
java.io.InputStream instream = entity.getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
instream.close();
//TLog.d(LOG_NAME, "Success: " + (result.length() > 300? result.substring(0, 300) + "... (" + result.length() + " chars)": result));
@@ -409,7 +409,8 @@ public class QbittorrentAdapter implements IDaemonAdapter {
null,
null, // Only available in /json/propertiesGeneral on a per-torrent basis, unfortunately
null,
- null));
+ null,
+ settings.getType()));
}
// Return the list
diff --git a/lib/src/org/transdroid/daemon/Rtorrent/RtorrentAdapter.java b/lib/src/org/transdroid/daemon/Rtorrent/RtorrentAdapter.java
index 7e06af29..21b4760d 100644
--- a/lib/src/org/transdroid/daemon/Rtorrent/RtorrentAdapter.java
+++ b/lib/src/org/transdroid/daemon/Rtorrent/RtorrentAdapter.java
@@ -27,13 +27,17 @@ import java.net.URI;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Date;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import org.base64.android.Base64;
import org.transdroid.daemon.Daemon;
import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.DaemonSettings;
import org.transdroid.daemon.IDaemonAdapter;
+import org.transdroid.daemon.Label;
import org.transdroid.daemon.Priority;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentDetails;
@@ -77,6 +81,7 @@ public class RtorrentAdapter implements IDaemonAdapter {
private DaemonSettings settings;
private XMLRPCClient rpcclient;
+ private List lastKnownLabels = null;
public RtorrentAdapter(DaemonSettings settings) {
this.settings = settings;
@@ -90,7 +95,7 @@ public class RtorrentAdapter implements IDaemonAdapter {
case Retrieve:
Object result = makeRtorrentCall("d.multicall", new String[] { "main", "d.get_hash=", "d.get_name=", "d.get_state=", "d.get_down_rate=", "d.get_up_rate=", "d.get_peers_connected=", "d.get_peers_not_connected=", "d.get_peers_accounted=", "d.get_bytes_done=", "d.get_up_total=", "d.get_size_bytes=", "d.get_creation_date=", "d.get_left_bytes=", "d.get_complete=", "d.is_active=", "d.is_hash_checking=", "d.get_base_path=", "d.get_base_filename=", "d.get_message=", "d.get_custom=addtime", "d.get_custom=seedingtime", "d.get_custom1=" });
- return new RetrieveTaskSuccessResult((RetrieveTask) task, onTorrentsRetrieved(result),null);
+ return new RetrieveTaskSuccessResult((RetrieveTask) task, onTorrentsRetrieved(result), lastKnownLabels);
case GetTorrentDetails:
@@ -236,14 +241,16 @@ public class RtorrentAdapter implements IDaemonAdapter {
initialise();
}
+ String params = "";
+ for (Object arg : arguments) params += " " + arg.toString();
try {
- String params = "";
- for (Object arg : arguments) params += " " + arg.toString();
- DLog.d(LOG_NAME, "Calling " + serverMethod + " with params [" + (params.length() > 300? params.substring(0, 300) + "...": params) + " ]");
+ DLog.d(LOG_NAME, "Calling " + serverMethod + " with params [" + (params.length() > 100? params.substring(0, 100) + "...": params) + " ]");
return rpcclient.call(serverMethod, arguments);
} catch (XMLRPCException e) {
DLog.d(LOG_NAME, e.toString());
- throw new DaemonException(ExceptionType.ConnectionError, "Error making call to " + serverMethod + " with params " + arguments.toString() + ": " + e.toString());
+ if (e.getCause() instanceof DaemonException)
+ throw (DaemonException) e.getCause();
+ throw new DaemonException(ExceptionType.ConnectionError, "Error making call to " + serverMethod + " with params [" + (params.length() > 100? params.substring(0, 100) + "...": params) + " ]: " + e.toString());
}
}
@@ -277,10 +284,8 @@ public class RtorrentAdapter implements IDaemonAdapter {
// Parse torrent list from response
// Formatted as Object[][], see http://libtorrent.rakshasa.no/wiki/RTorrentCommands#Download
- // 'Labels' are supported in rTorrent as 'groups' that can become the 'active view';
- // support for this is not trivial since it requires multiple calls to get all the info at best
- // (if it is even feasible with the current approach)
List torrents = new ArrayList();
+ Map labels = new HashMap();
Object[] responseList = (Object[]) response;
for (int i = 0; i < responseList.length; i++) {
@@ -296,7 +301,7 @@ public class RtorrentAdapter implements IDaemonAdapter {
} catch (NumberFormatException e) {
// Not a number (timestamp); ignore and fall back to using creationtime
}
- if(addtime != null)
+ if (addtime != null)
// Successfully received the addtime from rTorrent (which is a String like '1337089336\n')
added = new Date(addtime * 1000L);
else {
@@ -315,7 +320,7 @@ public class RtorrentAdapter implements IDaemonAdapter {
} catch (NumberFormatException e) {
// Not a number (timestamp); ignore and fall back to using creationtime
}
- if(seedingtime != null)
+ if (seedingtime != null)
// Successfully received the seedingtime from rTorrent (which is a String like '1337089336\n')
finished = new Date(seedingtime * 1000L);
@@ -323,7 +328,13 @@ public class RtorrentAdapter implements IDaemonAdapter {
String label = null;
try {
label = URLDecoder.decode((String)info[21], "UTF-8");
- } catch (UnsupportedEncodingException e) {
+ if (labels.containsKey(label)) {
+ labels.put(label, labels.get(label) + 1);
+ } else {
+ labels.put(label, 0);
+ }
+ } catch (UnsupportedEncodingException e) {
+ // Can't decode label name; ignore it
}
if (info[3] instanceof Long) {
@@ -350,10 +361,11 @@ public class RtorrentAdapter implements IDaemonAdapter {
(Long)info[10], // totalSize
((Long)info[8]).floatValue() / ((Long)info[10]).floatValue(), // partDone
0f, // TODO: Add availability data
- label, // See remark on rTorrent/groups above
+ label,
added,
finished,
- error));
+ error,
+ settings.getType()));
} else {
@@ -379,13 +391,19 @@ public class RtorrentAdapter implements IDaemonAdapter {
(Integer)info[10], // totalSize
((Integer)info[8]).floatValue() / ((Integer)info[10]).floatValue(), // partDone
0f, // TODO: Add availability data
- label, // See remark on rTorrent/groups above
+ label,
added,
finished,
- error));
+ error,
+ settings.getType()));
}
}
+ lastKnownLabels = new ArrayList();
+ for (Entry pair : labels.entrySet()) {
+ if (pair.getKey() != null)
+ lastKnownLabels.add(new Label(pair.getKey(), pair.getValue()));
+ }
return torrents;
}
diff --git a/lib/src/org/transdroid/daemon/Synology/SynologyAdapter.java b/lib/src/org/transdroid/daemon/Synology/SynologyAdapter.java
index bf0708ea..6db4ce80 100644
--- a/lib/src/org/transdroid/daemon/Synology/SynologyAdapter.java
+++ b/lib/src/org/transdroid/daemon/Synology/SynologyAdapter.java
@@ -326,7 +326,8 @@ public class SynologyAdapter implements IDaemonAdapter {
jsonTorrent.getString("title"),
new Date(detail.getLong("create_time") * 1000),
null,
- ""
+ "",
+ settings.getType()
);
}
@@ -426,7 +427,7 @@ public class SynologyAdapter implements IDaemonAdapter {
}
// Read JSON response
java.io.InputStream instream = entity.getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
JSONObject json;
json = new JSONObject(result);
instream.close();
diff --git a/lib/src/org/transdroid/daemon/Tfb4rt/StatsParser.java b/lib/src/org/transdroid/daemon/Tfb4rt/StatsParser.java
index af7f5cc7..31dc7ae7 100644
--- a/lib/src/org/transdroid/daemon/Tfb4rt/StatsParser.java
+++ b/lib/src/org/transdroid/daemon/Tfb4rt/StatsParser.java
@@ -5,6 +5,7 @@ import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
+import org.transdroid.daemon.Daemon;
import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentStatus;
@@ -80,7 +81,8 @@ public class StatsParser {
null, // Not supported in the XML stats
null,
null,
- null));
+ null,
+ Daemon.Tfb4rt));
} else if (next == XmlPullParser.START_TAG && name.equals("transfer")){
diff --git a/lib/src/org/transdroid/daemon/Tfb4rt/Tfb4rtAdapter.java b/lib/src/org/transdroid/daemon/Tfb4rt/Tfb4rtAdapter.java
index 4de338dc..2187031e 100644
--- a/lib/src/org/transdroid/daemon/Tfb4rt/Tfb4rtAdapter.java
+++ b/lib/src/org/transdroid/daemon/Tfb4rt/Tfb4rtAdapter.java
@@ -195,7 +195,7 @@ public class Tfb4rtAdapter implements IDaemonAdapter {
// Read response (a successful action always returned '1')
InputStream instream = response.getEntity().getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
instream.close();
if (result.trim().equals("1")) {
return true;
@@ -232,7 +232,7 @@ public class Tfb4rtAdapter implements IDaemonAdapter {
// Read response (a successful action always returned '1')
InputStream instream = response.getEntity().getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
instream.close();
if (result.equals("1")) {
return true;
diff --git a/lib/src/org/transdroid/daemon/Torrent.java b/lib/src/org/transdroid/daemon/Torrent.java
index 63622186..75bfb86c 100644
--- a/lib/src/org/transdroid/daemon/Torrent.java
+++ b/lib/src/org/transdroid/daemon/Torrent.java
@@ -35,7 +35,7 @@ public final class Torrent implements Parcelable, Comparable {
final private String hash;
final private String name;
private TorrentStatus statusCode;
- final private String locationDir;
+ private String locationDir;
final private int rateDownload;
final private int rateUpload;
@@ -55,6 +55,7 @@ public final class Torrent implements Parcelable, Comparable {
final private Date dateAdded;
final private Date dateDone;
final private String error;
+ final private Daemon daemon;
//public long getID() { return id; }
//public String getHash() { return hash; }
@@ -80,6 +81,7 @@ public final class Torrent implements Parcelable, Comparable {
public Date getDateAdded() { return dateAdded; }
public Date getDateDone() { return dateDone; }
public String getError() { return error; }
+ public Daemon getDaemon() { return daemon; }
private Torrent(Parcel in) {
this.id = in.readLong();
@@ -108,12 +110,13 @@ public final class Torrent implements Parcelable, Comparable {
long lDateDone = in.readLong();
this.dateDone = (lDateDone == -1)? null: new Date(lDateDone);
this.error = in.readString();
+ this.daemon = Daemon.valueOf(in.readString());
}
public Torrent(long id, String hash, String name, TorrentStatus statusCode, String locationDir, int rateDownload, int rateUpload,
int peersGettingFromUs, int peersSendingToUs, int peersConnected, int peersKnown, int eta,
long downloadedEver, long uploadedEver, long totalSize, float partDone, float available, String label,
- Date dateAdded, Date realDateDone, String error) {
+ Date dateAdded, Date realDateDone, String error, Daemon daemon) {
this.id = id;
this.hash = hash;
this.name = name;
@@ -148,6 +151,7 @@ public final class Torrent implements Parcelable, Comparable {
}
}
this.error = error;
+ this.daemon = daemon;
}
/**
@@ -241,6 +245,10 @@ public final class Torrent implements Parcelable, Comparable {
public void mimicNewLabel(String newLabel) {
label = newLabel;
}
+
+ public void mimicNewLocation(String newLocation) {
+ locationDir = newLocation;
+ }
@Override
public String toString() {
@@ -295,6 +303,7 @@ public final class Torrent implements Parcelable, Comparable {
dest.writeLong((dateAdded == null)? -1: dateAdded.getTime());
dest.writeLong((dateDone == null)? -1: dateDone.getTime());
dest.writeString(error);
+ dest.writeString(daemon.name());
}
}
diff --git a/lib/src/org/transdroid/daemon/TorrentFile.java b/lib/src/org/transdroid/daemon/TorrentFile.java
index 9b0a5d24..2c7322dc 100644
--- a/lib/src/org/transdroid/daemon/TorrentFile.java
+++ b/lib/src/org/transdroid/daemon/TorrentFile.java
@@ -84,6 +84,10 @@ public final class TorrentFile implements Parcelable, Comparable {
return priority;
}
+ public void mimicPriority(Priority newPriority) {
+ priority = newPriority;
+ }
+
public float getPartDone() {
return (float)downloaded / (float)totalSize;
}
diff --git a/lib/src/org/transdroid/daemon/TorrentFilesComparator.java b/lib/src/org/transdroid/daemon/TorrentFilesComparator.java
index 4a3848d9..de6a5b09 100644
--- a/lib/src/org/transdroid/daemon/TorrentFilesComparator.java
+++ b/lib/src/org/transdroid/daemon/TorrentFilesComparator.java
@@ -28,8 +28,9 @@ import java.util.Comparator;
*/
public class TorrentFilesComparator implements Comparator {
- TorrentFilesSortBy sortBy;
- boolean reversed;
+ private TorrentFilesSortBy sortBy;
+ private boolean reversed;
+ private Comparator alphanumComparator = new AlphanumComparator();
/**
* Instantiate a torrent files comparator.
@@ -50,7 +51,7 @@ public class TorrentFilesComparator implements Comparator {
case TotalSize:
return new Long(file1.getTotalSize()).compareTo(file2.getTotalSize());
default:
- return file1.getName().toLowerCase().compareTo(file2.getName().toLowerCase());
+ return alphanumComparator.compare(file1.getName().toLowerCase(), file2.getName().toLowerCase());
}
} else {
switch (sortBy) {
@@ -59,7 +60,7 @@ public class TorrentFilesComparator implements Comparator {
case TotalSize:
return 0 - new Long(file1.getTotalSize()).compareTo(file2.getTotalSize());
default:
- return 0 - file1.getName().toLowerCase().compareTo(file2.getName().toLowerCase());
+ return 0 - alphanumComparator.compare(file1.getName().toLowerCase(), file2.getName().toLowerCase());
}
}
}
diff --git a/lib/src/org/transdroid/daemon/TorrentsComparator.java b/lib/src/org/transdroid/daemon/TorrentsComparator.java
index 8cb93f9f..b2ccc812 100644
--- a/lib/src/org/transdroid/daemon/TorrentsComparator.java
+++ b/lib/src/org/transdroid/daemon/TorrentsComparator.java
@@ -28,28 +28,27 @@ import java.util.Comparator;
*/
public class TorrentsComparator implements Comparator {
- TorrentsSortBy sortBy;
- boolean reversed;
+ private TorrentsSortBy sortBy;
+ private boolean reversed;
+ private Comparator alphanumComparator = new AlphanumComparator();
/**
- * Instantiate a torrents comparator. The daemon object is used to check support for comparing
- * on the set properties. If the daemon does not support the property, ascending Alphanumeric
- * sorting will be used even if sorting is requested on the unsupported property.
- * @param daemon The loaded server daemon, which exposes what features and properties it supports
+ * Instantiate a torrents comparator. The daemon type is used to check support for comparing on the set property. If
+ * the daemon does not support the property, Alphanumeric sorting will be used even if sorting is requested on the
+ * unsupported property.
+ * @param daemonType The currently loaded server daemon's type, which exposes what features and properties it supports
* @param sortBy The requested sorting property (Alphanumeric is used for unsupported properties that are requested)
* @param reversed If the sorting should be in reverse order
*/
- public TorrentsComparator(IDaemonAdapter daemon, TorrentsSortBy sortBy, boolean reversed) {
+ public TorrentsComparator(Daemon daemonType, TorrentsSortBy sortBy, boolean reversed) {
this.sortBy = sortBy;
this.reversed = reversed;
- switch (sortBy) {
- case DateAdded:
- if (daemon != null && !Daemon.supportsDateAdded(daemon.getType())) {
+ if (sortBy == TorrentsSortBy.DateAdded) {
+ if (daemonType != null && !Daemon.supportsDateAdded(daemonType)) {
// Reset the sorting to simple Alphanumeric
this.sortBy = TorrentsSortBy.Alphanumeric;
this.reversed = false;
}
- break;
}
}
@@ -68,7 +67,7 @@ public class TorrentsComparator implements Comparator {
case Ratio:
return new Double(tor1.getRatio()).compareTo(new Double(tor2.getRatio()));
default:
- return tor1.getName().toLowerCase().compareTo(tor2.getName().toLowerCase());
+ return alphanumComparator.compare(tor1.getName().toLowerCase(), tor2.getName().toLowerCase());
}
} else {
switch (sortBy) {
@@ -83,7 +82,7 @@ public class TorrentsComparator implements Comparator {
case Ratio:
return 0 - new Double(tor1.getRatio()).compareTo(new Double(tor2.getRatio()));
default:
- return 0 - tor1.getName().toLowerCase().compareTo(tor2.getName().toLowerCase());
+ return 0 - alphanumComparator.compare(tor1.getName().toLowerCase(), tor2.getName().toLowerCase());
}
}
}
diff --git a/lib/src/org/transdroid/daemon/Transmission/TransmissionAdapter.java b/lib/src/org/transdroid/daemon/Transmission/TransmissionAdapter.java
index aa4bb4e1..86763e4d 100644
--- a/lib/src/org/transdroid/daemon/Transmission/TransmissionAdapter.java
+++ b/lib/src/org/transdroid/daemon/Transmission/TransmissionAdapter.java
@@ -396,17 +396,20 @@ public class TransmissionAdapter implements IDaemonAdapter {
}
// Execute
+ DLog.d(LOG_NAME, "Execute " + data.getString("method") + " request to " + httppost.getURI().toString());
HttpResponse response = httpclient.execute(httppost);
// Authentication error?
if (response.getStatusLine().getStatusCode() == 401) {
- throw new DaemonException(ExceptionType.AuthenticationFailure, "401 HTTP response (username or password incorrect)");
+ throw new DaemonException(ExceptionType.AuthenticationFailure,
+ "401 HTTP response (username or password incorrect)");
}
// 409 error because of a session id?
if (response.getStatusLine().getStatusCode() == 409) {
// Retry post, but this time with the new session token that was encapsulated in the 409 response
+ DLog.d(LOG_NAME, "Receive HTTP 409 with new session code; now try again for the actual request");
sessionToken = response.getFirstHeader(sessionHeader).getValue();
httppost.addHeader(sessionHeader, sessionToken);
response = httpclient.execute(httppost);
@@ -418,12 +421,13 @@ public class TransmissionAdapter implements IDaemonAdapter {
// Read JSON response
java.io.InputStream instream = entity.getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
+ DLog.d(LOG_NAME,
+ "Received content response starting with "
+ + (result.length() > 100 ? result.substring(0, 100) + "..." : result));
JSONObject json = new JSONObject(result);
instream.close();
- //TLog.d(LOG_NAME, "Success: " + (result.length() > 200? result.substring(0, 200) + "... (" + result.length() + " chars)": result));
-
// Return the JSON object
return json;
}
@@ -457,7 +461,16 @@ public class TransmissionAdapter implements IDaemonAdapter {
* @return The URL of the RPC API
*/
private String buildWebUIUrl() {
- return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + (settings.getFolder() == null? "": settings.getFolder()) + "/transmission/rpc";
+ String folder = "/transmission";
+ if (settings.getFolder() != null && !settings.getFolder().trim().equals("")) {
+ // Allow the user's folder setting to override /transmission (as per Transmission's rpc-url option)
+ folder = settings.getFolder().trim();
+ // Strip any trailing slashes
+ if (folder.endsWith("/"))
+ folder = folder.substring(0, folder.length() - 1);
+ }
+ return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + folder
+ + "/rpc";
}
private ArrayList parseJsonRetrieveTorrents(JSONObject response) throws JSONException {
@@ -500,7 +513,8 @@ public class TransmissionAdapter implements IDaemonAdapter {
null, // No label/category/group support in the RPC API for now
new Date(tor.getLong(RPC_DATEADDED) * 1000L),
new Date(tor.getLong(RPC_DATEDONE) * 1000L),
- errorString));
+ errorString,
+ settings.getType()));
}
// Return the list
diff --git a/lib/src/org/transdroid/daemon/Utorrent/UtorrentAdapter.java b/lib/src/org/transdroid/daemon/Utorrent/UtorrentAdapter.java
index 40b9214b..f95cc856 100644
--- a/lib/src/org/transdroid/daemon/Utorrent/UtorrentAdapter.java
+++ b/lib/src/org/transdroid/daemon/Utorrent/UtorrentAdapter.java
@@ -302,7 +302,7 @@ public class UtorrentAdapter implements IDaemonAdapter {
// Read JSON response
InputStream instream = response.getEntity().getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
if ((result.equals("") || result.trim().equals("invalid request"))) {
authtoken = null;
throw new DaemonException(ExceptionType.AuthenticationFailure, "Response was '" + result.replace("\n", "") + "' instead of a proper JSON object (and we used auth token '" + authtoken + "')");
@@ -341,7 +341,7 @@ public class UtorrentAdapter implements IDaemonAdapter {
throw new DaemonException(ExceptionType.ConnectionError, "Not found (404); server doesn't exist or is inaccessible");
}
InputStream instream = response.getEntity().getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
authtoken = result.replaceAll("\\<.*?>","").trim();
}
@@ -366,7 +366,7 @@ public class UtorrentAdapter implements IDaemonAdapter {
// Read JSON response
InputStream instream = response.getEntity().getContent();
- String result = HttpHelper.ConvertStreamToString(instream);
+ String result = HttpHelper.convertStreamToString(instream);
JSONObject json = new JSONObject(result);
instream.close();
return json;
@@ -515,7 +515,8 @@ public class UtorrentAdapter implements IDaemonAdapter {
addedOnDate,
completedOnDate,
// uTorrent doesn't give the error message, so just remind that there is some error
- status == TorrentStatus.Error? "See GUI for error message": null));
+ status == TorrentStatus.Error? "See GUI for error message": null,
+ settings.getType()));
}
return torrents;
diff --git a/lib/src/org/transdroid/daemon/Vuze/VuzeAdapter.java b/lib/src/org/transdroid/daemon/Vuze/VuzeAdapter.java
index b19ca94d..ba55e981 100644
--- a/lib/src/org/transdroid/daemon/Vuze/VuzeAdapter.java
+++ b/lib/src/org/transdroid/daemon/Vuze/VuzeAdapter.java
@@ -31,6 +31,7 @@ import java.util.Map;
import org.apache.openjpa.lib.util.Base16Encoder;
import org.transdroid.daemon.Daemon;
import org.transdroid.daemon.DaemonException;
+import org.transdroid.daemon.DaemonException.ExceptionType;
import org.transdroid.daemon.DaemonMethod;
import org.transdroid.daemon.DaemonSettings;
import org.transdroid.daemon.IDaemonAdapter;
@@ -38,7 +39,6 @@ import org.transdroid.daemon.Priority;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.TorrentStatus;
-import org.transdroid.daemon.DaemonException.ExceptionType;
import org.transdroid.daemon.task.AddByFileTask;
import org.transdroid.daemon.task.AddByUrlTask;
import org.transdroid.daemon.task.DaemonTask;
@@ -99,18 +99,27 @@ public class VuzeAdapter implements IDaemonAdapter {
case AddByFile:
byte[] bytes;
+ FileInputStream in = null;
try {
// Request to add a torrent by local .torrent file
String file = ((AddByFileTask)task).getFile();
- FileInputStream in = new FileInputStream(new File(URI.create(file)));
+ in = new FileInputStream(new File(URI.create(file)));
bytes = new byte[in.available()];
in.read(bytes, 0, in.available());
+ in.close();
} catch (FileNotFoundException e) {
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString()));
} catch (IllegalArgumentException e) {
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, "Invalid local URI"));
} catch (Exception e) {
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString()));
+ } finally {
+ try {
+ if (in != null)
+ in.close();
+ } catch (IOException e) {
+ // Ignore; it was already closed or never opened
+ }
}
makeVuzeCall(DaemonMethod.AddByFile, "createFromBEncodedData[byte[]]", new String[] { Base16Encoder.encode(bytes) });
return new DaemonTaskSuccessResult(task);
@@ -406,7 +415,8 @@ public class VuzeAdapter implements IDaemonAdapter {
null, // TODO: Implement Vuze label support
new Date((Long) statsinfo.get("time_started")), // dateAdded
null, // Unsupported?
- error));
+ error,
+ settings.getType()));
}
diff --git a/lib/src/org/transdroid/daemon/util/Collections2.java b/lib/src/org/transdroid/daemon/util/Collections2.java
index 97286575..e4a2083d 100644
--- a/lib/src/org/transdroid/daemon/util/Collections2.java
+++ b/lib/src/org/transdroid/daemon/util/Collections2.java
@@ -15,7 +15,7 @@ public class Collections2 {
String result = "";
Iterator it = iterable.iterator();
while (it.hasNext()) {
- result = (first ? "" : separator) + it.next().toString();
+ result += (first ? "" : separator) + it.next().toString();
first = false;
}
return result;
diff --git a/lib/src/org/transdroid/daemon/util/FileSizeConverter.java b/lib/src/org/transdroid/daemon/util/FileSizeConverter.java
index a31c9966..13f18e38 100644
--- a/lib/src/org/transdroid/daemon/util/FileSizeConverter.java
+++ b/lib/src/org/transdroid/daemon/util/FileSizeConverter.java
@@ -15,71 +15,99 @@
* along with Transdroid. If not, see .
*
*/
- package org.transdroid.daemon.util;
+package org.transdroid.daemon.util;
/**
* Quick and dirty file size formatter.
- *
* @author erickok
- *
*/
public class FileSizeConverter {
private static final String DECIMAL_FORMATTER = "%.1f";
-
+
/**
* A quantity in which to express a file size.
- *
* @author erickok
- *
*/
public enum SizeUnit {
- B,
- KB,
- MB,
- GB
+ B, KB, MB, GB
}
-
+
private static int INC_SIZE = 1024;
-
- // Returns a file size given in bytes to a different unit, as a formatted string
- public static String getSize(long from, SizeUnit to)
- {
+
+ /**
+ * Returns a file size (in bytes) in a different unit, as a formatted string
+ * @param from The file size in bytes
+ * @param to The unit to convert to
+ * @return A formatted string with number (rounded to one decimal) and unit, e.g. 1177.4MB
+ */
+ public static String getSize(long from, SizeUnit to) {
String out;
switch (to) {
case B:
out = String.valueOf(from);
break;
case KB:
- out = String.format(DECIMAL_FORMATTER, ((double)from) / 1024);
+ out = String.format(DECIMAL_FORMATTER, ((double) from) / INC_SIZE);
break;
case MB:
- out = String.format(DECIMAL_FORMATTER, ((double)from) / 1024 / 1024);
+ out = String.format(DECIMAL_FORMATTER, ((double) from) / INC_SIZE / INC_SIZE);
break;
default:
- out = String.format(DECIMAL_FORMATTER, ((double)from) / 1024 / 1024 / 1024);
+ out = String.format(DECIMAL_FORMATTER, ((double) from) / INC_SIZE / INC_SIZE / INC_SIZE);
break;
}
-
- return (out + " " + to.toString());
+
+ return (out + " " + to.toString());
}
- // Returns a file size in bytes in a nice readable formatted string
+ /**
+ * Returns a file size as nice readable string, with unit, e.g. 1234567890 (bytes) returns 1,15GB
+ * @param from The file size in bytes
+ * @return A formatted string with number (rounded to one decimal), with unit text
+ */
public static String getSize(long from) {
return getSize(from, true);
}
// Returns a file size in bytes in a nice readable formatted string
+ /**
+ * Returns a file size as nice readable string, e.g. 1234567890 (bytes) returns 1,15 or 1,15GB
+ * @param from The file size in bytes
+ * @param withUnit Whether to also append the appropriate unit (B, KB, MB, GB) as text
+ * @return A formatted string with number (rounded to one decimal) and optionally unit
+ */
public static String getSize(long from, boolean withUnit) {
if (from < INC_SIZE) {
- return String.valueOf(from) + (withUnit? SizeUnit.B.toString(): "");
- } else if (from < (INC_SIZE * INC_SIZE)) {
- return String.format(DECIMAL_FORMATTER, ((double)from) / INC_SIZE) + (withUnit? SizeUnit.KB.toString(): "");
+ return String.valueOf(from) + (withUnit ? SizeUnit.B.toString() : "");
+ } else if (from < (INC_SIZE * INC_SIZE)) {
+ return String.format(DECIMAL_FORMATTER, ((double) from) / INC_SIZE)
+ + (withUnit ? SizeUnit.KB.toString() : "");
} else if (from < (INC_SIZE * INC_SIZE * INC_SIZE)) {
- return String.format(DECIMAL_FORMATTER, ((double)from) / INC_SIZE / INC_SIZE) + (withUnit? SizeUnit.MB.toString(): "");
- } else {
- return String.format(DECIMAL_FORMATTER, ((double)from) / INC_SIZE / INC_SIZE / INC_SIZE) + (withUnit? SizeUnit.GB.toString(): "");
+ return String.format(DECIMAL_FORMATTER, ((double) from) / INC_SIZE / INC_SIZE)
+ + (withUnit ? SizeUnit.MB.toString() : "");
+ } else {
+ return String.format(DECIMAL_FORMATTER, ((double) from) / INC_SIZE / INC_SIZE / INC_SIZE)
+ + (withUnit ? SizeUnit.GB.toString() : "");
}
}
-
+
+ /**
+ * Returns the unit to display some file size (as returned by getSize(long)) in, e.g. 1234567890 (bytes) returns GB
+ * as it is 1.2GB big
+ * @param from The file size in bytes
+ * @return The unit, i.e. B, KB, MB or GB
+ */
+ public static SizeUnit getSizeUnit(long from) {
+ if (from < INC_SIZE) {
+ return SizeUnit.B;
+ } else if (from < (INC_SIZE * INC_SIZE)) {
+ return SizeUnit.KB;
+ } else if (from < (INC_SIZE * INC_SIZE * INC_SIZE)) {
+ return SizeUnit.MB;
+ } else {
+ return SizeUnit.GB;
+ }
+ }
+
}
diff --git a/lib/src/org/transdroid/daemon/util/HttpHelper.java b/lib/src/org/transdroid/daemon/util/HttpHelper.java
index e5c70f98..2698b288 100644
--- a/lib/src/org/transdroid/daemon/util/HttpHelper.java
+++ b/lib/src/org/transdroid/daemon/util/HttpHelper.java
@@ -15,13 +15,16 @@
* along with Transdroid. If not, see .
*
*/
- package org.transdroid.daemon.util;
+package org.transdroid.daemon.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import java.util.zip.GZIPInputStream;
import org.apache.http.Header;
@@ -51,11 +54,11 @@ import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.DaemonException.ExceptionType;
import org.transdroid.daemon.DaemonSettings;
+import android.net.Uri;
+
/**
* Provides a set of general helper methods that can be used in web-based communication.
- *
* @author erickok
- *
*/
public class HttpHelper {
@@ -64,43 +67,48 @@ public class HttpHelper {
public static final String SCHEME_HTTPS = "https";
public static final String SCHEME_MAGNET = "magnet";
public static final String SCHEME_FILE = "file";
-
+
/**
* The 'User-Agent' name to send to the server
*/
public static String userAgent = null;
/**
- * Creates a standard Apache HttpClient that is thread safe, supports different
- * SSL auth methods and basic authentication
+ * Creates a standard Apache HttpClient that is thread safe, supports different SSL auth methods and basic
+ * authentication
* @param settings The server settings to adhere
* @return An HttpClient that should be stored locally and reused for every new request
* @throws DaemonException Thrown when information (such as username/password) is missing
*/
- public static DefaultHttpClient createStandardHttpClient(DaemonSettings settings, boolean userBasicAuth) throws DaemonException {
- return createStandardHttpClient(userBasicAuth && settings.shouldUseAuthentication(), settings.getUsername(), settings.getPassword(), settings.getSslTrustAll(), settings.getSslTrustKey(), settings.getTimeoutInMilliseconds(), settings.getAddress(), settings.getPort());
+ public static DefaultHttpClient createStandardHttpClient(DaemonSettings settings, boolean userBasicAuth)
+ throws DaemonException {
+ return createStandardHttpClient(userBasicAuth && settings.shouldUseAuthentication(), settings.getUsername(),
+ settings.getPassword(), settings.getSslTrustAll(), settings.getSslTrustKey(),
+ settings.getTimeoutInMilliseconds(), settings.getAddress(), settings.getPort());
}
/**
- * Creates a standard Apache HttpClient that is thread safe, supports different
- * SSL auth methods and basic authentication
+ * Creates a standard Apache HttpClient that is thread safe, supports different SSL auth methods and basic
+ * authentication
* @param sslTrustAll Whether to trust all SSL certificates
* @param sslTrustkey A specific SSL key to accept exclusively
* @param timeout The connection timeout for all requests
- * @param authAddress The authentication domain address
+ * @param authAddress The authentication domain address
* @param authPort The authentication domain port number
* @return An HttpClient that should be stored locally and reused for every new request
* @throws DaemonException Thrown when information (such as username/password) is missing
*/
- public static DefaultHttpClient createStandardHttpClient(boolean userBasicAuth, String username, String password, boolean sslTrustAll, String sslTrustkey, int timeout, String authAddress, int authPort) throws DaemonException {
+ public static DefaultHttpClient createStandardHttpClient(boolean userBasicAuth, String username, String password,
+ boolean sslTrustAll, String sslTrustkey, int timeout, String authAddress, int authPort)
+ throws DaemonException {
// Register http and https sockets
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", new PlainSocketFactory(), 80));
SocketFactory https_socket = sslTrustAll ? new FakeSocketFactory()
- : sslTrustkey != null ? new FakeSocketFactory(sslTrustkey) : SSLSocketFactory.getSocketFactory();
- registry.register(new Scheme("https", https_socket, 443));
-
+ : sslTrustkey != null ? new FakeSocketFactory(sslTrustkey) : SSLSocketFactory.getSocketFactory();
+ registry.register(new Scheme("https", https_socket, 443));
+
// Standard parameters
HttpParams httpparams = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpparams, timeout);
@@ -108,124 +116,155 @@ public class HttpHelper {
if (userAgent != null) {
HttpProtocolParams.setUserAgent(httpparams, userAgent);
}
-
- DefaultHttpClient httpclient = new DefaultHttpClient(new ThreadSafeClientConnManager(httpparams, registry), httpparams);
-
+
+ DefaultHttpClient httpclient = new DefaultHttpClient(new ThreadSafeClientConnManager(httpparams, registry),
+ httpparams);
+
// Authentication credentials
if (userBasicAuth) {
if (username == null || password == null) {
- throw new DaemonException(ExceptionType.AuthenticationFailure, "No username or password was provided while we hadauthentication enabled");
+ throw new DaemonException(ExceptionType.AuthenticationFailure,
+ "No username or password was provided while we hadauthentication enabled");
}
httpclient.getCredentialsProvider().setCredentials(
- new AuthScope(authAddress, authPort, AuthScope.ANY_REALM),
- new UsernamePasswordCredentials(username, password));
+ new AuthScope(authAddress, authPort, AuthScope.ANY_REALM),
+ new UsernamePasswordCredentials(username, password));
}
-
+
return httpclient;
-
+
}
-
+
/**
- * HTTP request interceptor to allow for GZip-encoded data transfer
+ * HTTP request interceptor to allow for GZip-encoded data transfer
*/
public static HttpRequestInterceptor gzipRequestInterceptor = new HttpRequestInterceptor() {
- public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
- if (!request.containsHeader("Accept-Encoding")) {
- request.addHeader("Accept-Encoding", "gzip");
- }
- }
- };
-
- /**
- * HTTP response interceptor that decodes GZipped data
- */
- public static HttpResponseInterceptor gzipResponseInterceptor = new HttpResponseInterceptor() {
- public void process(final HttpResponse response, final HttpContext context) throws HttpException, IOException {
- HttpEntity entity = response.getEntity();
- Header ceheader = entity.getContentEncoding();
- if (ceheader != null) {
- HeaderElement[] codecs = ceheader.getElements();
- for (int i = 0; i < codecs.length; i++) {
-
- if (codecs[i].getName().equalsIgnoreCase("gzip")) {
- response.setEntity(new HttpHelper.GzipDecompressingEntity(response.getEntity()));
- return;
- }
- }
- }
- }
-
- };
-
- /**
- * HTTP entity wrapper to decompress GZipped HTTP responses
- */
+ public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
+ if (!request.containsHeader("Accept-Encoding")) {
+ request.addHeader("Accept-Encoding", "gzip");
+ }
+ }
+ };
+
+ /**
+ * HTTP response interceptor that decodes GZipped data
+ */
+ public static HttpResponseInterceptor gzipResponseInterceptor = new HttpResponseInterceptor() {
+ public void process(final HttpResponse response, final HttpContext context) throws HttpException, IOException {
+ HttpEntity entity = response.getEntity();
+ Header ceheader = entity.getContentEncoding();
+ if (ceheader != null) {
+ HeaderElement[] codecs = ceheader.getElements();
+ for (int i = 0; i < codecs.length; i++) {
+
+ if (codecs[i].getName().equalsIgnoreCase("gzip")) {
+ response.setEntity(new HttpHelper.GzipDecompressingEntity(response.getEntity()));
+ return;
+ }
+ }
+ }
+ }
+
+ };
+
+ /**
+ * HTTP entity wrapper to decompress GZipped HTTP responses
+ */
private static class GzipDecompressingEntity extends HttpEntityWrapper {
-
- public GzipDecompressingEntity(final HttpEntity entity) {
- super(entity);
- }
-
- @Override
- public InputStream getContent() throws IOException, IllegalStateException {
-
- // the wrapped entity's getContent() decides about repeatability
- InputStream wrappedin = wrappedEntity.getContent();
-
- return new GZIPInputStream(wrappedin);
- }
-
- @Override
- public long getContentLength() {
- // length of ungzipped content is not known
- return -1;
- }
-
+
+ public GzipDecompressingEntity(final HttpEntity entity) {
+ super(entity);
+ }
+
+ @Override
+ public InputStream getContent() throws IOException, IllegalStateException {
+
+ // the wrapped entity's getContent() decides about repeatability
+ InputStream wrappedin = wrappedEntity.getContent();
+
+ return new GZIPInputStream(wrappedin);
+ }
+
+ @Override
+ public long getContentLength() {
+ // length of ungzipped content is not known
+ return -1;
+ }
+
+ }
+
+ /*
+ * To convert the InputStream to String we use the BufferedReader.readLine() method. We iterate until the
+ * BufferedReader return null which means there's no more data to read. Each line will appended to a StringBuilder
+ * and returned as String.
+ *
+ * Taken from http://senior.ceng.metu.edu.tr/2009/praeda/2009/01/11/a-simple-restful-client-at-android/
+ */
+ public static String convertStreamToString(InputStream is, String encoding) throws UnsupportedEncodingException {
+ InputStreamReader isr;
+ if (encoding != null) {
+ isr = new InputStreamReader(is, encoding);
+ } else {
+ isr = new InputStreamReader(is);
+ }
+ BufferedReader reader = new BufferedReader(isr);
+ StringBuilder sb = new StringBuilder();
+
+ String line = null;
+ try {
+ while ((line = reader.readLine()) != null) {
+ sb.append(line + "\n");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ is.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return sb.toString();
}
- /*
- * To convert the InputStream to String we use the BufferedReader.readLine()
- * method. We iterate until the BufferedReader return null which means
- * there's no more data to read. Each line will appended to a StringBuilder
- * and returned as String.
- *
- * Taken from http://senior.ceng.metu.edu.tr/2009/praeda/2009/01/11/a-simple-restful-client-at-android/
- */
- public static String ConvertStreamToString(InputStream is, String encoding) throws UnsupportedEncodingException {
- InputStreamReader isr;
- if (encoding != null) {
- isr = new InputStreamReader(is, encoding);
- } else {
- isr = new InputStreamReader(is);
- }
- BufferedReader reader = new BufferedReader(isr);
- StringBuilder sb = new StringBuilder();
-
- String line = null;
- try {
- while ((line = reader.readLine()) != null) {
- sb.append(line + "\n");
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- is.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- return sb.toString();
- }
-
- public static String ConvertStreamToString(InputStream is) {
- try {
- return ConvertStreamToString(is, null);
+ public static String convertStreamToString(InputStream is) {
+ try {
+ return convertStreamToString(is, null);
} catch (UnsupportedEncodingException e) {
- // Since this is going to use the default encoding, it is never going to crash on an UnsupportedEncodingException
+ // Since this is going to use the default encoding, it is never going to crash on an
+ // UnsupportedEncodingException
e.printStackTrace();
return null;
}
- }
-
+ }
+
+ /**
+ * Parses the individual parameters from a textual cookie representation-like string and returns them in an unsorted
+ * map. Inspired by Android's (API level 11+) getQueryParameterNames(Uri).
+ * @param raw A string of the form key1=value1;key2=value
+ * @return An unsorted, unmodifiable map of pairs of string keys and string values
+ */
+ public static Map parseCookiePairs(String raw) {
+
+ Map pairs = new HashMap();
+ int start = 0;
+ do {
+ int next = raw.indexOf(';', start);
+ int end = (next == -1) ? raw.length() : next;
+ int separator = raw.indexOf('=', start);
+ if (separator > end || separator == -1) {
+ separator = end;
+ }
+
+ String name = raw.substring(start, separator);
+ String value = raw.substring(separator + 1, end);
+ pairs.put(Uri.decode(name), Uri.decode(value));
+
+ start = end + 1;
+ } while (start < raw.length());
+
+ return Collections.unmodifiableMap(pairs);
+
+ }
+
}
\ No newline at end of file
diff --git a/lib/src/org/xmlrpc/android/XMLRPCClient.java b/lib/src/org/xmlrpc/android/XMLRPCClient.java
index 08ba4451..48320585 100644
--- a/lib/src/org/xmlrpc/android/XMLRPCClient.java
+++ b/lib/src/org/xmlrpc/android/XMLRPCClient.java
@@ -14,6 +14,8 @@ import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
+import org.transdroid.daemon.DaemonException;
+import org.transdroid.daemon.DaemonMethod;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
@@ -319,6 +321,9 @@ public class XMLRPCClient {
// check status code
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
+ if (statusCode == HttpStatus.SC_UNAUTHORIZED)
+ throw new DaemonException(DaemonException.ExceptionType.AuthenticationFailure, "HTTP status code: "
+ + statusCode + " != " + HttpStatus.SC_OK);
throw new XMLRPCException("HTTP status code: " + statusCode + " != " + HttpStatus.SC_OK);
}
diff --git a/lite/.classpath b/lite/.classpath
new file mode 100644
index 00000000..7bc01d9a
--- /dev/null
+++ b/lite/.classpath
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/lite/.project b/lite/.project
new file mode 100644
index 00000000..e65b9dd2
--- /dev/null
+++ b/lite/.project
@@ -0,0 +1,33 @@
+
+
+ Transdroid Lite
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/lite/AndroidManifest.xml b/lite/AndroidManifest.xml
new file mode 100644
index 00000000..9a290bc3
--- /dev/null
+++ b/lite/AndroidManifest.xml
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lite/proguard-project.txt b/lite/proguard-project.txt
new file mode 100644
index 00000000..f2fe1559
--- /dev/null
+++ b/lite/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/lite/project.properties b/lite/project.properties
new file mode 100644
index 00000000..4bc32995
--- /dev/null
+++ b/lite/project.properties
@@ -0,0 +1,15 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-18
+android.library.reference.1=../core
diff --git a/lite/res/values/strings.xml b/lite/res/values/strings.xml
new file mode 100644
index 00000000..e9e12ebd
--- /dev/null
+++ b/lite/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ Transdroid Lite
+
+
\ No newline at end of file