mirror of
https://github.com/syncthing/syncthing-android.git
synced 2024-12-27 05:11:30 +00:00
Handle storage permissions Fix multiple processes being started.
This commit is contained in:
parent
6a4c99848d
commit
165c136bea
7 changed files with 140 additions and 59 deletions
|
@ -30,7 +30,8 @@
|
|||
android:name=".SyncthingApp">
|
||||
<activity
|
||||
android:name=".activities.FirstStartActivity"
|
||||
android:label="@string/app_name">
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleInstance">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
|
|
@ -107,6 +107,7 @@ public class FirstStartActivity extends Activity implements Button.OnClickListen
|
|||
grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(this, R.string.toast_write_storage_permission_required,
|
||||
Toast.LENGTH_LONG).show();
|
||||
this.finish();
|
||||
} else {
|
||||
startApp();
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import android.content.pm.PackageManager;
|
|||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.Manifest;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
@ -24,6 +25,7 @@ import android.support.design.widget.TabLayout;
|
|||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.view.GravityCompat;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v4.widget.DrawerLayout;
|
||||
|
@ -255,6 +257,17 @@ public class MainActivity extends StateDialogActivity
|
|||
onNewIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
// Check if storage permission has been revoked at runtime.
|
||||
if ((ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
|
||||
PackageManager.PERMISSION_GRANTED)) {
|
||||
startActivity(new Intent(this, FirstStartActivity.class));
|
||||
this.finish();
|
||||
}
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
|
|
@ -65,7 +65,7 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh
|
|||
}
|
||||
|
||||
public interface OnDeviceStateChangedListener {
|
||||
void onDeviceStateChanged();
|
||||
void onDeviceStateChanged(boolean shouldRun);
|
||||
}
|
||||
|
||||
private final Context mContext;
|
||||
|
@ -74,16 +74,22 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh
|
|||
private final OnDeviceStateChangedListener mListener;
|
||||
@Inject SharedPreferences mPreferences;
|
||||
|
||||
private @Nullable NetworkReceiver mNetworkReceiver;
|
||||
private @Nullable BatteryReceiver mBatteryReceiver;
|
||||
private @Nullable BroadcastReceiver mPowerSaveModeChangedReceiver;
|
||||
private @Nullable NetworkReceiver mNetworkReceiver = null;
|
||||
private @Nullable BatteryReceiver mBatteryReceiver = null;
|
||||
private @Nullable BroadcastReceiver mPowerSaveModeChangedReceiver = null;
|
||||
|
||||
private boolean mIsAllowedNetworkConnection;
|
||||
private String mWifiSsid;
|
||||
private boolean mIsCharging;
|
||||
private boolean mIsPowerSaving;
|
||||
|
||||
/**
|
||||
* Stores the result of the last call to {@link decideShouldRun}.
|
||||
*/
|
||||
private boolean lastDeterminedShouldRun = false;
|
||||
|
||||
public DeviceStateHolder(Context context, OnDeviceStateChangedListener listener) {
|
||||
Log.v(TAG, "Created new instance");
|
||||
((SyncthingApp) context.getApplicationContext()).component().inject(this);
|
||||
mContext = context;
|
||||
mBroadcastManager = LocalBroadcastManager.getInstance(mContext);
|
||||
|
@ -94,6 +100,7 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh
|
|||
}
|
||||
|
||||
public void shutdown() {
|
||||
Log.v(TAG, "Shutting down");
|
||||
mBroadcastManager.unregisterReceiver(mReceiver);
|
||||
mPreferences.unregisterOnSharedPreferenceChangeListener(this);
|
||||
|
||||
|
@ -166,9 +173,7 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh
|
|||
mIsPowerSaving = intent.getBooleanExtra(EXTRA_IS_POWER_SAVING, mIsPowerSaving);
|
||||
Log.i(TAG, "State updated, allowed network connection: " + mIsAllowedNetworkConnection +
|
||||
", charging: " + mIsCharging + ", power saving: " + mIsPowerSaving);
|
||||
|
||||
updateWifiSsid();
|
||||
mListener.onDeviceStateChanged();
|
||||
updateShouldRunDecision();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,15 +188,20 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh
|
|||
}
|
||||
}
|
||||
|
||||
public void refreshNetworkInfo() {
|
||||
updateWifiSsid();
|
||||
mListener.onDeviceStateChanged();
|
||||
public void updateShouldRunDecision() {
|
||||
// Check if the current conditions changed the result of decideShouldRun()
|
||||
// compared to the last determined result.
|
||||
boolean newShouldRun = decideShouldRun();
|
||||
if (newShouldRun != lastDeterminedShouldRun) {
|
||||
mListener.onDeviceStateChanged(newShouldRun);
|
||||
lastDeterminedShouldRun = newShouldRun;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if Syncthing should currently run.
|
||||
*/
|
||||
boolean shouldRun() {
|
||||
private boolean decideShouldRun() {
|
||||
boolean prefRespectPowerSaving = mPreferences.getBoolean("respect_battery_saving", true);
|
||||
if (prefRespectPowerSaving && mIsPowerSaving)
|
||||
return false;
|
||||
|
@ -200,6 +210,7 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh
|
|||
boolean prefStopMobileData = mPreferences.getBoolean(Constants.PREF_SYNC_ONLY_WIFI, false);
|
||||
boolean prefStopNotCharging = mPreferences.getBoolean(Constants.PREF_SYNC_ONLY_CHARGING, false);
|
||||
|
||||
updateWifiSsid();
|
||||
if (prefStopMobileData && !isWhitelistedNetworkConnection())
|
||||
return false;
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ public class NotificationHandler {
|
|||
private static final int ID_RESTART = 2;
|
||||
private static final int ID_STOP_BACKGROUND_WARNING = 3;
|
||||
private static final int ID_CRASH = 9;
|
||||
private static final int ID_MISSING_PERM = 10;
|
||||
private static final String CHANNEL_PERSISTENT = "01_syncthing_persistent";
|
||||
private static final String CHANNEL_INFO = "02_syncthing_notifications";
|
||||
private static final String CHANNEL_PERSISTENT_WAITING = "03_syncthing_persistent_waiting";
|
||||
|
@ -171,6 +172,19 @@ public class NotificationHandler {
|
|||
mNotificationManager.notify(id, n);
|
||||
}
|
||||
|
||||
public void showStoragePermissionRevokedNotification() {
|
||||
Intent intent = new Intent(mContext, FirstStartActivity.class);
|
||||
Notification n = getNotificationBuilder(mInfoChannel)
|
||||
.setContentTitle(mContext.getString(R.string.syncthing_terminated))
|
||||
.setContentText(mContext.getString(R.string.toast_write_storage_permission_required))
|
||||
.setSmallIcon(R.drawable.ic_stat_notify)
|
||||
.setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0))
|
||||
.setAutoCancel(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.build();
|
||||
mNotificationManager.notify(ID_MISSING_PERM, n);
|
||||
}
|
||||
|
||||
public void showRestartNotification() {
|
||||
Intent intent = new Intent(mContext, SyncthingService.class)
|
||||
.setAction(SyncthingService.ACTION_RESTART);
|
||||
|
|
|
@ -2,11 +2,14 @@ package com.nutomic.syncthingandroid.service;
|
|||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.SharedPreferences;
|
||||
import android.Manifest;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
@ -91,7 +94,7 @@ public class SyncthingService extends Service {
|
|||
private ConfigXml mConfig;
|
||||
private RestApi mApi;
|
||||
private EventProcessor mEventProcessor;
|
||||
private DeviceStateHolder mDeviceStateHolder;
|
||||
private @Nullable DeviceStateHolder mDeviceStateHolder = null;
|
||||
private SyncthingRunnable mSyncthingRunnable;
|
||||
private Handler mHandler;
|
||||
|
||||
|
@ -113,6 +116,31 @@ public class SyncthingService extends Service {
|
|||
*/
|
||||
private boolean mStopScheduled = false;
|
||||
|
||||
/**
|
||||
* True if the user granted the storage permission.
|
||||
*/
|
||||
private boolean mStoragePermissionGranted = false;
|
||||
|
||||
/**
|
||||
* Starts the native binary.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
PRNGFixes.apply();
|
||||
((SyncthingApp) getApplication()).component().inject(this);
|
||||
mHandler = new Handler();
|
||||
|
||||
/**
|
||||
* If runtime permissions are revoked, android kills and restarts the service.
|
||||
* see issue: https://github.com/syncthing/syncthing-android/issues/871
|
||||
* We need to recheck if we still have the storage permission.
|
||||
*/
|
||||
mStoragePermissionGranted = (ContextCompat.checkSelfPermission(this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
|
||||
PackageManager.PERMISSION_GRANTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles intents, either {@link #ACTION_RESTART}, or intents having
|
||||
* {@link DeviceStateHolder#EXTRA_IS_ALLOWED_NETWORK_CONNECTION} or
|
||||
|
@ -120,6 +148,18 @@ public class SyncthingService extends Service {
|
|||
*/
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (!mStoragePermissionGranted) {
|
||||
Log.e(TAG, "User revoked storage permission. Stopping service.");
|
||||
if (mNotificationHandler != null) {
|
||||
mNotificationHandler.showStoragePermissionRevokedNotification();
|
||||
}
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
mDeviceStateHolder = new DeviceStateHolder(SyncthingService.this, this::onUpdatedShouldRunDecision);
|
||||
mNotificationHandler.updatePersistentNotification(this);
|
||||
|
||||
if (intent == null)
|
||||
return START_STICKY;
|
||||
|
||||
|
@ -136,35 +176,39 @@ public class SyncthingService extends Service {
|
|||
new StartupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
});
|
||||
} else if (ACTION_REFRESH_NETWORK_INFO.equals(intent.getAction())) {
|
||||
mDeviceStateHolder.refreshNetworkInfo();
|
||||
mDeviceStateHolder.updateShouldRunDecision();
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks according to preferences and charging/wifi state, whether syncthing should be enabled
|
||||
* or not.
|
||||
*
|
||||
* Depending on the result, syncthing is started or stopped, and {@link #onApiChange} is
|
||||
* called.
|
||||
* After run conditions monitored by {@link DeviceStateHolder} changed and
|
||||
* it had an influence on the decision to run/terminate syncthing, this
|
||||
* function is called to notify this class to run/terminate the syncthing binary.
|
||||
* {@link #onApiChange} is called while applying the decision change.
|
||||
*/
|
||||
private void updateState() {
|
||||
// Start syncthing.
|
||||
if (mDeviceStateHolder.shouldRun()) {
|
||||
if (mCurrentState == State.ACTIVE || mCurrentState == State.STARTING) {
|
||||
mStopScheduled = false;
|
||||
return;
|
||||
private void onUpdatedShouldRunDecision(boolean shouldRun) {
|
||||
if (shouldRun) {
|
||||
// Start syncthing.
|
||||
switch (mCurrentState) {
|
||||
case DISABLED:
|
||||
case INIT:
|
||||
// HACK: Make sure there is no syncthing binary left running from an improper
|
||||
// shutdown (eg Play Store update).
|
||||
shutdown(State.INIT, () -> {
|
||||
Log.i(TAG, "Starting syncthing according to current state and preferences after State.INIT");
|
||||
new StartupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
});
|
||||
break;
|
||||
case STARTING:
|
||||
case ACTIVE:
|
||||
mStopScheduled = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// HACK: Make sure there is no syncthing binary left running from an improper
|
||||
// shutdown (eg Play Store update).
|
||||
shutdown(State.INIT, () -> {
|
||||
Log.i(TAG, "Starting syncthing according to current state and preferences");
|
||||
new StartupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
});
|
||||
}
|
||||
// Stop syncthing.
|
||||
else {
|
||||
} else {
|
||||
// Stop syncthing.
|
||||
if (mCurrentState == State.DISABLED)
|
||||
return;
|
||||
|
||||
|
@ -173,20 +217,6 @@ public class SyncthingService extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the native binary.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
PRNGFixes.apply();
|
||||
((SyncthingApp) getApplication()).component().inject(this);
|
||||
mHandler = new Handler();
|
||||
|
||||
mDeviceStateHolder = new DeviceStateHolder(SyncthingService.this, this::updateState);
|
||||
mNotificationHandler.updatePersistentNotification(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the initial configuration, and updates the config when coming from an old
|
||||
* version.
|
||||
|
@ -248,18 +278,26 @@ public class SyncthingService extends Service {
|
|||
*/
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
synchronized (mStateLock) {
|
||||
if (mCurrentState == State.INIT || mCurrentState == State.STARTING) {
|
||||
Log.i(TAG, "Delay shutting down service until initialisation of Syncthing finished");
|
||||
mStopScheduled = true;
|
||||
|
||||
} else {
|
||||
Log.i(TAG, "Shutting down service immediately");
|
||||
shutdown(State.DISABLED, () -> {});
|
||||
if (mStoragePermissionGranted) {
|
||||
synchronized (mStateLock) {
|
||||
if (mCurrentState == State.INIT || mCurrentState == State.STARTING) {
|
||||
Log.i(TAG, "Delay shutting down synchting binary until initialisation finished");
|
||||
mStopScheduled = true;
|
||||
} else {
|
||||
Log.i(TAG, "Shutting down syncthing binary immediately");
|
||||
shutdown(State.DISABLED, () -> {});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the storage permission got revoked, we did not start the binary and
|
||||
// are in State.INIT requiring an immediate shutdown of this service class.
|
||||
Log.i(TAG, "Shutting down syncthing binary due to missing storage permission.");
|
||||
shutdown(State.DISABLED, () -> {});
|
||||
}
|
||||
|
||||
mDeviceStateHolder.shutdown();
|
||||
if (mDeviceStateHolder != null) {
|
||||
mDeviceStateHolder.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -277,7 +315,8 @@ public class SyncthingService extends Service {
|
|||
if (mApi != null)
|
||||
mApi.shutdown();
|
||||
|
||||
mNotificationHandler.cancelPersistentNotification(this);
|
||||
if (mNotificationHandler != null)
|
||||
mNotificationHandler.cancelPersistentNotification(this);
|
||||
|
||||
if (mSyncthingRunnable != null) {
|
||||
mSyncthingRunnable.killSyncthing(onKilledListener);
|
||||
|
|
|
@ -578,6 +578,8 @@ Please report any problems you encounter via Github.</string>
|
|||
|
||||
<string name="syncthing_disabled">Syncthing is disabled</string>
|
||||
|
||||
<string name="syncthing_terminated">Syncthing was terminated</string>
|
||||
|
||||
<!-- Toast shown if syncthing failed to create a config -->
|
||||
<string name="config_create_failed">Failed to create config file</string>
|
||||
|
||||
|
|
Loading…
Reference in a new issue