diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/StateDialogActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/StateDialogActivity.java index f91e2c51..150aee76 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/StateDialogActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/StateDialogActivity.java @@ -5,18 +5,23 @@ import android.content.Intent; import android.databinding.DataBindingUtil; import android.os.Bundle; import android.os.Handler; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.ActivityCompat; import android.view.View; import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.databinding.DialogLoadingBinding; +import com.nutomic.syncthingandroid.model.RunConditionCheckResult; import com.nutomic.syncthingandroid.service.SyncthingService; import com.nutomic.syncthingandroid.service.SyncthingService.State; import com.nutomic.syncthingandroid.util.Util; +import java.util.Collection; import java.util.concurrent.TimeUnit; +import static com.nutomic.syncthingandroid.model.RunConditionCheckResult.*; + /** * Handles loading/disabled dialogs. */ @@ -32,8 +37,10 @@ public abstract class StateDialogActivity extends SyncthingActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - registerOnServiceConnectedListener(() -> - getService().registerOnServiceStateChangeListener(this::onServiceStateChange)); + registerOnServiceConnectedListener(() -> { + getService().registerOnServiceStateChangeListener(this::onServiceStateChange); + getService().registerOnRunConditionCheckResultChange(this::onRunConditionCheckResultChange); + }); } @Override @@ -62,6 +69,7 @@ public abstract class StateDialogActivity extends SyncthingActivity { super.onDestroy(); if (getService() != null) { getService().unregisterOnServiceStateChangeListener(this::onServiceStateChange); + getService().unregisterOnRunConditionCheckResultChange(this::onRunConditionCheckResultChange); } dismissDisabledDialog(); } @@ -89,13 +97,20 @@ public abstract class StateDialogActivity extends SyncthingActivity { } } + private void onRunConditionCheckResultChange(RunConditionCheckResult result) { + if (mDisabledDialog != null && mDisabledDialog.isShowing()) { + mDisabledDialog.setMessage(getDisabledDialogMessage()); + } + } + private void showDisabledDialog() { if (this.isFinishing() && (mDisabledDialog != null)) { return; } + mDisabledDialog = new AlertDialog.Builder(this) .setTitle(R.string.syncthing_disabled_title) - .setMessage(R.string.syncthing_disabled_message) + .setMessage(getDisabledDialogMessage()) .setPositiveButton(R.string.syncthing_disabled_change_settings, (dialogInterface, i) -> { Intent intent = new Intent(this, SettingsActivity.class); @@ -110,6 +125,26 @@ public abstract class StateDialogActivity extends SyncthingActivity { .show(); } + @NonNull + private StringBuilder getDisabledDialogMessage() { + StringBuilder message = new StringBuilder(); + message.append(this.getResources().getString(R.string.syncthing_disabled_message)); + Collection reasons = getService().getCurrentRunConditionCheckResult().getBlockReasons(); + if (!reasons.isEmpty()) { + message.append("\n"); + message.append("\n"); + message.append(this.getResources().getString(R.string.syncthing_disabled_reason_heading)); + int count = 0; + for (BlockerReason reason : reasons) { + count++; + message.append("\n"); + if (reasons.size() > 1) message.append(count + ". "); + message.append(this.getString(reason.getResId())); + } + } + return message; + } + private void dismissDisabledDialog() { Util.dismissDialogSafe(mDisabledDialog, this); mDisabledDialog = null; diff --git a/app/src/main/java/com/nutomic/syncthingandroid/model/RunConditionCheckResult.java b/app/src/main/java/com/nutomic/syncthingandroid/model/RunConditionCheckResult.java new file mode 100644 index 00000000..7250ed4d --- /dev/null +++ b/app/src/main/java/com/nutomic/syncthingandroid/model/RunConditionCheckResult.java @@ -0,0 +1,78 @@ +package com.nutomic.syncthingandroid.model; + +import com.nutomic.syncthingandroid.R; + +import java.util.Collections; +import java.util.List; + +public class RunConditionCheckResult { + + public enum BlockerReason { + ON_BATTERY(R.string.syncthing_disabled_reason_on_battery), + ON_CHARGER(R.string.syncthing_disabled_reason_on_charger), + POWERSAVING_ENABLED(R.string.syncthing_disabled_reason_powersaving), + GLOBAL_SYNC_DISABLED(R.string.syncthing_disabled_reason_android_sync_disabled), + WIFI_SSID_NOT_WHITELISTED(R.string.syncthing_disabled_reason_wifi_ssid_not_whitelisted), + WIFI_WIFI_IS_METERED(R.string.syncthing_disabled_reason_wifi_is_metered), + NO_NETWORK_OR_FLIGHTMODE(R.string.syncthing_disabled_reason_no_network_or_flightmode), + NO_MOBILE_CONNECTION(R.string.syncthing_disabled_reason_no_mobile_connection), + NO_WIFI_CONNECTION(R.string.syncthing_disabled_reason_no_wifi_connection), + NO_ALLOWED_NETWORK(R.string.syncthing_disabled_reason_no_allowed_method); + + private final int resId; + + BlockerReason(int resId) { + this.resId = resId; + } + + public int getResId() { + return resId; + } + } + + public static final RunConditionCheckResult SHOULD_RUN = new RunConditionCheckResult(); + + private final boolean mShouldRun; + private final List mBlockReasons; + + /** + * Use SHOULD_RUN instead. + * Note: of course anybody could still construct it by providing an empty list to the other + * constructor. + */ + private RunConditionCheckResult() { + this(Collections.emptyList()); + } + + public RunConditionCheckResult(List blockReasons) { + mBlockReasons = Collections.unmodifiableList(blockReasons); + mShouldRun = blockReasons.isEmpty(); + } + + + public List getBlockReasons() { + return mBlockReasons; + } + + public boolean isShouldRun() { + return mShouldRun; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RunConditionCheckResult that = (RunConditionCheckResult) o; + + if (mShouldRun != that.mShouldRun) return false; + return mBlockReasons.equals(that.mBlockReasons); + } + + @Override + public int hashCode() { + int result = (mShouldRun ? 1 : 0); + result = 31 * result + mBlockReasons.hashCode(); + return result; + } +} 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 aa659549..99a3ba90 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/RunConditionMonitor.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/RunConditionMonitor.java @@ -16,19 +16,21 @@ import android.os.BatteryManager; import android.os.Build; import android.os.PowerManager; import android.support.annotation.Nullable; -import android.support.v4.content.LocalBroadcastManager; import android.util.Log; -import com.google.common.collect.Lists; import com.nutomic.syncthingandroid.SyncthingApp; -import com.nutomic.syncthingandroid.service.ReceiverManager; +import com.nutomic.syncthingandroid.model.RunConditionCheckResult; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.inject.Inject; +import static com.nutomic.syncthingandroid.model.RunConditionCheckResult.*; +import static com.nutomic.syncthingandroid.model.RunConditionCheckResult.BlockerReason.*; + /** * Holds information about the current wifi and charging state of the device. * @@ -52,7 +54,7 @@ public class RunConditionMonitor { }; public interface OnRunConditionChangedListener { - void onRunConditionChanged(boolean shouldRun); + void onRunConditionChanged(RunConditionCheckResult result); } private final Context mContext; @@ -60,14 +62,14 @@ public class RunConditionMonitor { private ReceiverManager mReceiverManager; /** - * Sending callback notifications through {@link OnDeviceStateChangedListener} is enabled if not null. + * Sending callback notifications through {@link OnRunConditionChangedListener} is enabled if not null. */ private @Nullable OnRunConditionChangedListener mOnRunConditionChangedListener = null; /** - * Stores the result of the last call to {@link decideShouldRun}. + * Stores the result of the last call to {@link #decideShouldRun()}. */ - private boolean lastDeterminedShouldRun = false; + private RunConditionCheckResult lastRunConditionCheckResult; public RunConditionMonitor(Context context, OnRunConditionChangedListener listener) { Log.v(TAG, "Created new instance"); @@ -140,21 +142,25 @@ public class RunConditionMonitor { } public void updateShouldRunDecision() { - // Check if the current conditions changed the result of decideShouldRun() + // Reason if the current conditions changed the result of decideShouldRun() // compared to the last determined result. - boolean newShouldRun = decideShouldRun(); - if (newShouldRun != lastDeterminedShouldRun) { + RunConditionCheckResult result = decideShouldRun(); + boolean change; + synchronized (this) { + change = lastRunConditionCheckResult == null || !lastRunConditionCheckResult.equals(result); + lastRunConditionCheckResult = result; + } + if (change) { if (mOnRunConditionChangedListener != null) { - mOnRunConditionChangedListener.onRunConditionChanged(newShouldRun); + mOnRunConditionChangedListener.onRunConditionChanged(result); } - lastDeterminedShouldRun = newShouldRun; } } /** * Determines if Syncthing should currently run. */ - private boolean decideShouldRun() { + private RunConditionCheckResult decideShouldRun() { // Get run conditions preferences. boolean prefRunOnMobileData= mPreferences.getBoolean(Constants.PREF_RUN_ON_MOBILE_DATA, false); boolean prefRunOnWifi= mPreferences.getBoolean(Constants.PREF_RUN_ON_WIFI, true); @@ -166,18 +172,20 @@ public class RunConditionMonitor { boolean prefRespectPowerSaving = mPreferences.getBoolean(Constants.PREF_RESPECT_BATTERY_SAVING, true); boolean prefRespectMasterSync = mPreferences.getBoolean(Constants.PREF_RESPECT_MASTER_SYNC, false); + List blockerReasons = new ArrayList<>(); + // PREF_POWER_SOURCE switch (prefPowerSource) { case POWER_SOURCE_CHARGER: if (!isCharging()) { Log.v(TAG, "decideShouldRun: POWER_SOURCE_AC && !isCharging"); - return false; + blockerReasons.add(ON_BATTERY); } break; case POWER_SOURCE_BATTERY: if (isCharging()) { Log.v(TAG, "decideShouldRun: POWER_SOURCE_BATTERY && isCharging"); - return false; + blockerReasons.add(ON_CHARGER); } break; case POWER_SOURCE_CHARGER_BATTERY: @@ -189,35 +197,43 @@ public class RunConditionMonitor { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (prefRespectPowerSaving && isPowerSaving()) { Log.v(TAG, "decideShouldRun: prefRespectPowerSaving && isPowerSaving"); - return false; + blockerReasons.add(POWERSAVING_ENABLED); } } // Android global AutoSync setting. if (prefRespectMasterSync && !ContentResolver.getMasterSyncAutomatically()) { Log.v(TAG, "decideShouldRun: prefRespectMasterSync && !getMasterSyncAutomatically"); - return false; + blockerReasons.add(GLOBAL_SYNC_DISABLED); } // Run on mobile data. - if (prefRunOnMobileData && isMobileDataConnection()) { + if (blockerReasons.isEmpty() && prefRunOnMobileData && isMobileDataConnection()) { Log.v(TAG, "decideShouldRun: prefRunOnMobileData && isMobileDataConnection"); - return true; + return SHOULD_RUN; } // Run on wifi. if (prefRunOnWifi && isWifiOrEthernetConnection()) { if (prefRunOnMeteredWifi) { - // We are on non-metered or metered wifi. Check if wifi whitelist run condition is met. + // We are on non-metered or metered wifi. Reason if wifi whitelist run condition is met. if (wifiWhitelistConditionMet(prefWifiWhitelistEnabled, whitelistedWifiSsids)) { Log.v(TAG, "decideShouldRun: prefRunOnWifi && isWifiOrEthernetConnection && prefRunOnMeteredWifi && wifiWhitelistConditionMet"); - return true; + if (blockerReasons.isEmpty()) return SHOULD_RUN; + } else { + blockerReasons.add(WIFI_SSID_NOT_WHITELISTED); } } else { - // Check if we are on a non-metered wifi and if wifi whitelist run condition is met. - if (!isMeteredNetworkConnection() && wifiWhitelistConditionMet(prefWifiWhitelistEnabled, whitelistedWifiSsids)) { - Log.v(TAG, "decideShouldRun: prefRunOnWifi && isWifiOrEthernetConnection && !prefRunOnMeteredWifi && !isMeteredNetworkConnection && wifiWhitelistConditionMet"); - return true; + // Reason if we are on a non-metered wifi and if wifi whitelist run condition is met. + if (!isMeteredNetworkConnection()) { + if (wifiWhitelistConditionMet(prefWifiWhitelistEnabled, whitelistedWifiSsids)) { + Log.v(TAG, "decideShouldRun: prefRunOnWifi && isWifiOrEthernetConnection && !prefRunOnMeteredWifi && !isMeteredNetworkConnection && wifiWhitelistConditionMet"); + if (blockerReasons.isEmpty()) return SHOULD_RUN; + } else { + blockerReasons.add(WIFI_SSID_NOT_WHITELISTED); + } + } else { + blockerReasons.add(WIFI_WIFI_IS_METERED); } } } @@ -225,14 +241,27 @@ public class RunConditionMonitor { // Run in flight mode. if (prefRunInFlightMode && isFlightMode()) { Log.v(TAG, "decideShouldRun: prefRunInFlightMode && isFlightMode"); - return true; + if (blockerReasons.isEmpty()) return SHOULD_RUN; } /** * If none of the above run conditions matched, don't run. */ Log.v(TAG, "decideShouldRun: return false"); - return false; + if (blockerReasons.isEmpty()) { + if (isFlightMode()) { + blockerReasons.add(NO_NETWORK_OR_FLIGHTMODE); + } else if (!prefRunOnWifi && !prefRunOnMobileData) { + blockerReasons.add(NO_ALLOWED_NETWORK); + } else if (prefRunOnMobileData) { + blockerReasons.add(NO_MOBILE_CONNECTION); + } else if (prefRunOnWifi) { + blockerReasons.add(NO_WIFI_CONNECTION); + } else { + blockerReasons.add(NO_NETWORK_OR_FLIGHTMODE); + } + } + return new RunConditionCheckResult(blockerReasons); } /** diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java index 99fefa3b..502bebdf 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java @@ -1,33 +1,34 @@ package com.nutomic.syncthingandroid.service; +import android.Manifest; import android.app.Service; import android.content.Intent; -import android.content.pm.PackageManager; import android.content.SharedPreferences; -import android.Manifest; +import android.content.pm.PackageManager; import android.os.AsyncTask; import android.os.Handler; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.Log; -import android.widget.Toast; import com.android.PRNGFixes; -import com.annimon.stream.Stream; import com.google.common.io.Files; import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.SyncthingApp; import com.nutomic.syncthingandroid.http.PollWebGuiAvailableTask; -import com.nutomic.syncthingandroid.model.Folder; +import com.nutomic.syncthingandroid.model.RunConditionCheckResult; import com.nutomic.syncthingandroid.util.ConfigXml; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.net.URL; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; @@ -99,6 +100,10 @@ public class SyncthingService extends Service { void onServiceStateChange(State currentState); } + public interface OnRunConditionCheckResultListener { + void onRunConditionCheckResultChanged(RunConditionCheckResult result); + } + /** * Indicates the current state of SyncthingService and of Syncthing itself. */ @@ -124,6 +129,7 @@ public class SyncthingService extends Service { * {@link onStartCommand}. */ private State mCurrentState = State.DISABLED; + private AtomicReference mCurrentCheckResult = new AtomicReference<>(RunConditionCheckResult.SHOULD_RUN); private ConfigXml mConfig; private @Nullable PollWebGuiAvailableTask mPollWebGuiAvailableTask = null; @@ -136,6 +142,7 @@ public class SyncthingService extends Service { private Handler mHandler; private final HashSet mOnServiceStateChangeListeners = new HashSet<>(); + private final HashSet mOnRunConditionCheckResultListeners = new HashSet<>(); private final SyncthingServiceBinder mBinder = new SyncthingServiceBinder(this); @Inject NotificationHandler mNotificationHandler; @@ -262,7 +269,13 @@ public class SyncthingService extends Service { * function is called to notify this class to run/terminate the syncthing binary. * {@link #onServiceStateChange} is called while applying the decision change. */ - private void onUpdatedShouldRunDecision(boolean newShouldRunDecision) { + private void onUpdatedShouldRunDecision(RunConditionCheckResult result) { + boolean newShouldRunDecision = result.isShouldRun(); + boolean reasonsChanged = !mCurrentCheckResult.getAndSet(result).equals(result); + if (reasonsChanged) { + onRunConditionCheckResultChange(result); + } + if (newShouldRunDecision != mLastDeterminedShouldRun) { Log.i(TAG, "shouldRun decision changed to " + newShouldRunDecision + " according to configured run conditions."); mLastDeterminedShouldRun = newShouldRunDecision; @@ -578,6 +591,30 @@ public class SyncthingService extends Service { }); } + public void registerOnRunConditionCheckResultChange(OnRunConditionCheckResultListener listener) { + listener.onRunConditionCheckResultChanged(mCurrentCheckResult.get()); + mOnRunConditionCheckResultListeners.add(listener); + } + + public void unregisterOnRunConditionCheckResultChange(OnRunConditionCheckResultListener listener) { + mOnRunConditionCheckResultListeners.remove(listener); + } + + private void onRunConditionCheckResultChange(RunConditionCheckResult result) { + mHandler.post(() -> { + for (Iterator i = mOnRunConditionCheckResultListeners.iterator(); + i.hasNext(); ) { + OnRunConditionCheckResultListener listener = i.next(); + if (listener != null) { + listener.onRunConditionCheckResultChanged(result); + } else { + i.remove(); + } + } + }); + } + + public URL getWebGuiUrl() { return mConfig.getWebGuiUrl(); } @@ -586,6 +623,10 @@ public class SyncthingService extends Service { return mCurrentState; } + public RunConditionCheckResult getCurrentRunConditionCheckResult() { + return mCurrentCheckResult.get(); + } + public NotificationHandler getNotificationHandler() { return mNotificationHandler; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e65c80a..b7c9a37a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -653,6 +653,19 @@ Please report any problems you encounter via Github. Exit + + Reasons: + Device is running on battery + Device is running on charger + Device is in power-saving mode + Global Synchronization is disabled + Current WiFi SSID is not whitelisted + Current WiFi is metered + No network connection or airplane mode enabled + Not connected to mobile data + Not connected to WiFi + Not configured to sync on WiFi nor mobile data + Syncthing is running