Skip to content

Commit

Permalink
Add option to trigger sync every hour (fixes #15) (#387)
Browse files Browse the repository at this point in the history
* AndroidManifest: Add SyncTriggerJobService

* Add Constants#isRunningOnEmulator

* Add Constants: WAIT_FOR_NEXT_SYNC_DELAY_SECS, TRIGGERED_SYNC_DURATION_SECS

* WIP: Schedule Job in BootReceiver

* Add Util/JobUtils

* Util/JobUtils: Improve log text

* Add service/SyncTriggerJobService

scheduled by JobScheduler. See JobUtils#scheduleSyncTriggerServiceJob for more details.

* RunConditionMonitor: Add SyncTriggerReceiver

via LocalBroadcastReceiver.

* BootReceiver: Add ToDo

* Add pref: PREF_RUN_ON_TIME_SCHEDULE

* Fine tune debug constants - time intervals

* JobUtils: In seconds please

* Add strings: en-GB

* RunConditionMonitor: Implement hourly sync time frames (fixes #15)

* Imported translation: de

* Fix lint: .JOB_SCHEDULER_SERVICE, API 21 instead of 23

* JobUtils: Noop on Android API level before 21 (L)

* Fix lint: RequiresApi(21) for SyncTriggerJobService

* Hide pref "run on time schedule" on Android < 5.x

* JobUtils: Show time of grace in brackets when logged

* BootReceiver: Realign comment, remove unnecessary code
  • Loading branch information
Catfriend1 authored Mar 29, 2019
1 parent 3e248fa commit 976d9f9
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 0 deletions.
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@
android:value=".activities.MainActivity" />
</activity>
<service android:name=".service.SyncthingService" />
<service
android:name=".service.SyncTriggerJobService"
android:label="SyncTriggerJobService"
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
<receiver android:name=".receiver.BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,11 @@ public void onActivityCreated(Bundle savedInstanceState) {
);

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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -123,6 +124,13 @@ public static String DYN_PREF_OBJECT_SYNC_ON_MOBILE_DATA(String objectPrefixAndI
: 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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
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;
Expand All @@ -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";
Expand Down Expand Up @@ -87,9 +92,17 @@ private class SyncConditionResult {

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;

Expand Down Expand Up @@ -142,16 +155,31 @@ public RunConditionMonitor(Context context,
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);
}

Expand Down Expand Up @@ -183,6 +211,38 @@ public void onReceive(Context context, Intent intent) {
}
}

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.
Expand Down Expand Up @@ -301,6 +361,15 @@ private boolean decideShouldRun() {
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
49 changes: 49 additions & 0 deletions app/src/main/java/com/nutomic/syncthingandroid/util/JobUtils.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
4 changes: 4 additions & 0 deletions app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,9 @@ Bitte melden Sie auftretende Probleme via GitHub.</string>
<string name="run_in_flight_mode_title">Starte ohne Netzwerkverbindung</string>
<string name="run_in_flight_mode_summary">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.</string>

<string name="run_on_time_schedule_title">Gemäß Zeitplan synchronisieren</string>
<string name="run_on_time_schedule_summary">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.</string>

<!-- Preferences - Behaviour -->
<string name="behaviour_autostart_title">Autostart</string>
<string name="behaviour_autostart_summary">Starte die App automatisch beim Hochfahren.</string>
Expand Down Expand Up @@ -760,6 +763,7 @@ Bitte melden Sie auftretende Probleme via GitHub.</string>
<!-- RunConditionMonitor -->

<!-- Explanations why syncthing is running or not running -->
<string name="reason_not_within_time_frame">Sie haben \'Gemäß Zeitplan synchronisieren\' aktiviert, und die letzte Synchronisierung ist nicht länger als eine Stunde her.</string>
<string name="reason_not_charging">Telefon wird nicht aufgeladen</string>
<string name="reason_not_on_battery_power">Telefon wird nicht batteriebetrieben</string>
<string name="reason_not_while_power_saving">Syncthing läuft nicht, weil das Telefon im Energiesparmodus ist.</string>
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,9 @@ Please report any problems you encounter via Github.</string>
<string name="run_in_flight_mode_title">Run without network connection</string>
<string name="run_in_flight_mode_summary">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.</string>

<string name="run_on_time_schedule_title">Run according to time schedule</string>
<string name="run_on_time_schedule_summary">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.</string>

<!-- Preferences - Behaviour -->
<string name="behaviour_autostart_title">Autostart</string>
<string name="behaviour_autostart_summary">Start app automatically on operating system startup.</string>
Expand Down Expand Up @@ -781,6 +784,7 @@ Please report any problems you encounter via Github.</string>
<!-- RunConditionMonitor -->

<!-- Explanations why syncthing is running or not running -->
<string name="reason_not_within_time_frame">You enabled \'Run according to time schedule\' and the last sync was no more than an hour ago.</string>
<string name="reason_not_charging">Phone is not charging.</string>
<string name="reason_not_on_battery_power">Phone is not running on battery power.</string>
<string name="reason_not_while_power_saving">Syncthing is not running as the phone is currently power saving.</string>
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/xml/app_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@
android:summary="@string/run_in_flight_mode_summary"
android:defaultValue="false" />

<!-- Run on time schedule -->
<CheckBoxPreference
android:key="run_on_time_schedule"
android:title="@string/run_on_time_schedule_title"
android:summary="@string/run_on_time_schedule_summary"
android:defaultValue="false" />

</PreferenceScreen>

<PreferenceScreen
Expand Down

0 comments on commit 976d9f9

Please sign in to comment.