diff --git a/README b/README
index 2f989bf7..7ab734cd 100644
--- a/README
+++ b/README
@@ -20,21 +20,45 @@ 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
+ 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/org/transdroid/gui/TorrentsFragment.java b/android/src/org/transdroid/gui/TorrentsFragment.java
index d92653b7..a9ae176e 100644
--- a/android/src/org/transdroid/gui/TorrentsFragment.java
+++ b/android/src/org/transdroid/gui/TorrentsFragment.java
@@ -1749,7 +1749,7 @@ public class TorrentsFragment extends SherlockFragment implements IDaemonCallbac
// Sort the new list of torrents
allTorrents = ((RetrieveTaskSuccessResult) result).getTorrents();
- Collections.sort(allTorrents, new TorrentsComparator(daemon, sortSetting, sortReversed));
+ Collections.sort(allTorrents, new TorrentsComparator(daemon.getType(), sortSetting, sortReversed));
// Sort the new list of labels
allLabels = ((RetrieveTaskSuccessResult) result).getLabels();
@@ -1936,7 +1936,7 @@ public class TorrentsFragment extends SherlockFragment implements IDaemonCallbac
if (!(getTorrentListAdapter() == null || getTorrentListAdapter().getCount() == 0)) {
// Sort the shown list of torrents using the new sortBy criteria
- Collections.sort(allTorrents, new TorrentsComparator(daemon, sortSetting, sortReversed));
+ Collections.sort(allTorrents, new TorrentsComparator(daemon.getType(), sortSetting, sortReversed));
updateTorrentsView(true);
}
diff --git a/core/.classpath b/core/.classpath
new file mode 100644
index 00000000..68860191
--- /dev/null
+++ b/core/.classpath
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..e30ef514
--- /dev/null
+++ b/core/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
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/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/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..0a043162
--- /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-17
+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/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/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/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/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/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/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/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/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/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_torrents.xml b/core/res/layout-w600dp/activity_torrents.xml
new file mode 100644
index 00000000..746cc9e0
--- /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_torrents.xml b/core/res/layout-w900dp/activity_torrents.xml
new file mode 100644
index 00000000..a686fff0
--- /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_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_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/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_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_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..0a62b6d7
--- /dev/null
+++ b/core/res/layout/list_item_simple.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ 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..cbfb2613
--- /dev/null
+++ b/core/res/menu/activity_details.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ 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..b275c63f
--- /dev/null
+++ b/core/res/menu/activity_torrents.xml
@@ -0,0 +1,66 @@
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..ac38581d
--- /dev/null
+++ b/core/res/menu/fragment_details.xml
@@ -0,0 +1,49 @@
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/res/menu/fragment_details_file.xml b/core/res/menu/fragment_details_file.xml
new file mode 100644
index 00000000..d0f4a8ba
--- /dev/null
+++ b/core/res/menu/fragment_details_file.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
\ 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..b3cef559
--- /dev/null
+++ b/core/res/values-v16/styles.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..a6b4dcbc
--- /dev/null
+++ b/core/res/values/changelog.xml
@@ -0,0 +1,10 @@
+
+
+
+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/
+
+
\ No newline at end of file
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..9ee708af
--- /dev/null
+++ b/core/res/values/colors_transdroid_dark.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ #CCaada62
+ #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..f3a6aee3
--- /dev/null
+++ b/core/res/values/colors_transdroid_light.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ #CC8ACC12
+ #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..a5608379
--- /dev/null
+++ b/core/res/values/strings.xml
@@ -0,0 +1,286 @@
+
+
+
+ Add
+ 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
+ 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 ↓
+ Downloading
+ Seeding
+ Paused
+ Queued
+ Stopped
+ Unknown status
+ Not downloaded
+ Low priority
+ Normal priority
+ High priority
+ TRACKERS
+ ERRORS
+ FILES
+
+ All labels
+ Unlabeled
+ New label
+ Setting a label is not supported by your client
+
+ %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\'
+ Torrent moved to \'%1$s\'
+ File priorities updated
+
+ Torrent search
+ Search for torrents
+ 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?
+
+ 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
+ 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
+ - Ktorrent
+ - qBittorrent
+ - rTorrent
+ - Torrentflux-b4rt
+ - Transmission
+ - µTorrent
+ - Vuze
+
+
+ - daemon_bitcomet
+ - daemon_bitflu
+ - daemon_bittorrent
+ - daemon_buffalonas
+ - daemon_deluge
+ - daemon_dlinkrouterbt
+ - 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
+
+ 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..1d68bca0
--- /dev/null
+++ b/core/res/values/styles.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..f2efdec1
--- /dev/null
+++ b/core/res/xml/pref_websearch.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
\ 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..9f811c8c
--- /dev/null
+++ b/core/res/xml/searchable.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/core/src/com/actionbarsherlock/view/SherlockListView.java b/core/src/com/actionbarsherlock/view/SherlockListView.java
new file mode 100644
index 00000000..3b2df828
--- /dev/null
+++ b/core/src/com/actionbarsherlock/view/SherlockListView.java
@@ -0,0 +1,337 @@
+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) {
+ 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/com/commonsware/cwac/merge/MergeAdapter.java b/core/src/com/commonsware/cwac/merge/MergeAdapter.java
new file mode 100644
index 00000000..a713b862
--- /dev/null
+++ b/core/src/com/commonsware/cwac/merge/MergeAdapter.java
@@ -0,0 +1,481 @@
+/***
+ Copyright (c) 2008-2009 CommonsWare, LLC
+ Portions (c) 2009 Google, Inc.
+
+ 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 com.commonsware.cwac.merge;
+
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+import android.widget.SectionIndexer;
+import java.util.ArrayList;
+import java.util.List;
+import com.commonsware.cwac.sacklist.SackOfViewsAdapter;
+
+/**
+ * Adapter that merges multiple child adapters and views
+ * into a single contiguous whole.
+ *
+ * Adapters used as pieces within MergeAdapter must have
+ * view type IDs monotonically increasing from 0. Ideally,
+ * adapters also have distinct ranges for their row ids, as
+ * returned by getItemId().
+ *
+ */
+public class MergeAdapter extends BaseAdapter implements SectionIndexer {
+ protected PieceStateRoster pieces=new PieceStateRoster();
+
+ /**
+ * 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());
+ }
+
+ /**
+ * Adds a new View to the roster of things to appear in
+ * the aggregate list.
+ *
+ * @param view
+ * Single view to add
+ */
+ public void addView(View view) {
+ addView(view, false);
+ }
+
+ /**
+ * Adds a new View to the roster of things to appear in
+ * the aggregate list.
+ *
+ * @param view
+ * Single view to add
+ * @param enabled
+ * false if views are disabled, true if enabled
+ */
+ public void addView(View view, boolean enabled) {
+ ArrayList list=new ArrayList(1);
+
+ list.add(view);
+
+ addViews(list, enabled);
+ }
+
+ /**
+ * Adds a list of views to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param views
+ * List of views to add
+ */
+ public void addViews(List views) {
+ addViews(views, false);
+ }
+
+ /**
+ * Adds a list of views to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param views
+ * List of views to add
+ * @param enabled
+ * false if views are disabled, true if enabled
+ */
+ public void addViews(List views, boolean enabled) {
+ if (enabled) {
+ addAdapter(new EnabledSackAdapter(views));
+ }
+ else {
+ addAdapter(new SackOfViewsAdapter(views));
+ }
+ }
+
+ /**
+ * Get the data item associated with the specified
+ * position in the data set.
+ *
+ * @param position
+ * Position of the item whose data we want
+ */
+ @Override
+ public Object getItem(int position) {
+ for (ListAdapter piece : getPieces()) {
+ int size=piece.getCount();
+
+ if (position < size) {
+ return(piece.getItem(position));
+ }
+
+ position-=size;
+ }
+
+ return(null);
+ }
+
+ /**
+ * 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 : getPieces()) {
+ int size=piece.getCount();
+
+ if (position < size) {
+ return(piece);
+ }
+
+ position-=size;
+ }
+
+ return(null);
+ }
+
+ /**
+ * How many items are in the data set represented by this
+ * Adapter.
+ */
+ @Override
+ public int getCount() {
+ int total=0;
+
+ for (ListAdapter piece : getPieces()) {
+ total+=piece.getCount();
+ }
+
+ return(total);
+ }
+
+ /**
+ * Returns the number of types of Views that will be
+ * created by getView().
+ */
+ @Override
+ public int getViewTypeCount() {
+ int total=0;
+
+ for (PieceState piece : pieces.getRawPieces()) {
+ total+=piece.adapter.getViewTypeCount();
+ }
+
+ return(Math.max(total, 1)); // needed for
+ // setListAdapter() before
+ // content add'
+ }
+
+ /**
+ * Get the type of View that will be created by getView()
+ * 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 (PieceState piece : pieces.getRawPieces()) {
+ if (piece.isActive) {
+ int size=piece.adapter.getCount();
+
+ if (position < size) {
+ result=typeOffset + piece.adapter.getItemViewType(position);
+ break;
+ }
+
+ position-=size;
+ }
+
+ typeOffset+=piece.adapter.getViewTypeCount();
+ }
+
+ return(result);
+ }
+
+ /**
+ * Are all items in this 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 : getPieces()) {
+ int size=piece.getCount();
+
+ if (position < size) {
+ return(piece.isEnabled(position));
+ }
+
+ position-=size;
+ }
+
+ return(false);
+ }
+
+ /**
+ * Get a 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
+ */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ for (ListAdapter piece : getPieces()) {
+ int size=piece.getCount();
+
+ if (position < size) {
+
+ return(piece.getView(position, convertView, parent));
+ }
+
+ position-=size;
+ }
+
+ return(null);
+ }
+
+ /**
+ * Get the row id associated with the specified position
+ * in the list.
+ *
+ * @param position
+ * Position of the item whose data we want
+ */
+ @Override
+ public long getItemId(int position) {
+ for (ListAdapter piece : getPieces()) {
+ int size=piece.getCount();
+
+ if (position < size) {
+ return(piece.getItemId(position));
+ }
+
+ position-=size;
+ }
+
+ return(-1);
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ int position=0;
+
+ for (ListAdapter piece : getPieces()) {
+ 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);
+ }
+
+ @Override
+ public int getSectionForPosition(int position) {
+ int section=0;
+
+ for (ListAdapter piece : getPieces()) {
+ 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);
+ }
+
+ @Override
+ public Object[] getSections() {
+ ArrayList sections=new ArrayList();
+
+ for (ListAdapter piece : getPieces()) {
+ if (piece instanceof SectionIndexer) {
+ Object[] curSections=((SectionIndexer)piece).getSections();
+
+ if (curSections != null) {
+ for (Object section : curSections) {
+ sections.add(section);
+ }
+ }
+ }
+ }
+
+ if (sections.size() == 0) {
+ return(new String[0]);
+ }
+
+ return(sections.toArray(new Object[0]));
+ }
+
+ public void setActive(ListAdapter adapter, boolean isActive) {
+ pieces.setActive(adapter, isActive);
+ notifyDataSetChanged();
+ }
+
+ public void setActive(View v, boolean isActive) {
+ pieces.setActive(v, isActive);
+ notifyDataSetChanged();
+ }
+
+ protected List getPieces() {
+ return(pieces.getPieces());
+ }
+
+ private static class PieceState {
+ ListAdapter adapter;
+ boolean isActive=true;
+
+ PieceState(ListAdapter adapter, boolean isActive) {
+ this.adapter=adapter;
+ this.isActive=isActive;
+ }
+ }
+
+ private static class PieceStateRoster {
+ protected ArrayList pieces=new ArrayList();
+ protected ArrayList active=null;
+
+ void add(ListAdapter adapter) {
+ pieces.add(new PieceState(adapter, true));
+ }
+
+ void setActive(ListAdapter adapter, boolean isActive) {
+ for (PieceState state : pieces) {
+ if (state.adapter==adapter) {
+ state.isActive=isActive;
+ active=null;
+ break;
+ }
+ }
+ }
+
+ void setActive(View v, boolean isActive) {
+ for (PieceState state : pieces) {
+ if (state.adapter instanceof SackOfViewsAdapter &&
+ ((SackOfViewsAdapter)state.adapter).hasView(v)) {
+ state.isActive=isActive;
+ active=null;
+ break;
+ }
+ }
+ }
+
+ List getRawPieces() {
+ return(pieces);
+ }
+
+ List getPieces() {
+ if (active == null) {
+ active=new ArrayList();
+
+ for (PieceState state : pieces) {
+ if (state.isActive) {
+ active.add(state.adapter);
+ }
+ }
+ }
+
+ return(active);
+ }
+ }
+
+ private static class EnabledSackAdapter extends SackOfViewsAdapter {
+ public EnabledSackAdapter(List views) {
+ super(views);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return(true);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return(true);
+ }
+ }
+
+ 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/com/commonsware/cwac/sacklist/SackOfViewsAdapter.java b/core/src/com/commonsware/cwac/sacklist/SackOfViewsAdapter.java
new file mode 100644
index 00000000..2d248e58
--- /dev/null
+++ b/core/src/com/commonsware/cwac/sacklist/SackOfViewsAdapter.java
@@ -0,0 +1,177 @@
+/***
+ Copyright (c) 2008-2009 CommonsWare, LLC
+ Portions (c) 2009 Google, Inc.
+
+ 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 com.commonsware.cwac.sacklist;
+
+import java.util.ArrayList;
+import java.util.List;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * Adapter that simply returns row views from a list.
+ *
+ * If you supply a size, you must implement newView(), to
+ * create a required view. The adapter will then cache these
+ * views.
+ *
+ * If you supply a list of views in the constructor, that
+ * list will be used directly. If any elements in the list
+ * are null, then newView() will be called just for those
+ * slots.
+ *
+ * Subclasses may also wish to override areAllItemsEnabled()
+ * (default: false) and isEnabled() (default: false), if some
+ * of their rows should be selectable.
+ *
+ * It is assumed each view is unique, and therefore will not
+ * get recycled.
+ *
+ * Note that this adapter is not designed for long lists. It
+ * is more for screens that should behave like a list. This
+ * is particularly useful if you combine this with other
+ * adapters (e.g., SectionedAdapter) that might have an
+ * arbitrary number of rows, so it all appears seamless.
+ */
+public class SackOfViewsAdapter extends BaseAdapter {
+ private List views=null;
+
+ /**
+ * Constructor creating an empty list of views, but with
+ * a specified count. Subclasses must override newView().
+ */
+ public SackOfViewsAdapter(int count) {
+ super();
+
+ views=new ArrayList(count);
+
+ for (int i=0;i views) {
+ super();
+
+ this.views=views;
+ }
+
+ /**
+ * Get the data item associated with the specified
+ * position in the data set.
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public Object getItem(int position) {
+ return(views.get(position));
+ }
+
+ /**
+ * How many items are in the data set represented by this
+ * Adapter.
+ */
+ @Override
+ public int getCount() {
+ return(views.size());
+ }
+
+ /**
+ * Returns the number of types of Views that will be
+ * created by getView().
+ */
+ @Override
+ public int getViewTypeCount() {
+ return(getCount());
+ }
+
+ /**
+ * Get the type of View that will be created by getView()
+ * for the specified item.
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public int getItemViewType(int position) {
+ return(position);
+ }
+
+ /**
+ * Are all items in this 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) {
+ return(false);
+ }
+
+ /**
+ * Get a 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
+ */
+ @Override
+ public View getView(int position, View convertView,
+ ViewGroup parent) {
+ View result=views.get(position);
+
+ if (result==null) {
+ result=newView(position, parent);
+ views.set(position, result);
+ }
+
+ return(result);
+ }
+
+ /**
+ * Get the row id associated with the specified position
+ * in the list.
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public long getItemId(int position) {
+ return(position);
+ }
+
+ public boolean hasView(View v) {
+ return(views.contains(v));
+ }
+
+ /**
+ * Create a new View to go into the list at the specified
+ * position.
+ * @param position Position of the item whose data we want
+ * @param parent ViewGroup containing the returned View
+ */
+ protected View newView(int position, ViewGroup parent) {
+ throw new RuntimeException("You must override newView()!");
+ }
+}
\ No newline at end of file
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..d681deba
--- /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..1eca6cb5
--- /dev/null
+++ b/core/src/org/transdroid/core/app/search/SearchHelper.java
@@ -0,0 +1,116 @@
+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 synchornous 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
+ */
+ public List 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.moveToFirst()) {
+ List 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..6bbfaa76
--- /dev/null
+++ b/core/src/org/transdroid/core/app/search/SearchResult.java
@@ -0,0 +1,64 @@
+package org.transdroid.core.app.search;
+
+import java.util.Date;
+
+/**
+ * Represents a search result as retrieved by querying the Torrent Search package.
+ * @author Eric Kok
+ */
+public class SearchResult {
+
+ 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;
+ }
+
+}
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..a30ad09e
--- /dev/null
+++ b/core/src/org/transdroid/core/app/search/SearchSite.java
@@ -0,0 +1,40 @@
+package org.transdroid.core.app.search;
+
+import org.transdroid.core.gui.lists.SimpleListItem;
+
+/**
+ * Represents an available torrent site that can be searched using the Torrent Search package.
+ * @author Eric Kok
+ */
+public class SearchSite implements SimpleListItem {
+
+ 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;
+ }
+
+}
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..6d376232
--- /dev/null
+++ b/core/src/org/transdroid/core/app/settings/ApplicationSettings.java
@@ -0,0 +1,356 @@
+package org.transdroid.core.app.settings;
+
+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 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;
+
+ 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 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 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));
+ // @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));
+ }
+
+ // 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.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 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
+ return new RssfeedSetting(order,
+ prefs.getString("rssfeed_name_" + order, null),
+ prefs.getString("rssfeed_url_" + order, null),
+ prefs.getBoolean("rssfeed_reqauth_" + order, false),
+ prefs.getString("rssfeed_lastnew_" + order, null));
+ // @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.putString("rssfeed_lastnew_" + i, prefs.getString("rssfeed_lastnew_" + (i + 1), null));
+ }
+
+ // 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_lastnew_" + max);
+ edit.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);
+ }
+
+}
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..58bd4003
--- /dev/null
+++ b/core/src/org/transdroid/core/app/settings/NotificationSettings.java
@@ -0,0 +1,97 @@
+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);
+ }
+
+ 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..cd90f932
--- /dev/null
+++ b/core/src/org/transdroid/core/app/settings/RssfeedSetting.java
@@ -0,0 +1,80 @@
+package org.transdroid.core.app.settings;
+
+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 String lastNew;
+
+ public RssfeedSetting(int order, String name, String baseUrl, boolean needsAuth, String lastNew) {
+ this.order = order;
+ this.name = name;
+ this.url = baseUrl;
+ this.requiresAuth = needsAuth;
+ this.lastNew = lastNew;
+ }
+
+ 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
+ * @return The last new item's URL as URL-encoded string
+ */
+ public String getLastNew() {
+ // TODO: Persist this into Preferences
+ return this.lastNew;
+ }
+
+ /**
+ * Record the URL of what is now the last item we retrieved
+ * @param lastNew The URL of the last new item as URL-encoded string
+ */
+ public void setLastNew(String lastNew) {
+ this.lastNew = lastNew;
+ }
+
+ /**
+ * 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..2b8690f0
--- /dev/null
+++ b/core/src/org/transdroid/core/app/settings/ServerSetting.java
@@ -0,0 +1,236 @@
+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;
+ }
+
+ 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() : "");
+ }
+
+ @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..f860cf09
--- /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..76923215
--- /dev/null
+++ b/core/src/org/transdroid/core/app/settings/WebsearchSetting.java
@@ -0,0 +1,58 @@
+package org.transdroid.core.app.settings;
+
+import org.transdroid.core.gui.lists.SimpleListItem;
+
+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 {
+
+ 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;
+
+ public WebsearchSetting(int order, String name, String baseUrl) {
+ this.order = order;
+ this.name = name;
+ this.baseUrl = baseUrl;
+ }
+
+ 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 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..ce514a90
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/DetailsActivity.java
@@ -0,0 +1,309 @@
+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.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 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;
+
+ // 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) {
+ 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);
+
+ }
+
+ @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) {
+ onTaskSucceeded(
+ (DaemonTaskSuccessResult) result,
+ getString(withData ? R.string.result_removed_with_data : R.string.result_removed, torrent.getName()));
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result);
+ }
+ }
+
+ @Background
+ @Override
+ public void updateLabel(Torrent torrent, String newLabel) {
+ torrent.mimicNewLabel(newLabel);
+ DaemonTaskResult result = SetLabelTask.create(currentConnection, torrent, 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
+ fragmentDetails.updateIsLoading(false);
+ fragmentDetails.perhapsUpdateTorrent(torrents);
+ }
+
+}
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..c86f9b51
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/DetailsFragment.java
@@ -0,0 +1,325 @@
+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.FragmentArg;
+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.gui.lists.DetailsAdapter;
+import org.transdroid.core.gui.lists.SimpleListItemAdapter;
+import org.transdroid.core.gui.navigation.NavigationHelper;
+import org.transdroid.core.gui.navigation.SelectionManagerMode;
+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 {
+
+ // Local data
+ @InstanceState
+ @FragmentArg
+ protected Torrent torrent = null;
+ @InstanceState
+ protected TorrentDetails torrentDetails = null;
+ @InstanceState
+ protected ArrayList torrentFiles = 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() {
+
+ // 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;
+ }
+ }
+ }
+
+ /**
+ * 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() {
+ // TODO: Show label selection dialog
+ }
+
+ @OptionsItem(resName = "action_updatetrackers")
+ protected void updateTrackers() {
+ // TODO: Show trackers edit dialog
+ }
+
+ 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_file, 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/SearchHistoryProvider.java b/core/src/org/transdroid/core/gui/SearchHistoryProvider.java
new file mode 100644
index 00000000..4a85ebfa
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/SearchHistoryProvider.java
@@ -0,0 +1,26 @@
+package org.transdroid.core.gui;
+
+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.SearchHistoryProvider";
+ public final static int MODE = DATABASE_MODE_QUERIES;
+
+ public SearchHistoryProvider() {
+ super();
+ 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/ServerStatusView.java b/core/src/org/transdroid/core/gui/ServerStatusView.java
new file mode 100644
index 00000000..8bbda367
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/ServerStatusView.java
@@ -0,0 +1,65 @@
+package org.transdroid.core.gui;
+
+import java.util.List;
+
+import org.androidannotations.annotations.EViewGroup;
+import org.androidannotations.annotations.ViewById;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.TorrentStatus;
+import org.transdroid.daemon.util.FileSizeConverter;
+
+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 {
+
+ @ViewById
+ protected TextView downcountText, upcountText, downcountSign, upcountSign, downspeedText, upspeedText;
+
+ public ServerStatusView(Context context) {
+ super(context);
+ }
+
+ /**
+ * 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);
+ }
+
+ 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);
+
+ }
+
+}
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..1d6a10d4
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/TorrentsActivity.java
@@ -0,0 +1,841 @@
+package org.transdroid.core.gui;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+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.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.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.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.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.StartTask;
+import org.transdroid.daemon.task.StopTask;
+import org.transdroid.daemon.util.DLog;
+
+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;
+
+ // 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
+ ServerSetting lastUsed = applicationSettings.getLastUsedServer();
+ if (lastUsed == null) {
+ // No server settings yet;
+ return;
+ }
+ // 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) {
+ handleStartIntent();
+ }
+
+ }
+
+ @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);
+ 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")) {
+ String title = data.substring(data.lastIndexOf("/"));
+ 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
+ if (dataUri.getScheme().equals("file")) {
+ String title = data.substring(data.lastIndexOf("/"));
+ 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_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).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 DaemonTaskResult) {
+ 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 DaemonTaskResult) {
+ 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 DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_added, title));
+ refreshTorrents();
+ } else {
+ onCommunicationError((DaemonTaskFailureResult) result, false);
+ }
+ }
+
+ private void addTorrentFromDownloads(Uri contentUri) {
+
+ InputStream input = null;
+ try {
+ // Open the content uri as input stream
+ input = getContentResolver().openInputStream(contentUri);
+
+ // Write a temporary file with the torrent contents
+ File 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 (SecurityException e) {
+ // No longer access to this file
+ Crouton.showText(this, R.string.error_torrentfile, NavigationHelper.CROUTON_ERROR_STYLE);
+ } catch (IOException e1) {
+ // Can't write temporary file
+ Crouton.showText(this, R.string.error_torrentfile, NavigationHelper.CROUTON_ERROR_STYLE);
+ } finally {
+ try {
+ if (input != null)
+ input.close();
+ } catch (IOException e) {
+ 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 DaemonTaskResult) {
+ 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).execute();
+ if (result instanceof DaemonTaskResult) {
+ onTaskSucceeded((DaemonTaskSuccessResult) result, 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);
+ }
+ }
+
+ @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) {
+
+ // Report the newly retrieved list of torrents to the torrents fragment
+ fragmentTorrents.updateIsLoading(false);
+ fragmentTorrents.updateTorrents(new ArrayList(torrents));
+
+ // 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
+ List navigationLabels = Label.convertToNavigationLabels(labels,
+ getResources().getString(R.string.labels_unlabeled));
+ if (navigationListAdapter != null) {
+ // Labels are shown in the dedicated side navigation
+ navigationListAdapter.updateLabels(navigationLabels);
+ } else {
+ // Labels are shown in the action bar spinner
+ navigationSpinnerAdapter.updateLabels(navigationLabels);
+ }
+
+ // 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..aa729211
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/TorrentsFragment.java
@@ -0,0 +1,335 @@
+package org.transdroid.core.gui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+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.NavigationFilter;
+import org.transdroid.core.gui.navigation.SelectionManagerMode;
+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 {
+
+ // Local data
+ @Bean
+ protected ApplicationSettings applicationSettings;
+ @InstanceState
+ protected ArrayList torrents = null;
+ @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);
+
+ }
+
+ /**
+ * 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) {
+ torrents = newTorrents;
+ applyNavigationFilter(null); // Resets the filter and shown list of torrents
+ }
+
+ /**
+ * Clears the currently visible list of torrents.
+ * @param b
+ */
+ public void clear(boolean clearError) {
+ this.torrents = null;
+ if (clearError)
+ this.connectionErrorMessage = null;
+ 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
+ List 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) {
+ // TODO: Open label selection dialogue
+ 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);
+ }
+
+ /**
+ * 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); // 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); // 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); // 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..0eea9194
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/DetailsAdapter.java
@@ -0,0 +1,174 @@
+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.core.gui.navigation.FilterSeparatorView_;
+import org.transdroid.daemon.Torrent;
+import org.transdroid.daemon.TorrentFile;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+import com.commonsware.cwac.merge.MergeAdapter;
+
+/**
+ * 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 TorrentDetailsView torrentDetailsView = null;
+ private FilterSeparatorView trackersSeparatorView = null;
+ private SimpleListItemAdapter trackersAdapter = null;
+ private FilterSeparatorView errorsSeparatorView = null;
+ private SimpleListItemAdapter errorsAdapter = null;
+ private FilterSeparatorView torrentFilesSeparatorView = 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);
+ torrentDetailsView.setVisibility(View.GONE);
+ addView(torrentDetailsView, false);
+
+ // Trackers
+ trackersSeparatorView = FilterSeparatorView_.build(context).setText(context.getString(R.string.status_trackers));
+ trackersSeparatorView.setVisibility(View.GONE);
+ addView(trackersSeparatorView, false);
+ this.trackersAdapter = new SimpleListItemAdapter(context, new ArrayList());
+ addAdapter(trackersAdapter);
+
+ // Tracker errors
+ errorsSeparatorView = FilterSeparatorView_.build(context).setText(context.getString(R.string.status_errors));
+ errorsSeparatorView.setVisibility(View.GONE);
+ addView(errorsSeparatorView, false);
+ this.errorsAdapter = new SimpleListItemAdapter(context, new ArrayList());
+ addAdapter(errorsAdapter);
+
+ // Torrent files
+ torrentFilesSeparatorView = FilterSeparatorView_.build(context).setText(context.getString(R.string.status_files));
+ torrentFilesSeparatorView.setVisibility(View.GONE);
+ addView(torrentFilesSeparatorView, false);
+ 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);
+ torrentDetailsView.setVisibility(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());
+ torrentFilesSeparatorView.setVisibility(View.GONE);
+ } else {
+ torrentFilesAdapter.update(torrentFiles);
+ torrentFilesSeparatorView.setVisibility(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());
+ trackersSeparatorView.setVisibility(View.GONE);
+ } else {
+ trackersAdapter.update(trackers);
+ trackersSeparatorView.setVisibility(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());
+ errorsSeparatorView.setVisibility(View.GONE);
+ } else {
+ errorsAdapter.update(errors);
+ errorsSeparatorView.setVisibility(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/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..e6b28e54
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/SimpleListItemAdapter.java
@@ -0,0 +1,93 @@
+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;
+
+ 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();
+ }
+
+ @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));
+ 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 errorStrings A list of string
+ * @return A list of SimpleStringItem objects representing the input strings
+ */
+ public static List wrapStringsList(List errorStrings) {
+ ArrayList errors = new ArrayList();
+ if (errorStrings != null) {
+ for (String errorString : errorStrings) {
+ errors.add(new SimpleStringItem(errorString));
+ }
+ }
+ 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..2caaff61
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/lists/SimpleListItemView.java
@@ -0,0 +1,28 @@
+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) {
+ itemText.setText(filterItem.getName());
+ }
+
+}
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..8323f31d
--- /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,
+ 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/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..2721ce24
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/log/Log.java
@@ -0,0 +1,71 @@
+package org.transdroid.core.gui.log;
+
+import java.sql.SQLException;
+import java.util.Date;
+
+import org.androidannotations.annotations.EBean;
+import org.androidannotations.annotations.EBean.Scope;
+import org.androidannotations.annotations.OrmLiteDao;
+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;
+
+ protected Log(Context context) {
+ this.context = context;
+ }
+
+ protected void log(String logName, int priority, String message) {
+ 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..4f50e910
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/FilterListAdapter.java
@@ -0,0 +1,94 @@
+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.navigation.StatusType.StatusTypeFilter;
+
+import android.content.Context;
+import android.view.View;
+
+import com.commonsware.cwac.merge.MergeAdapter;
+
+/**
+ * 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 FilterSeparatorView statusTypeSeparator;
+ protected FilterSeparatorView labelSeperator;
+ protected FilterSeparatorView 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 = FilterSeparatorView_.build(context).setText(context.getString(R.string.navigation_servers));
+ serverSeparator.setVisibility(servers.isEmpty()? View.GONE: View.VISIBLE);
+ addView(serverSeparator, false);
+ this.serverItems = new FilterListItemAdapter(context, servers);
+ addAdapter(serverItems);
+ } else if (this.serverItems != null && servers != null) {
+ serverSeparator.setVisibility(servers.isEmpty()? View.GONE: View.VISIBLE);
+ this.serverItems.update(servers);
+ } else {
+ serverSeparator.setVisibility(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 = FilterSeparatorView_.build(context).setText(
+ context.getString(R.string.navigation_status));
+ statusTypeSeparator.setVisibility(statusTypes.isEmpty()? View.GONE: View.VISIBLE);
+ addView(statusTypeSeparator, false);
+ this.statusTypeItems = new FilterListItemAdapter(context, statusTypes);
+ addAdapter(statusTypeItems);
+ } else if (this.statusTypeItems != null && statusTypes != null) {
+ statusTypeSeparator.setVisibility(statusTypes.isEmpty()? View.GONE: View.VISIBLE);
+ this.statusTypeItems.update(statusTypes);
+ } else {
+ statusTypeSeparator.setVisibility(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 = FilterSeparatorView_.build(context).setText(context.getString(R.string.navigation_labels));
+ labelSeperator.setVisibility(labels.isEmpty()? View.GONE: View.VISIBLE);
+ addView(labelSeperator, false);
+ this.labelItems = new FilterListItemAdapter(context, labels);
+ addAdapter(labelItems);
+ } else if (this.labelItems != null && labels != null) {
+ labelSeperator.setVisibility(labels.isEmpty()? View.GONE: View.VISIBLE);
+ this.labelItems.update(labels);
+ } else {
+ labelSeperator.setVisibility(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..131c6d53
--- /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.setVisibility(View.INVISIBLE);
+ 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..eab089f2
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/Label.java
@@ -0,0 +1,93 @@
+package org.transdroid.core.gui.navigation;
+
+import java.util.ArrayList;
+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 {
+
+ 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);
+ }
+
+ /**
+ * 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 List convertToNavigationLabels(List daemonLabels,
+ String unnamedLabel) {
+ if (daemonLabels == null)
+ return null;
+ unnamedLabelText = unnamedLabel;
+ List localLabels = new ArrayList();
+ for (org.transdroid.daemon.Label label : daemonLabels) {
+ localLabels.add(new Label(label));
+ }
+ 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..9c813e61
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/NavigationHelper.java
@@ -0,0 +1,96 @@
+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.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.TypefaceSpan;
+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;
+
+ /**
+ * 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();
+
+ /**
+ * 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 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) {
+ 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..5cdd1e66
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/navigation/SelectionManagerMode.java
@@ -0,0 +1,110 @@
+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))
+ 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/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/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/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..06631546
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/NotificationSettingsActivity.java
@@ -0,0 +1,40 @@
+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.ApplicationSettings;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+
+import com.actionbarsherlock.app.SherlockPreferenceActivity;
+
+@EActivity
+public class NotificationSettingsActivity extends SherlockPreferenceActivity {
+
+ @Bean
+ protected ApplicationSettings applicationSettings;
+
+ @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);
+
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ @OptionsItem(android.R.id.home)
+ protected void navigateUp() {
+ MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start();
+ }
+
+}
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..5d1d7a56
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/RssfeedSettingsActivity.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 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");
+
+ }
+
+ @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..8e6da89f
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/SystemSettingsActivity.java
@@ -0,0 +1,190 @@
+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 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.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
+ 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 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..261cd531
--- /dev/null
+++ b/core/src/org/transdroid/core/gui/settings/WebsearchSettingsActivity.java
@@ -0,0 +1,48 @@
+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");
+
+ }
+
+ @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/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/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..616f300c
--- /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-16
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/build.xml b/external/Crouton/library/build.xml
new file mode 100644
index 00000000..2f6f323a
--- /dev/null
+++ b/external/Crouton/library/build.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/project.properties b/external/Crouton/library/project.properties
new file mode 100644
index 00000000..0b79b87b
--- /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-16
+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/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/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..95092b1e
--- /dev/null
+++ b/full/AndroidManifest.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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..deae3e8d
--- /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-16
+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/src/org/transdroid/daemon/BitComet/BitCometAdapter.java b/lib/src/org/transdroid/daemon/BitComet/BitCometAdapter.java
index 732a4e8a..525c9c0c 100644
--- a/lib/src/org/transdroid/daemon/BitComet/BitCometAdapter.java
+++ b/lib/src/org/transdroid/daemon/BitComet/BitCometAdapter.java
@@ -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..cfcc2fe0 100644
--- a/lib/src/org/transdroid/daemon/Bitflu/BitfluAdapter.java
+++ b/lib/src/org/transdroid/daemon/Bitflu/BitfluAdapter.java
@@ -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..06db1af9 100644
--- a/lib/src/org/transdroid/daemon/BuffaloNas/BuffaloNasAdapter.java
+++ b/lib/src/org/transdroid/daemon/BuffaloNas/BuffaloNasAdapter.java
@@ -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..b9eabbaf 100644
--- a/lib/src/org/transdroid/daemon/DLinkRouterBT/DLinkRouterBTAdapter.java
+++ b/lib/src/org/transdroid/daemon/DLinkRouterBT/DLinkRouterBTAdapter.java
@@ -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..edd9301b 100644
--- a/lib/src/org/transdroid/daemon/Daemon.java
+++ b/lib/src/org/transdroid/daemon/Daemon.java
@@ -111,6 +111,45 @@ 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 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 +160,9 @@ public enum Daemon {
if (daemonCode == null) {
return null;
}
+ if (daemonCode.equals("daemon_bitcomet")) {
+ return BitComet;
+ }
if (daemonCode.equals("daemon_bitflu")) {
return Bitflu;
}
@@ -160,9 +202,6 @@ public enum Daemon {
if (daemonCode.equals("daemon_vuze")) {
return Vuze;
}
- if (daemonCode.equals("daemon_bitcomet")) {
- return BitComet;
- }
return null;
}
diff --git a/lib/src/org/transdroid/daemon/Deluge/DelugeAdapter.java b/lib/src/org/transdroid/daemon/Deluge/DelugeAdapter.java
index 8a7fa785..6ce49d45 100644
--- a/lib/src/org/transdroid/daemon/Deluge/DelugeAdapter.java
+++ b/lib/src/org/transdroid/daemon/Deluge/DelugeAdapter.java
@@ -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/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..40bfceb1 100644
--- a/lib/src/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java
+++ b/lib/src/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java
@@ -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/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/Torrent.java b/lib/src/org/transdroid/daemon/Torrent.java
index 63622186..4a3c0f3e 100644
--- a/lib/src/org/transdroid/daemon/Torrent.java
+++ b/lib/src/org/transdroid/daemon/Torrent.java
@@ -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;
}
/**
@@ -295,6 +299,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/TorrentsComparator.java b/lib/src/org/transdroid/daemon/TorrentsComparator.java
index 8cb93f9f..43a30884 100644
--- a/lib/src/org/transdroid/daemon/TorrentsComparator.java
+++ b/lib/src/org/transdroid/daemon/TorrentsComparator.java
@@ -32,24 +32,22 @@ public class TorrentsComparator implements Comparator {
boolean reversed;
/**
- * 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;
}
}
diff --git a/lib/src/org/transdroid/daemon/Transmission/TransmissionAdapter.java b/lib/src/org/transdroid/daemon/Transmission/TransmissionAdapter.java
index aa4bb4e1..0aeb6d1b 100644
--- a/lib/src/org/transdroid/daemon/Transmission/TransmissionAdapter.java
+++ b/lib/src/org/transdroid/daemon/Transmission/TransmissionAdapter.java
@@ -500,7 +500,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..982fa079 100644
--- a/lib/src/org/transdroid/daemon/Utorrent/UtorrentAdapter.java
+++ b/lib/src/org/transdroid/daemon/Utorrent/UtorrentAdapter.java
@@ -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/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/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..8b92510f
--- /dev/null
+++ b/lite/AndroidManifest.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..deae3e8d
--- /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-16
+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