diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6cce31c1..7742d30b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -147,6 +147,11 @@ android:value=".activities.MainActivity" /> + + diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/SettingsActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/SettingsActivity.java index c4bdf0af..78793d37 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/SettingsActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/SettingsActivity.java @@ -298,6 +298,11 @@ public class SettingsActivity extends SyncthingActivity { ); mCategoryRunConditions = (PreferenceScreen) findPreference("category_run_conditions"); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + // Remove pref as we use JobScheduler implementation which is not available on API < 21. + CheckBoxPreference prefRunOnTimeSchedule = (CheckBoxPreference) findPreference(Constants.PREF_RUN_ON_TIME_SCHEDULE); + mCategoryRunConditions.removePreference(prefRunOnTimeSchedule); + } setPreferenceCategoryChangeListener(mCategoryRunConditions, this::onRunConditionPreferenceChange); /* Behaviour */ diff --git a/app/src/main/java/com/nutomic/syncthingandroid/receiver/BootReceiver.java b/app/src/main/java/com/nutomic/syncthingandroid/receiver/BootReceiver.java index 8c38bc39..f98ce30d 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/receiver/BootReceiver.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/receiver/BootReceiver.java @@ -18,6 +18,10 @@ public class BootReceiver extends BroadcastReceiver { private static final String TAG = "BootReceiver"; + /** + * For testing purposes: + * adb root & adb shell am broadcast -a android.intent.action.BOOT_COMPLETED + */ @Override public void onReceive(Context context, Intent intent) { Boolean bootCompleted = intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED); diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/Constants.java b/app/src/main/java/com/nutomic/syncthingandroid/service/Constants.java index ff975e67..288efeef 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/Constants.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/Constants.java @@ -27,6 +27,7 @@ public class Constants { public static final String PREF_RESPECT_BATTERY_SAVING = "respect_battery_saving"; public static final String PREF_RESPECT_MASTER_SYNC = "respect_master_sync"; public static final String PREF_RUN_IN_FLIGHT_MODE = "run_in_flight_mode"; + public static final String PREF_RUN_ON_TIME_SCHEDULE = "run_on_time_schedule"; // Preferences - Behaviour public static final String PREF_USE_ROOT = "use_root"; @@ -123,6 +124,13 @@ public class Constants { : 5 ); + /** + * If the user enabled hourly one-time shot sync, the following + * parameters are effective. + */ + public static final int WAIT_FOR_NEXT_SYNC_DELAY_SECS = isRunningOnEmulator() ? 10 : 3600; + public static final int TRIGGERED_SYNC_DURATION_SECS = isRunningOnEmulator() ? 20 : 300; + /** * Directory where config is exported to and imported from. */ diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/RunConditionMonitor.java b/app/src/main/java/com/nutomic/syncthingandroid/service/RunConditionMonitor.java index d674c665..78bdd2ea 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/RunConditionMonitor.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/RunConditionMonitor.java @@ -19,11 +19,13 @@ import android.os.Handler; import android.os.Looper; import android.os.PowerManager; import android.support.annotation.Nullable; +import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.SyncthingApp; import com.nutomic.syncthingandroid.service.ReceiverManager; +import com.nutomic.syncthingandroid.util.JobUtils; import java.util.HashSet; import java.util.Set; @@ -41,6 +43,9 @@ public class RunConditionMonitor { private Boolean ENABLE_VERBOSE_LOG = false; + public static final String ACTION_SYNC_TRIGGER_FIRED = + "com.github.catfriend1.syncthingandroid.service.RunConditionMonitor.ACTION_SYNC_TRIGGER_FIRED"; + private static final String POWER_SOURCE_CHARGER_BATTERY = "ac_and_battery_power"; private static final String POWER_SOURCE_CHARGER = "ac_power"; private static final String POWER_SOURCE_BATTERY = "battery_power"; @@ -87,9 +92,17 @@ public class RunConditionMonitor { private final Context mContext; private ReceiverManager mReceiverManager; + private @Nullable SyncTriggerReceiver mSyncTriggerReceiver = null; private Resources res; private String mRunDecisionExplanation = ""; + /** + * Only relevant if the user has enabled turning Syncthing on by + * time schedule for a specific amount of time periodically. + * Holds true if we are within a "SyncthingNative should run" time frame. + */ + private Boolean mTimeConditionMatch = false; + @Inject SharedPreferences mPreferences; @@ -142,16 +155,31 @@ public class RunConditionMonitor { mSyncStatusObserverHandle = ContentResolver.addStatusChangeListener( ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, mSyncStatusObserver); + // SyncTriggerReceiver + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(mContext); + mSyncTriggerReceiver = new SyncTriggerReceiver(); + localBroadcastManager.registerReceiver(mSyncTriggerReceiver, + new IntentFilter(ACTION_SYNC_TRIGGER_FIRED)); + // Initially determine if syncthing should run under current circumstances. updateShouldRunDecision(); + + // Initially schedule the SyncTrigger job. + JobUtils.scheduleSyncTriggerServiceJob(context, Constants.WAIT_FOR_NEXT_SYNC_DELAY_SECS); } public void shutdown() { LogV("Shutting down"); + JobUtils.cancelAllScheduledJobs(mContext); if (mSyncStatusObserverHandle != null) { ContentResolver.removeStatusChangeListener(mSyncStatusObserverHandle); mSyncStatusObserverHandle = null; } + if (mSyncTriggerReceiver != null) { + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(mContext); + localBroadcastManager.unregisterReceiver(mSyncTriggerReceiver); + mSyncTriggerReceiver = null; + } mReceiverManager.unregisterAllReceivers(mContext); } @@ -183,6 +211,38 @@ public class RunConditionMonitor { } } + private class SyncTriggerReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + LogV("SyncTriggerReceiver: onReceive"); + boolean prefRunOnTimeSchedule = mPreferences.getBoolean(Constants.PREF_RUN_ON_TIME_SCHEDULE, false); + if (!prefRunOnTimeSchedule) { + mTimeConditionMatch = false; + } else { + /** + * Toggle the "digital input" for this condition as the condition change is + * triggered by a time schedule and not the OS notifying us. + */ + mTimeConditionMatch = !mTimeConditionMatch; + updateShouldRunDecision(); + } + + /** + * Reschedule the job. + * If we are within a "SyncthingNative should run" time frame, + * let the receiver fire and change to "SyncthingNative shouldn't run" after + * TRIGGERED_SYNC_DURATION_SECS seconds elapsed. + * If we are within a "SyncthingNative shouldn't run" time frame, + * let the receiver fire and change to "SyncthingNative should run" after + * WAIT_FOR_NEXT_SYNC_DELAY_SECS seconds elapsed. + */ + JobUtils.scheduleSyncTriggerServiceJob( + context, + mTimeConditionMatch ? Constants.TRIGGERED_SYNC_DURATION_SECS : Constants.WAIT_FOR_NEXT_SYNC_DELAY_SECS + ); + } + } + /** * Event handler that is fired after preconditions changed. * We then need to decide if syncthing should run. @@ -301,6 +361,15 @@ public class RunConditionMonitor { boolean prefRespectPowerSaving = mPreferences.getBoolean(Constants.PREF_RESPECT_BATTERY_SAVING, true); boolean prefRespectMasterSync = mPreferences.getBoolean(Constants.PREF_RESPECT_MASTER_SYNC, false); boolean prefRunInFlightMode = mPreferences.getBoolean(Constants.PREF_RUN_IN_FLIGHT_MODE, false); + boolean prefRunOnTimeSchedule = mPreferences.getBoolean(Constants.PREF_RUN_ON_TIME_SCHEDULE, false); + + // PREF_RUN_ON_TIME_SCHEDULE + if (prefRunOnTimeSchedule && !mTimeConditionMatch) { + // Currently, we aren't within a "SyncthingNative should run" time frame. + LogV("decideShouldRun: PREF_RUN_ON_TIME_SCHEDULE && !mTimeConditionMatch"); + mRunDecisionExplanation = res.getString(R.string.reason_not_within_time_frame); + return false; + } // PREF_POWER_SOURCE switch (prefPowerSource) { diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncTriggerJobService.java b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncTriggerJobService.java new file mode 100644 index 00000000..4b1643b9 --- /dev/null +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncTriggerJobService.java @@ -0,0 +1,37 @@ +package com.nutomic.syncthingandroid.service; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.RequiresApi; +import android.support.v4.content.LocalBroadcastManager; +// import android.util.Log; + +import com.nutomic.syncthingandroid.service.Constants; +import com.nutomic.syncthingandroid.service.RunConditionMonitor; +import com.nutomic.syncthingandroid.util.JobUtils; + +/** + * SyncTriggerJobService to be scheduled by the JobScheduler. + * See {@link JobUtils#scheduleSyncTriggerServiceJob} for more details. + */ +@RequiresApi(21) +public class SyncTriggerJobService extends JobService { + private static final String TAG = "SyncTriggerJobService"; + + @Override + public boolean onStartJob(JobParameters params) { + // Log.v(TAG, "onStartJob: Job fired."); + Context context = getApplicationContext(); + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context); + Intent intent = new Intent(RunConditionMonitor.ACTION_SYNC_TRIGGER_FIRED); + localBroadcastManager.sendBroadcast(intent); + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + return true; + } +} diff --git a/app/src/main/java/com/nutomic/syncthingandroid/util/JobUtils.java b/app/src/main/java/com/nutomic/syncthingandroid/util/JobUtils.java new file mode 100644 index 00000000..59c039d6 --- /dev/null +++ b/app/src/main/java/com/nutomic/syncthingandroid/util/JobUtils.java @@ -0,0 +1,49 @@ +package com.nutomic.syncthingandroid.util; + +import android.annotation.TargetApi; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.content.ComponentName; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import com.nutomic.syncthingandroid.service.SyncTriggerJobService; + +public class JobUtils { + + private static final String TAG = "JobUtils"; + + private static final int TOLERATED_INACCURACY_IN_SECONDS = 120; + + @TargetApi(21) + public static void scheduleSyncTriggerServiceJob(Context context, int delayInSeconds) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return; + } + ComponentName serviceComponent = new ComponentName(context, SyncTriggerJobService.class); + JobInfo.Builder builder = new JobInfo.Builder(0, serviceComponent); + + // Wait at least "delayInSeconds". + builder.setMinimumLatency(delayInSeconds * 1000); + + // Maximum tolerated delay. + builder.setOverrideDeadline((delayInSeconds + TOLERATED_INACCURACY_IN_SECONDS) * 1000); + + // Schedule the start of "SyncTriggerJobService" in "X" seconds. + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(context.JOB_SCHEDULER_SERVICE); + jobScheduler.schedule(builder.build()); + Log.i(TAG, "Scheduled SyncTriggerJobService to run in " + + Integer.toString(delayInSeconds) + + "(+" + Integer.toString(TOLERATED_INACCURACY_IN_SECONDS) + ") seconds."); + } + + @TargetApi(21) + public static void cancelAllScheduledJobs(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return; + } + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(context.JOB_SCHEDULER_SERVICE); + jobScheduler.cancelAll(); + } +} diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 50514366..3aecb00f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -454,6 +454,9 @@ Bitte melden Sie auftretende Probleme via GitHub. Starte ohne Netzwerkverbindung Durch Aktivieren der Option wird Syncthing auch dann laufen, wenn Du offline bist. Aktiviere dies, wenn dein Telefon Probleme beim Erkennen von manuell hergestellten WLAN-Verbindungen im Flugmodus hat. + Gemäß Zeitplan synchronisieren + Durch Aktivieren dieser Option wird versucht, stündlich für 5 Minuten zu synchronisieren. Dies kann eine Menge Batterie einsparen, erfordert jedoch, dass Synchronisierungspartner online sind. Hinweis: Dies kann unvollständige temporäre Dateien zurücklassen, bis die nächste geplante Synchronisierung stattfindet und abgeschlossen ist. + Autostart Starte die App automatisch beim Hochfahren. @@ -760,6 +763,7 @@ Bitte melden Sie auftretende Probleme via GitHub. + Sie haben \'Gemäß Zeitplan synchronisieren\' aktiviert, und die letzte Synchronisierung ist nicht länger als eine Stunde her. Telefon wird nicht aufgeladen Telefon wird nicht batteriebetrieben Syncthing läuft nicht, weil das Telefon im Energiesparmodus ist. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9a0a746c..27205732 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -457,6 +457,9 @@ Please report any problems you encounter via Github. Run without network connection Enabling this option will cause Syncthing to run even when you\'re offline. Enable if your device has problems detecting manual Wi-Fi connections during flight mode. + Run according to time schedule + Enabling this option will attempt to sync every hour for the duration of 5 minutes. This can save a lot of battery but requires sync partners to be online. Please note: This may leave incomplete temporary files behind until the next scheduled sync takes place. + Autostart Start app automatically on operating system startup. @@ -781,6 +784,7 @@ Please report any problems you encounter via Github. + You enabled \'Run according to time schedule\' and the last sync was no more than an hour ago. Phone is not charging. Phone is not running on battery power. Syncthing is not running as the phone is currently power saving. diff --git a/app/src/main/res/xml/app_settings.xml b/app/src/main/res/xml/app_settings.xml index d363eaf4..be93e943 100644 --- a/app/src/main/res/xml/app_settings.xml +++ b/app/src/main/res/xml/app_settings.xml @@ -72,6 +72,13 @@ android:summary="@string/run_in_flight_mode_summary" android:defaultValue="false" /> + + +