Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to trigger sync every hour (fixes #15) #387

Merged
merged 24 commits into from
Mar 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
97a9765
AndroidManifest: Add SyncTriggerJobService
Catfriend1 Mar 25, 2019
00588e7
Add Constants#isRunningOnEmulator
Catfriend1 Mar 25, 2019
60adf70
Add Constants: WAIT_FOR_NEXT_SYNC_DELAY_SECS, TRIGGERED_SYNC_DURATION…
Catfriend1 Mar 25, 2019
b02622f
WIP: Schedule Job in BootReceiver
Catfriend1 Mar 25, 2019
8773113
Add Util/JobUtils
Catfriend1 Mar 25, 2019
fc4c35a
Util/JobUtils: Improve log text
Catfriend1 Mar 25, 2019
0e53354
Add service/SyncTriggerJobService
Catfriend1 Mar 25, 2019
210f898
RunConditionMonitor: Add SyncTriggerReceiver
Catfriend1 Mar 25, 2019
ee55c24
BootReceiver: Add ToDo
Catfriend1 Mar 25, 2019
655b38a
Add pref: PREF_RUN_ON_TIME_SCHEDULE
Catfriend1 Mar 28, 2019
c9aaa29
Fine tune debug constants - time intervals
Catfriend1 Mar 28, 2019
d5b1766
JobUtils: In seconds please
Catfriend1 Mar 28, 2019
651c433
Add strings: en-GB
Catfriend1 Mar 28, 2019
293d2e7
RunConditionMonitor: Implement hourly sync time frames (fixes #15)
Catfriend1 Mar 28, 2019
fe65c5c
Imported translation: de
Catfriend1 Mar 28, 2019
7bcf339
Merge branch 'master' into 20190325-jobScheduler
Catfriend1 Mar 28, 2019
8956c8c
Fix lint: .JOB_SCHEDULER_SERVICE, API 21 instead of 23
Catfriend1 Mar 28, 2019
3dd3e00
JobUtils: Noop on Android API level before 21 (L)
Catfriend1 Mar 28, 2019
1173047
Fix lint: RequiresApi(21) for SyncTriggerJobService
Catfriend1 Mar 28, 2019
bc623f4
Merge branch 'master' into 20190325-jobScheduler
Catfriend1 Mar 28, 2019
0ae9ed2
Hide pref "run on time schedule" on Android < 5.x
Catfriend1 Mar 28, 2019
6297f55
Merge branch 'master' into 20190325-jobScheduler
Catfriend1 Mar 28, 2019
6f1824c
JobUtils: Show time of grace in brackets when logged
Catfriend1 Mar 29, 2019
cc7359b
BootReceiver: Realign comment, remove unnecessary code
Catfriend1 Mar 29, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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