diff --git a/core/AndroidManifest.xml b/core/AndroidManifest.xml index 6f69f60e..34f5d8e2 100644 --- a/core/AndroidManifest.xml +++ b/core/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="3" + android:versionName="2.0-alpha3" > - \ No newline at end of file + diff --git a/core/res/values/strings.xml b/core/res/values/strings.xml index bc7a0c4d..4231864f 100644 --- a/core/res/values/strings.xml +++ b/core/res/values/strings.xml @@ -100,15 +100,28 @@ KB/S Reset Update - PICK A LABEL - NEW LABEL - Remove label - E.g. movies or linux + + New torrent added + %1$s new torrents added + + + Torrent is finished + %1$s torrents are finished + + + %1$s added, %2$s finished torrent + %1$s added, %2$s finished torrents + + %1$s and others All labels Unlabeled New label Setting a label is not supported by your client + PICK A LABEL + NEW LABEL + Remove label + E.g. movies or linux %1$s added (refreshing) %1$s removed @@ -153,6 +166,11 @@ %1$d item selected %1$d items selected + + New RSS feed torrent available + %1$s new RSS feed torrents + + New torrents for %1$s Servers Add new server @@ -320,6 +338,10 @@ Please enter a positive number Please enter a valid label or pick from the list + New Transdroid version available + New Transdroid search module available + You can now update to %1$s + Transdroid \u00A9 Eric Kok, 2312 development Published under GNU General Public License v3 diff --git a/core/src/org/transdroid/core/app/settings/ApplicationSettings.java b/core/src/org/transdroid/core/app/settings/ApplicationSettings.java index 7f2c524e..cbaee05e 100644 --- a/core/src/org/transdroid/core/app/settings/ApplicationSettings.java +++ b/core/src/org/transdroid/core/app/settings/ApplicationSettings.java @@ -9,6 +9,8 @@ import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.EBean; import org.androidannotations.annotations.EBean.Scope; import org.androidannotations.annotations.RootContext; +import org.json.JSONArray; +import org.json.JSONException; import org.transdroid.core.app.search.SearchHelper; import org.transdroid.core.app.search.SearchSite; import org.transdroid.core.gui.search.SearchSetting; @@ -460,4 +462,30 @@ public class ApplicationSettings { prefs.edit().putString("header_setsearchsite", site.getKey()).commit(); } + /** + * Returns the statistics of this server as it was last seen by the background server checker service. + * @param server The server for which to retrieved the statistics from the stored preferences + * @return A JSON array of JSON objects, each which represent a since torrent + */ + public JSONArray getServerLastStats(ServerSetting server) { + String lastStats = prefs.getString(server.getUniqueIdentifier(), null); + if (lastStats == null) + return null; + try { + return new JSONArray(lastStats); + } catch (JSONException e) { + return null; + } + } + + /** + * Stores the now-last seen statistics of the supplied server by the background server checker service to the + * internal stored preferences. + * @param server The server to which the statistics apply to + * @param lastStats A JSON array of JSON objects that each represent a single seen torrent + */ + public void setServerLastStats(ServerSetting server, JSONArray lastStats) { + prefs.edit().putString(server.getUniqueIdentifier(), lastStats.toString()).commit(); + } + } diff --git a/core/src/org/transdroid/core/app/settings/NotificationSettings.java b/core/src/org/transdroid/core/app/settings/NotificationSettings.java index 58bd4003..44368c62 100644 --- a/core/src/org/transdroid/core/app/settings/NotificationSettings.java +++ b/core/src/org/transdroid/core/app/settings/NotificationSettings.java @@ -71,6 +71,15 @@ public class NotificationSettings { return prefs.getBoolean("notifications_vibrate", false); } + /** + * Returns the default vibrate pattern to use if the user enabled notification vibrations; check + * {@link #shouldVibrate()}, + * @return A unique pattern for vibrations in Transdroid + */ + public long[] getDefaultVibratePattern() { + return new long[]{100, 100, 200, 300, 400, 700}; // Unique pattern? + } + private int getRawLedColour() { return prefs.getInt("notifications_ledcolour", -1); } diff --git a/core/src/org/transdroid/core/app/settings/ServerSetting.java b/core/src/org/transdroid/core/app/settings/ServerSetting.java index 2b8690f0..fdd29b81 100644 --- a/core/src/org/transdroid/core/app/settings/ServerSetting.java +++ b/core/src/org/transdroid/core/app/settings/ServerSetting.java @@ -189,6 +189,10 @@ public class ServerSetting implements SimpleListItem { return this.key; } + /** + * Returns a string that the user can use to identify the server by internal settings (rather than the name). + * @return A human-readable identifier in the form [https://]username@address:port/folder + */ public String getHumanReadableIdentifier() { if (isAutoGenerated) { // Hide the 'implementation details'; just give the username and server @@ -201,6 +205,19 @@ public class ServerSetting implements SimpleListItem { + (Daemon.supportsCustomFolder(getType()) && getFolder() != null ? getFolder() : ""); } + /** + * Returns a string that acts as a unique identifier for this server, non-depending on the internal storage + * order/index. THis may be used to store additional details about this server elsewhere. It may change if the user + * changes server settings, but not with name or notification settings. + * @return A unique identifying string, based primarily on the configured address, port number, SSL settings and + * user name; returns null if the server is not yet fully identifiable (during configuration, for example) + */ + public String getUniqueIdentifier() { + if (getType() == null || getAddress() == null || getAddress().equals("")) + return null; + return getType().toString() + "|" + getHumanReadableIdentifier(); + } + @Override public boolean equals(Object o) { if (o instanceof ServerSetting) { diff --git a/core/src/org/transdroid/core/gui/TorrentsActivity.java b/core/src/org/transdroid/core/gui/TorrentsActivity.java index 755526db..b87eedca 100644 --- a/core/src/org/transdroid/core/gui/TorrentsActivity.java +++ b/core/src/org/transdroid/core/gui/TorrentsActivity.java @@ -38,6 +38,7 @@ import org.transdroid.core.gui.search.BarcodeHelper; import org.transdroid.core.gui.search.FilePickerHelper; import org.transdroid.core.gui.search.UrlEntryDialog; import org.transdroid.core.gui.settings.*; +import org.transdroid.core.service.BootReceiver; import org.transdroid.daemon.Daemon; import org.transdroid.daemon.IDaemonAdapter; import org.transdroid.daemon.Priority; @@ -185,12 +186,17 @@ public class TorrentsActivity extends SherlockFragmentActivity implements OnNavi // Log messages from the server daemons using our singleton logger DLog.setLogger(Log_.getInstance_(this)); - // Connect to the last used server + // Connect to the last used server or a server that was supplied in the starting intent ServerSetting lastUsed = applicationSettings.getLastUsedServer(); if (lastUsed == null) { // No server settings yet; return; } + if (getIntent().getExtras() == null && getIntent().hasExtra("org.transdroid.START_SERVER")) { + lastUsed = applicationSettings.getServerSetting(getIntent().getExtras().getInt( + "org.transdroid.START_SERVER")); + } + // Set this as selection in the action bar spinner; we can use the server setting key since we have stable ids getSupportActionBar().setSelectedNavigationItem(lastUsed.getOrder() + 1); skipNextOnNavigationItemSelectedCall = true; @@ -200,6 +206,10 @@ public class TorrentsActivity extends SherlockFragmentActivity implements OnNavi currentConnection = lastUsed.createServerAdapter(); handleStartIntent(); } + + // Start the alarms for the background services, if needed + BootReceiver.startBackgroundServices(getApplicationContext(), false); + BootReceiver.startAppUpdatesService(getApplicationContext()); } diff --git a/core/src/org/transdroid/core/gui/log/Log.java b/core/src/org/transdroid/core/gui/log/Log.java index bd9c7430..51c0c79f 100644 --- a/core/src/org/transdroid/core/gui/log/Log.java +++ b/core/src/org/transdroid/core/gui/log/Log.java @@ -37,7 +37,7 @@ public class Log implements ITLogger { } protected void log(String logName, int priority, String message) { - if (!navigationHelper.inDebugMode()) + if (navigationHelper.inDebugMode()) android.util.Log.println(priority, LOG_NAME, message); try { // Store this log message to the database diff --git a/core/src/org/transdroid/core/gui/settings/NotificationSettingsActivity.java b/core/src/org/transdroid/core/gui/settings/NotificationSettingsActivity.java index 06631546..73fc4775 100644 --- a/core/src/org/transdroid/core/gui/settings/NotificationSettingsActivity.java +++ b/core/src/org/transdroid/core/gui/settings/NotificationSettingsActivity.java @@ -4,31 +4,50 @@ 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 org.transdroid.core.app.settings.NotificationSettings; +import org.transdroid.core.service.BootReceiver; import android.annotation.TargetApi; import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Build; import android.os.Bundle; import com.actionbarsherlock.app.SherlockPreferenceActivity; @EActivity -public class NotificationSettingsActivity extends SherlockPreferenceActivity { +public class NotificationSettingsActivity extends SherlockPreferenceActivity implements + OnSharedPreferenceChangeListener { @Bean - protected ApplicationSettings applicationSettings; - + protected NotificationSettings notificationSettings; + @SuppressWarnings("deprecation") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - + // Just load the notification-related preferences from XML addPreferencesFromResource(R.xml.pref_notifications); - + + } + + @SuppressWarnings("deprecation") + @Override + protected void onResume() { + super.onResume(); + // Start/stop the background service appropriately + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @SuppressWarnings("deprecation") + @Override + protected void onPause() { + super.onPause(); + getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @@ -37,4 +56,16 @@ public class NotificationSettingsActivity extends SherlockPreferenceActivity { MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); } + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + + if (!notificationSettings.isEnabled()) { + // Disabled background notifications; disable the alarms that start the service + BootReceiver.cancelBackgroundServices(getApplicationContext()); + } + + // (Re-)enable the alarms for the background services + BootReceiver.startBackgroundServices(getApplicationContext(), true); + } + } diff --git a/core/src/org/transdroid/core/gui/settings/SystemSettingsActivity.java b/core/src/org/transdroid/core/gui/settings/SystemSettingsActivity.java index 8e6da89f..8b9f6352 100644 --- a/core/src/org/transdroid/core/gui/settings/SystemSettingsActivity.java +++ b/core/src/org/transdroid/core/gui/settings/SystemSettingsActivity.java @@ -13,6 +13,7 @@ import org.transdroid.core.app.settings.SettingsPersistence; import org.transdroid.core.gui.log.ErrorLogSender; import org.transdroid.core.gui.navigation.DialogHelper; import org.transdroid.core.gui.navigation.NavigationHelper; +import org.transdroid.core.service.BootReceiver; import android.annotation.TargetApi; import android.app.AlertDialog; @@ -24,6 +25,7 @@ import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceManager; @@ -58,7 +60,8 @@ public class SystemSettingsActivity extends SherlockPreferenceActivity { // Just load the system-related preferences from XML addPreferencesFromResource(R.xml.pref_system); - // Handle outgoing links + // Handle outgoing links and preference changes + findPreference("system_checkupdates").setOnPreferenceClickListener(onCheckUpdatesClick); findPreference("system_sendlog").setOnPreferenceClickListener(onSendLogClick); findPreference("system_installhelp").setOnPreferenceClickListener(onInstallHelpClick); findPreference("system_changelog").setOnPreferenceClickListener(onChangeLogClick); @@ -73,6 +76,17 @@ public class SystemSettingsActivity extends SherlockPreferenceActivity { MainSettingsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); } + private OnPreferenceClickListener onCheckUpdatesClick = new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + if (((CheckBoxPreference) preference).isChecked()) + BootReceiver.startAppUpdatesService(getApplicationContext()); + else + BootReceiver.cancelAppUpdates(getApplicationContext()); + return true; + } + }; + private OnPreferenceClickListener onSendLogClick = new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { diff --git a/core/src/org/transdroid/core/service/AlarmReceiver.java b/core/src/org/transdroid/core/service/AlarmReceiver.java new file mode 100644 index 00000000..384e9e58 --- /dev/null +++ b/core/src/org/transdroid/core/service/AlarmReceiver.java @@ -0,0 +1,34 @@ +package org.transdroid.core.service; + +import org.androidannotations.annotations.EReceiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * Acts simply as an intermediary to start the appropriate background service when an alarm goes off. + * @author Eric Kok + */ +@EReceiver +public class AlarmReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getIntExtra("service", -1)) { + case BootReceiver.ALARM_SERVERCHECKER: + context.startService(new Intent(context, ServerCheckerService_.class)); + break; + case BootReceiver.ALARM_RSSCHECKER: + context.startService(new Intent(context, RssCheckerService_.class)); + break; + case BootReceiver.ALARM_APPUPDATES: + context.startService(new Intent(context, AppUpdateService_.class)); + break; + default: + // No valid service start ID + break; + } + } + +} diff --git a/core/src/org/transdroid/core/service/AppUpdateService.java b/core/src/org/transdroid/core/service/AppUpdateService.java new file mode 100644 index 00000000..f99fdfd4 --- /dev/null +++ b/core/src/org/transdroid/core/service/AppUpdateService.java @@ -0,0 +1,136 @@ +package org.transdroid.core.service; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Random; + +import org.androidannotations.annotations.Bean; +import org.androidannotations.annotations.EService; +import org.androidannotations.annotations.SystemService; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.AbstractHttpClient; +import org.apache.http.impl.client.DefaultHttpClient; +import org.transdroid.core.R; +import org.transdroid.core.app.settings.NotificationSettings; +import org.transdroid.core.app.settings.SystemSettings; +import org.transdroid.core.gui.log.Log; +import org.transdroid.daemon.util.HttpHelper; + +import android.app.IntentService; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationCompat.Builder; + +@EService +public class AppUpdateService extends IntentService { + + private static final String LATEST_URL_APP = "http://www.transdroid.org/update/latest-app.php"; + private static final String LATEST_URL_SEARCH = "http://www.transdroid.org/update/latest-search.php"; + private static final String DOWNLOAD_URL_APP = "http://www.transdroid.org/latest"; + private static final String DOWNLOAD_URL_SEARCH = "http://www.transdroid.org/latest-search"; + + @Bean + protected ConnectivityHelper connectivityHelper; + @Bean + protected SystemSettings systemSettings; + @Bean + protected NotificationSettings notificationSettings; + @SystemService + protected NotificationManager notificationManager; + + public AppUpdateService() { + super("AppUpdateService"); + } + + @Override + protected void onHandleIntent(Intent intent) { + + if (!connectivityHelper.shouldPerformActions() || !systemSettings.checkForUpdates()) { + Log.d(this, + "Skip the app update service, as background data is disabled, the service is disabled or we are not connected."); + return; + } + + DefaultHttpClient httpclient = new DefaultHttpClient(); + Random random = new Random(); + + try { + + // Retrieve what is the latest released app and search module versions + String[] app = retrieveLatestVersion(httpclient, LATEST_URL_APP); + String[] search = retrieveLatestVersion(httpclient, LATEST_URL_SEARCH); + int appVersion = Integer.parseInt(app[0].trim()); + int searchVersion = Integer.parseInt(search[0].trim()); + + // New version of the app? + try { + PackageInfo appPackage = getPackageManager().getPackageInfo(getPackageName(), 0); + if (appPackage.versionCode < appVersion) { + // New version available! Notify the user. + newNotification(getString(R.string.update_app_newversion), + getString(R.string.update_app_newversion), + getString(R.string.update_updateto, app[1].trim()), + DOWNLOAD_URL_APP + "?" + Integer.toString(random.nextInt()), 90000); + } + } catch (NameNotFoundException e) { + // Not installed... this can never happen since this Service is part of the app itself + } + + // New version of the search module? + try { + PackageInfo searchPackage = getPackageManager().getPackageInfo("org.transdroid.search", 0); + if (searchPackage.versionCode < searchVersion) { + // New version available! Notify the user. + newNotification(getString(R.string.update_search_newversion), + getString(R.string.update_search_newversion), + getString(R.string.update_updateto, search[1].trim()), + DOWNLOAD_URL_SEARCH + "?" + Integer.toString(random.nextInt()), 90001); + } + } catch (NameNotFoundException e) { + // The search module isn't installed yet at all; ignore and wait for the user to manually + // install it (when the first search is initiated) + } + + } catch (Exception e) { + // Cannot check right now for some reason; log and ignore + Log.d(this, "Cannot retrieve latest app or search module version code from the site: " + e.toString()); + } + + } + + /** + * Retrieves the latest version number of the app or search module by checking an online text file that looks like + * '160|1.1.15' for version code 160 and version name 1.1.15. + * @param httpclient An already instantiated HTTP client + * @param url The URL of the the text file that contains the current latest version code and name + * @return A string array with two elements: the version code and the version number + * @throws ClientProtocolException Thrown when the provided URL is invalid + * @throws IOException Thrown when the last version information could not be retrieved + */ + private String[] retrieveLatestVersion(AbstractHttpClient httpclient, String url) throws ClientProtocolException, + IOException { + HttpResponse request = httpclient.execute(new HttpGet(url)); + InputStream stream = request.getEntity().getContent(); + String appVersion[] = HttpHelper.convertStreamToString(stream).split("\\|"); + stream.close(); + return appVersion; + } + + private void newNotification(String ticker, String title, String text, String downloadUrl, int notifyID) { + PendingIntent pi = PendingIntent.getActivity(this, notifyID, + new Intent(Intent.ACTION_VIEW, Uri.parse(downloadUrl)), Intent.FLAG_ACTIVITY_NEW_TASK); + Builder builder = new NotificationCompat.Builder(this).setSmallIcon(R.drawable.ic_stat_notification) + .setTicker(ticker).setContentTitle(title).setContentText(text) + .setLights(notificationSettings.getDesiredLedColour(), 600, 1000) + .setSound(notificationSettings.getSound()).setAutoCancel(true).setContentIntent(pi); + notificationManager.notify(notifyID, builder.build()); + } + +} diff --git a/core/src/org/transdroid/core/service/BootReceiver.java b/core/src/org/transdroid/core/service/BootReceiver.java new file mode 100644 index 00000000..ceab951f --- /dev/null +++ b/core/src/org/transdroid/core/service/BootReceiver.java @@ -0,0 +1,88 @@ +package org.transdroid.core.service; + +import org.transdroid.core.app.settings.NotificationSettings; +import org.transdroid.core.app.settings.NotificationSettings_; +import org.transdroid.core.app.settings.SystemSettings; +import org.transdroid.core.app.settings.SystemSettings_; +import org.transdroid.core.gui.log.Log; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.SystemClock; + +/** + * Receives the intent that the device has been started in order to set up proper alarms for all background services. + * @author Eric Kok + */ +public class BootReceiver extends BroadcastReceiver { + + public static final int ALARM_SERVERCHECKER = 0; + public static final int ALARM_RSSCHECKER = 1; + public static final int ALARM_APPUPDATES = 2; + + public static PendingIntent piServerChecker = null, piRssChecker = null, piAppUpdates = null; + + @Override + public void onReceive(Context context, Intent intent) { + startBackgroundServices(context, false); + startAppUpdatesService(context); + } + + public static void startBackgroundServices(Context context, boolean forceReload) { + NotificationSettings notificationSettings = NotificationSettings_.getInstance_(context); + AlarmManager alarms = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + if (notificationSettings.isEnabled() && (forceReload || (piServerChecker == null && piRssChecker == null))) { + + Log.d(context, "Boot signal received, starting server and rss checker background services"); + // Schedule repeating alarms, with the first being (somewhat) in 1 second from now + piServerChecker = PendingIntent.getBroadcast(context, ALARM_SERVERCHECKER, new Intent(context, + AlarmReceiver_.class).putExtra("service", ALARM_SERVERCHECKER), 0); + piRssChecker = PendingIntent.getBroadcast(context, ALARM_RSSCHECKER, new Intent(context, + AlarmReceiver_.class).putExtra("service", ALARM_RSSCHECKER), 0); + alarms.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, + notificationSettings.getInvervalInMilliseconds(), piServerChecker); + alarms.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, + notificationSettings.getInvervalInMilliseconds(), piRssChecker); + + } + } + + public static void startAppUpdatesService(Context context) { + SystemSettings systemSettings = SystemSettings_.getInstance_(context); + AlarmManager alarms = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + if (systemSettings.checkForUpdates() && piAppUpdates == null) { + + Log.d(context, "Boot signal received, starting app update checker service"); + // Schedule a daily, with the first being (somewhat) in 1 second from now + piAppUpdates = PendingIntent.getBroadcast(context, ALARM_APPUPDATES, new Intent(context, + AlarmReceiver_.class).putExtra("service", ALARM_APPUPDATES), 0); + alarms.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, + AlarmManager.INTERVAL_DAY, piAppUpdates); + + } + } + + public static void cancelBackgroundServices(Context context) { + AlarmManager alarms = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + if (piServerChecker != null) { + alarms.cancel(piServerChecker); + piServerChecker = null; + } + if (piRssChecker != null) { + alarms.cancel(piRssChecker); + piRssChecker = null; + } + } + + public static void cancelAppUpdates(Context context) { + if (piAppUpdates != null) { + AlarmManager alarms = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + alarms.cancel(piAppUpdates); + piAppUpdates = null; + } + } + +} diff --git a/core/src/org/transdroid/core/service/ConnectivityHelper.java b/core/src/org/transdroid/core/service/ConnectivityHelper.java new file mode 100644 index 00000000..fe043320 --- /dev/null +++ b/core/src/org/transdroid/core/service/ConnectivityHelper.java @@ -0,0 +1,29 @@ +package org.transdroid.core.service; + +import org.androidannotations.annotations.EBean; +import org.androidannotations.annotations.SystemService; +import org.androidannotations.annotations.EBean.Scope; + +import android.content.Context; +import android.net.ConnectivityManager; + +@EBean(scope = Scope.Singleton) +public class ConnectivityHelper { + + @SystemService + protected ConnectivityManager connectivityManager; + + public ConnectivityHelper(Context context) { + } + + @SuppressWarnings("deprecation") + public boolean shouldPerformActions() { + // First check the old background data setting (this will always be true for ICS+) + if (!connectivityManager.getBackgroundDataSetting()) + return false; + + // Still good? Check the current active network instead + return connectivityManager.getActiveNetworkInfo().isConnected(); + } + +} diff --git a/core/src/org/transdroid/core/service/RssCheckerService.java b/core/src/org/transdroid/core/service/RssCheckerService.java new file mode 100644 index 00000000..307b8807 --- /dev/null +++ b/core/src/org/transdroid/core/service/RssCheckerService.java @@ -0,0 +1,107 @@ +package org.transdroid.core.service; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.androidannotations.annotations.Bean; +import org.androidannotations.annotations.EService; +import org.androidannotations.annotations.SystemService; +import org.transdroid.core.R; +import org.transdroid.core.app.settings.ApplicationSettings; +import org.transdroid.core.app.settings.NotificationSettings; +import org.transdroid.core.app.settings.RssfeedSetting; +import org.transdroid.core.gui.log.Log; +import org.transdroid.core.gui.rss.RssfeedsActivity_; +import org.transdroid.core.rssparser.Item; +import org.transdroid.core.rssparser.RssParser; +import org.transdroid.daemon.util.Collections2; + +import android.app.IntentService; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Intent; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationCompat.Builder; + +/** + * A background service that checks all user-configured RSS feeds for new items. + * @author Eric Kok + */ +@EService +public class RssCheckerService extends IntentService { + + @Bean + protected ConnectivityHelper connectivityHelper; + @Bean + protected NotificationSettings notificationSettings; + @Bean + protected ApplicationSettings applicationSettings; + @SystemService + protected NotificationManager notificationManager; + + public RssCheckerService() { + super("RssCheckerService"); + } + + @Override + protected void onHandleIntent(Intent intent) { + + if (!connectivityHelper.shouldPerformActions() || !notificationSettings.isEnabled()) { + Log.d(this, + "Skip the RSS checker service, as background data is disabled, the service is disabled or we are not connected."); + return; + } + + // Check every RSS feed for new items + int unread = 0; + Set hasUnread = new LinkedHashSet(); + for (RssfeedSetting feed : applicationSettings.getRssfeedSettings()) { + try { + + Log.d(this, "Try to parse " + feed.getName() + " (" + feed.getUrl() + ")"); + RssParser parser = new RssParser(feed.getUrl()); + parser.parse(); + if (parser.getChannel() == null) + continue; + + // Find the last item that is newer than the last viewed date + for (Item item : parser.getChannel().getItems()) { + if (item.getPubdate() != null && item.getPubdate().before(feed.getLastViewed())) { + break; + } else { + unread++; + if (!hasUnread.contains(feed.getName())) + hasUnread.add(feed.getName()); + } + } + + Log.d(this, feed.getName() + " has " + (hasUnread.contains(feed.getName()) ? "" : "no ") + + "unread items"); + + } catch (Exception e) { + // Ignore RSS feeds that could not be retrieved or parsed + } + } + + if (unread == 0) { + // No new items; just exit + return; + } + + // Provide a notification, since there are new RSS items + PendingIntent pi = PendingIntent.getActivity(this, 80000, new Intent(this, RssfeedsActivity_.class), + Intent.FLAG_ACTIVITY_NEW_TASK); + String title = getResources().getQuantityString(R.plurals.rss_service_new, unread, Integer.toString(unread)); + String forString = Collections2.joinString(hasUnread, ", "); + Builder builder = new NotificationCompat.Builder(this).setSmallIcon(R.drawable.ic_stat_notification) + .setTicker(title).setContentTitle(title) + .setContentText(getString(R.string.rss_service_newfor, forString)).setNumber(unread) + .setLights(notificationSettings.getDesiredLedColour(), 600, 1000) + .setSound(notificationSettings.getSound()).setAutoCancel(true).setContentIntent(pi); + if (notificationSettings.shouldVibrate()) + builder.setVibrate(notificationSettings.getDefaultVibratePattern()); + notificationManager.notify(80001, builder.build()); + + } + +} diff --git a/core/src/org/transdroid/core/service/ServerCheckerService.java b/core/src/org/transdroid/core/service/ServerCheckerService.java new file mode 100644 index 00000000..a04a528a --- /dev/null +++ b/core/src/org/transdroid/core/service/ServerCheckerService.java @@ -0,0 +1,183 @@ +package org.transdroid.core.service; + +import java.util.ArrayList; +import java.util.List; + +import org.androidannotations.annotations.Bean; +import org.androidannotations.annotations.EService; +import org.androidannotations.annotations.SystemService; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.transdroid.core.R; +import org.transdroid.core.app.settings.ApplicationSettings; +import org.transdroid.core.app.settings.NotificationSettings; +import org.transdroid.core.app.settings.ServerSetting; +import org.transdroid.core.gui.TorrentsActivity_; +import org.transdroid.core.gui.log.Log; +import org.transdroid.daemon.IDaemonAdapter; +import org.transdroid.daemon.Torrent; +import org.transdroid.daemon.task.DaemonTaskResult; +import org.transdroid.daemon.task.RetrieveTask; +import org.transdroid.daemon.task.RetrieveTaskSuccessResult; +import org.transdroid.daemon.util.Collections2; + +import android.app.IntentService; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Intent; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationCompat.Builder; +import android.support.v4.app.NotificationCompat.InboxStyle; + +/** + * A background service that checks all user-configured servers (if so desired) for new and finished torrents. + * @author Eric Kok + */ +@EService +public class ServerCheckerService extends IntentService { + + @Bean + protected ConnectivityHelper connectivityHelper; + @Bean + protected NotificationSettings notificationSettings; + @Bean + protected ApplicationSettings applicationSettings; + @SystemService + protected NotificationManager notificationManager; + + public ServerCheckerService() { + super("ServerCheckerService"); + } + + @Override + protected void onHandleIntent(Intent intent) { + + if (!connectivityHelper.shouldPerformActions() || !notificationSettings.isEnabled()) { + Log.d(this, + "Skip the server checker service, as background data is disabled, the service is disabled or we are not connected."); + return; + } + + int notifyBase = 10000; + for (ServerSetting server : applicationSettings.getServerSettings()) { + + // No need to check if the server is not properly configured or none of the two types of notifications are + // enabled by the user for this specific server + if (server.getType() == null || server.getAddress() == null || server.getAddress().equals("") + || !(server.shouldAlarmOnFinishedDownload() || server.shouldAlarmOnNewTorrent())) + return; + + // Get the statistics for the last time we checked this server + JSONArray lastStats = applicationSettings.getServerLastStats(server); + + // Synchronously retrieve torrents listing + IDaemonAdapter adapter = server.createServerAdapter(); + DaemonTaskResult result = RetrieveTask.create(adapter).execute(); + if (!(result instanceof RetrieveTaskSuccessResult)) { + // Cannot retrieve torrents at this time + return; + } + List retrieved = ((RetrieveTaskSuccessResult) result).getTorrents(); + Log.d(this, server.getName() + ": Retrieved torrent listing"); + + // Check for differences between the last and the current stats + JSONArray currentStats = new JSONArray(); + List newTorrents = new ArrayList(); + List doneTorrents = new ArrayList(); + for (Torrent torrent : retrieved) { + + // Remember this torrent for the next time + try { + currentStats.put(new JSONObject().put("id", torrent.getUniqueID()).put("done", + torrent.getPartDone() == 1F)); + } catch (JSONException e) { + // Can't build the JSON object; this should not happen and we can safely ignore it + } + + // See if this torrent was done the last time we checked + if (lastStats != null) { + Boolean wasDone = findLastDoneStat(lastStats, torrent); + if (server.shouldAlarmOnNewTorrent() && wasDone == null) { + // This torrent wasn't present earlier + newTorrents.add(torrent); + continue; + } + if (server.shouldAlarmOnFinishedDownload() && torrent.getPartDone() == 1F && !wasDone) + // This torrent is now done, but wasn't before + doneTorrents.add(torrent); + } + + } + + // Store the now-current statistics on torrents for the next time we check this server + applicationSettings.setServerLastStats(server, currentStats); + + // Notify on new and now-done torrents for this server + Log.d(this, server.getName() + ": " + newTorrents.size() + " new torrents, " + doneTorrents.size() + + " newly finished torrents."); + Intent i = new Intent(this, TorrentsActivity_.class); + i.putExtra("org.transdroid.START_SERVER", server.getOrder()); + // Should start the main activity directly into this server + PendingIntent pi = PendingIntent.getActivity(this, notifyBase + server.getOrder(), i, + Intent.FLAG_ACTIVITY_NEW_TASK); + ArrayList affectedTorrents = new ArrayList(newTorrents.size() + doneTorrents.size()); + affectedTorrents.addAll(newTorrents); + affectedTorrents.addAll(doneTorrents); + String title, forString = Collections2.joinString(affectedTorrents, ", "); + if (newTorrents.size() > 0 && doneTorrents.size() > 0) { + // Note: use the 'one' plural iif 1 new torrent was added and 1 was newly finished + title = getResources().getQuantityString(R.plurals.status_service_finished, + newTorrents.size() + doneTorrents.size() == 2 ? 1 : 2, Integer.toString(newTorrents.size()), + Integer.toString(doneTorrents.size())); + } else if (newTorrents.size() > 0) { + title = getResources().getQuantityString(R.plurals.status_service_added, newTorrents.size(), + Integer.toString(newTorrents.size())); + } else if (doneTorrents.size() > 0) { + title = getResources().getQuantityString(R.plurals.status_service_finished, doneTorrents.size(), + Integer.toString(doneTorrents.size())); + } else { + // No notification to show + continue; + } + + // Build the basic notification + Builder builder = new NotificationCompat.Builder(this).setSmallIcon(R.drawable.ic_stat_notification) + .setTicker(title).setContentTitle(title).setContentText(forString) + .setNumber(affectedTorrents.size()) + .setLights(notificationSettings.getDesiredLedColour(), 600, 1000) + .setSound(notificationSettings.getSound()).setAutoCancel(true).setContentIntent(pi); + if (notificationSettings.shouldVibrate()) + builder.setVibrate(notificationSettings.getDefaultVibratePattern()); + + // Add at most 5 lines with the affected torrents + InboxStyle inbox = new NotificationCompat.InboxStyle(builder); + if (affectedTorrents.size() < 6) { + for (Torrent affectedTorrent : affectedTorrents) { + inbox.addLine(affectedTorrent.getName()); + } + } else { + for (int j = 0; j < 4; j++) { + inbox.addLine(affectedTorrents.get(j).getName()); + } + inbox.addLine(getString(R.string.status_service_andothers, affectedTorrents.get(5).getName())); + } + notificationManager.notify(notifyBase + server.getOrder(), inbox.build()); + + } + + } + + private Boolean findLastDoneStat(JSONArray lastStats, Torrent torrent) { + for (int i = 0; i < lastStats.length(); i++) { + try { + if (lastStats.getJSONObject(i).getString("id").equals(torrent.getUniqueID())) + return lastStats.getJSONObject(i).getBoolean("done"); + } catch (JSONException e) { + return null; + } + } + return null; + } + +} diff --git a/full/AndroidManifest.xml b/full/AndroidManifest.xml index 81243c33..c3a1ecbc 100644 --- a/full/AndroidManifest.xml +++ b/full/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="3" + android:versionName="2.0-alpha3" > + @@ -26,10 +27,10 @@ + android:theme="@style/Theme.Sherlock" > @@ -68,8 +69,8 @@ @@ -79,8 +80,8 @@ @@ -90,8 +91,8 @@ @@ -101,8 +102,8 @@ @@ -112,8 +113,8 @@ @@ -123,8 +124,8 @@ @@ -134,8 +135,8 @@ @@ -146,6 +147,7 @@ + @@ -170,8 +172,8 @@ android:name="org.transdroid.core.gui.search.SearchActivity_" android:icon="@drawable/ic_launcher" android:label="@string/search_torrentsearch" - android:theme="@style/TransdroidTheme" - android:launchMode="singleTask" > + android:launchMode="singleTask" + android:theme="@style/TransdroidTheme" > @@ -192,11 +194,12 @@ android:name="android.app.default_searchable" android:value="org.transdroid.core.gui.search.SearchActivity_" /> - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lite/AndroidManifest.xml b/lite/AndroidManifest.xml index 055e0598..9a290bc3 100644 --- a/lite/AndroidManifest.xml +++ b/lite/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="3" + android:versionName="2.0-alpha3" > + @@ -26,10 +27,10 @@ + android:theme="@style/Theme.Sherlock" > + + + + + + + + + + + + \ No newline at end of file