1
0
Fork 0
mirror of https://github.com/syncthing/syncthing-android.git synced 2024-12-23 03:11:30 +00:00

Show reasons for disabled service in notification dialog (#1264)

This commit is contained in:
Martin Carpella 2019-01-21 22:05:45 +01:00 committed by Audrius Butkevicius
parent 9796d7beb1
commit 700c55e9d3
5 changed files with 233 additions and 37 deletions

View file

@ -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<BlockerReason> 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;

View file

@ -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<BlockerReason> 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<BlockerReason> blockReasons) {
mBlockReasons = Collections.unmodifiableList(blockReasons);
mShouldRun = blockReasons.isEmpty();
}
public List<BlockerReason> 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;
}
}

View file

@ -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<BlockerReason> 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);
}
/**

View file

@ -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<RunConditionCheckResult> 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<OnServiceStateChangeListener> mOnServiceStateChangeListeners = new HashSet<>();
private final HashSet<OnRunConditionCheckResultListener> 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<OnRunConditionCheckResultListener> 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;
}

View file

@ -653,6 +653,19 @@ Please report any problems you encounter via Github.</string>
<!-- Button text on the "syncthing disabled" dialog, used as menu item to stop syncthing service if "always_run_in_background" is true -->
<string name="exit">Exit</string>
<!-- Reasons for "syncthing disabled" dialog -->
<string name="syncthing_disabled_reason_heading">Reasons:</string>
<string name="syncthing_disabled_reason_on_battery">Device is running on battery</string>
<string name="syncthing_disabled_reason_on_charger">Device is running on charger</string>
<string name="syncthing_disabled_reason_powersaving">Device is in power-saving mode</string>
<string name="syncthing_disabled_reason_android_sync_disabled">Global Synchronization is disabled</string>
<string name="syncthing_disabled_reason_wifi_ssid_not_whitelisted">Current WiFi SSID is not whitelisted</string>
<string name="syncthing_disabled_reason_wifi_is_metered">Current WiFi is metered</string>
<string name="syncthing_disabled_reason_no_network_or_flightmode">No network connection or airplane mode enabled</string>
<string name="syncthing_disabled_reason_no_mobile_connection">Not connected to mobile data</string>
<string name="syncthing_disabled_reason_no_wifi_connection">Not connected to WiFi</string>
<string name="syncthing_disabled_reason_no_allowed_method">Not configured to sync on WiFi nor mobile data</string>
<!-- Title of the notification shown while syncthing is running and enabled -->
<string name="syncthing_active">Syncthing is running</string>