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

Refactor SyncthingService (lifecycle), DeviceStateHolder, RestApi, multiple fixes (#1119)

This commit is contained in:
Catfriend1 2018-06-10 00:39:42 +02:00 committed by Audrius Butkevicius
parent 165c136bea
commit e9eef4332b
22 changed files with 625 additions and 381 deletions

View file

@ -213,8 +213,9 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (getService() != null) { SyncthingService syncthingService = getService();
getService().unregisterOnApiChangeListener(this::onApiChange); if (syncthingService != null) {
syncthingService.unregisterOnServiceStateChangeListener(this::onServiceStateChange);
} }
mIdView.removeTextChangedListener(mIdTextWatcher); mIdView.removeTextChangedListener(mIdTextWatcher);
mNameView.removeTextChangedListener(mNameTextWatcher); mNameView.removeTextChangedListener(mNameTextWatcher);
@ -252,7 +253,7 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
} }
private void onServiceConnected() { private void onServiceConnected() {
getService().registerOnApiChangeListener(this::onApiChange); getService().registerOnServiceStateChangeListener(this::onServiceStateChange);
} }
/** /**
@ -271,7 +272,7 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis
} }
} }
private void onApiChange(SyncthingService.State currentState) { private void onServiceStateChange(SyncthingService.State currentState) {
if (currentState != ACTIVE) { if (currentState != ACTIVE) {
finish(); finish();
return; return;

View file

@ -16,7 +16,6 @@ import android.widget.Toast;
import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.SyncthingApp; import com.nutomic.syncthingandroid.SyncthingApp;
import com.nutomic.syncthingandroid.service.SyncthingService;
import javax.inject.Inject; import javax.inject.Inject;
@ -66,8 +65,6 @@ public class FirstStartActivity extends Activity implements Button.OnClickListen
mPreferences.edit().putBoolean("first_start", false).apply(); mPreferences.edit().putBoolean("first_start", false).apply();
} }
startService(new Intent(this, SyncthingService.class));
// In case start_into_web_gui option is enabled, start both activities so that back // In case start_into_web_gui option is enabled, start both activities so that back
// navigation works as expected. // navigation works as expected.
Intent mainIntent = new Intent(this, MainActivity.class); Intent mainIntent = new Intent(this, MainActivity.class);

View file

@ -51,7 +51,7 @@ import static com.nutomic.syncthingandroid.service.SyncthingService.State.ACTIVE
* Shows folder details and allows changing them. * Shows folder details and allows changing them.
*/ */
public class FolderActivity extends SyncthingActivity public class FolderActivity extends SyncthingActivity
implements SyncthingActivity.OnServiceConnectedListener, SyncthingService.OnApiChangeListener { implements SyncthingActivity.OnServiceConnectedListener, SyncthingService.OnServiceStateChangeListener {
public static final String EXTRA_IS_CREATE = public static final String EXTRA_IS_CREATE =
"com.nutomic.syncthingandroid.activities.DeviceActivity.IS_CREATE"; "com.nutomic.syncthingandroid.activities.DeviceActivity.IS_CREATE";
@ -234,8 +234,9 @@ public class FolderActivity extends SyncthingActivity
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (getService() != null) { SyncthingService syncthingService = getService();
getService().unregisterOnApiChangeListener(this); if (syncthingService != null) {
syncthingService.unregisterOnServiceStateChangeListener(this::onServiceStateChange);
} }
mLabelView.removeTextChangedListener(mTextWatcher); mLabelView.removeTextChangedListener(mTextWatcher);
mIdView.removeTextChangedListener(mTextWatcher); mIdView.removeTextChangedListener(mTextWatcher);
@ -270,11 +271,11 @@ public class FolderActivity extends SyncthingActivity
*/ */
@Override @Override
public void onServiceConnected() { public void onServiceConnected() {
getService().registerOnApiChangeListener(this); getService().registerOnServiceStateChangeListener(this);
} }
@Override @Override
public void onApiChange(SyncthingService.State currentState) { public void onServiceStateChange(SyncthingService.State currentState) {
if (currentState != ACTIVE) { if (currentState != ACTIVE) {
finish(); finish();
return; return;
@ -308,7 +309,7 @@ public class FolderActivity extends SyncthingActivity
// If the FolderActivity gets recreated after the VersioningDialogActivity is closed, then the result from the VersioningDialogActivity will be received before // If the FolderActivity gets recreated after the VersioningDialogActivity is closed, then the result from the VersioningDialogActivity will be received before
// the mFolder variable has been recreated, so the versioning config will be stored in the mVersioning variable until the mFolder variable has been // the mFolder variable has been recreated, so the versioning config will be stored in the mVersioning variable until the mFolder variable has been
// recreated in the onApiChange(). This has been observed to happen after the screen orientation has changed while the VersioningDialogActivity was open. // recreated in the onServiceStateChange(). This has been observed to happen after the screen orientation has changed while the VersioningDialogActivity was open.
private void attemptToApplyVersioningConfig() { private void attemptToApplyVersioningConfig() {
if (mFolder != null && mVersioning != null){ if (mFolder != null && mVersioning != null){
mFolder.versioning = mVersioning; mFolder.versioning = mVersioning;

View file

@ -44,7 +44,7 @@ import java.util.Iterator;
* Activity that allows selecting a directory in the local file system. * Activity that allows selecting a directory in the local file system.
*/ */
public class FolderPickerActivity extends SyncthingActivity public class FolderPickerActivity extends SyncthingActivity
implements AdapterView.OnItemClickListener, SyncthingService.OnApiChangeListener { implements AdapterView.OnItemClickListener, SyncthingService.OnServiceStateChangeListener {
private static final String EXTRA_INITIAL_DIRECTORY = private static final String EXTRA_INITIAL_DIRECTORY =
"com.nutomic.syncthingandroid.activities.FolderPickerActivity.INITIAL_DIRECTORY"; "com.nutomic.syncthingandroid.activities.FolderPickerActivity.INITIAL_DIRECTORY";
@ -156,13 +156,16 @@ public class FolderPickerActivity extends SyncthingActivity
@Override @Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) { public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
super.onServiceConnected(componentName, iBinder); super.onServiceConnected(componentName, iBinder);
getService().registerOnApiChangeListener(this); getService().registerOnServiceStateChangeListener(this);
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
getService().unregisterOnApiChangeListener(this); SyncthingService syncthingService = getService();
if (syncthingService != null) {
syncthingService.unregisterOnServiceStateChangeListener(this::onServiceStateChange);
}
} }
@Override @Override
@ -323,7 +326,7 @@ public class FolderPickerActivity extends SyncthingActivity
} }
@Override @Override
public void onApiChange(SyncthingService.State currentState) { public void onServiceStateChange(SyncthingService.State currentState) {
if (!isFinishing() && currentState != SyncthingService.State.ACTIVE) { if (!isFinishing() && currentState != SyncthingService.State.ACTIVE) {
setResult(Activity.RESULT_CANCELED); setResult(Activity.RESULT_CANCELED);
finish(); finish();

View file

@ -66,7 +66,7 @@ import static java.lang.Math.min;
* {@link DrawerFragment} in the navigation drawer. * {@link DrawerFragment} in the navigation drawer.
*/ */
public class MainActivity extends StateDialogActivity public class MainActivity extends StateDialogActivity
implements SyncthingService.OnApiChangeListener { implements SyncthingService.OnServiceStateChangeListener {
private static final String TAG = "MainActivity"; private static final String TAG = "MainActivity";
private static final String IS_SHOWING_RESTART_DIALOG = "RESTART_DIALOG_STATE"; private static final String IS_SHOWING_RESTART_DIALOG = "RESTART_DIALOG_STATE";
@ -102,7 +102,7 @@ public class MainActivity extends StateDialogActivity
* Handles various dialogs based on current state. * Handles various dialogs based on current state.
*/ */
@Override @Override
public void onApiChange(SyncthingService.State currentState) { public void onServiceStateChange(SyncthingService.State currentState) {
switch (currentState) { switch (currentState) {
case STARTING: case STARTING:
break; break;
@ -254,6 +254,10 @@ public class MainActivity extends StateDialogActivity
mDrawerLayout.addDrawerListener(mDrawerToggle); mDrawerLayout.addDrawerListener(mDrawerToggle);
setOptimalDrawerWidth(findViewById(R.id.drawer)); setOptimalDrawerWidth(findViewById(R.id.drawer));
// SyncthingService needs to be started from this activity as the user
// can directly launch this activity from the recent activity switcher.
startService(new Intent(this, SyncthingService.class));
onNewIntent(getIntent()); onNewIntent(getIntent());
} }
@ -271,19 +275,20 @@ public class MainActivity extends StateDialogActivity
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (getService() != null) { SyncthingService mSyncthingService = getService();
getService().unregisterOnApiChangeListener(this); if (mSyncthingService != null) {
getService().unregisterOnApiChangeListener(mFolderListFragment); mSyncthingService.unregisterOnServiceStateChangeListener(this);
getService().unregisterOnApiChangeListener(mDeviceListFragment); mSyncthingService.unregisterOnServiceStateChangeListener(mFolderListFragment);
mSyncthingService.unregisterOnServiceStateChangeListener(mDeviceListFragment);
} }
} }
@Override @Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) { public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
super.onServiceConnected(componentName, iBinder); super.onServiceConnected(componentName, iBinder);
getService().registerOnApiChangeListener(this); getService().registerOnServiceStateChangeListener(this);
getService().registerOnApiChangeListener(mFolderListFragment); getService().registerOnServiceStateChangeListener(mFolderListFragment);
getService().registerOnApiChangeListener(mDeviceListFragment); getService().registerOnServiceStateChangeListener(mDeviceListFragment);
} }
/** /**
@ -430,15 +435,19 @@ public class MainActivity extends StateDialogActivity
return super.onKeyDown(keyCode, e); return super.onKeyDown(keyCode, e);
} }
/**
* Close drawer on back button press.
*/
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
// Close drawer on back button press.
closeDrawer(); closeDrawer();
else } else {
super.onBackPressed(); /**
* Leave MainActivity in its state as the home button was pressed.
* This will avoid waiting for the loading spinner when getting back
* and give changes to do UI updates based on EventProcessor in the future.
*/
moveTaskToBack(true);
}
} }
/** /**

View file

@ -74,7 +74,7 @@ public class SettingsActivity extends SyncthingActivity {
public static class SettingsFragment extends PreferenceFragment public static class SettingsFragment extends PreferenceFragment
implements SyncthingActivity.OnServiceConnectedListener, implements SyncthingActivity.OnServiceConnectedListener,
SyncthingService.OnApiChangeListener, Preference.OnPreferenceChangeListener, SyncthingService.OnServiceStateChangeListener, Preference.OnPreferenceChangeListener,
Preference.OnPreferenceClickListener, SharedPreferences.OnSharedPreferenceChangeListener { Preference.OnPreferenceClickListener, SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "SettingsFragment"; private static final String TAG = "SettingsFragment";
@ -246,32 +246,26 @@ public class SettingsActivity extends SyncthingActivity {
@Override @Override
public void onServiceConnected() { public void onServiceConnected() {
Log.v(TAG, "onServiceConnected");
if (getActivity() == null) if (getActivity() == null)
return; return;
mSyncthingService = ((SyncthingActivity) getActivity()).getService(); mSyncthingService = ((SyncthingActivity) getActivity()).getService();
mSyncthingService.registerOnApiChangeListener(this); mSyncthingService.registerOnServiceStateChangeListener(this);
// Use callback to make sure getApi() doesn't return null.
mSyncthingService.registerOnWebGuiAvailableListener(() -> {
if (mSyncthingService.getApi().isConfigLoaded()) {
mGui = mSyncthingService.getApi().getGui();
mOptions = mSyncthingService.getApi().getOptions();
}
});
} }
@Override @Override
public void onApiChange(SyncthingService.State currentState) { public void onServiceStateChange(SyncthingService.State currentState) {
boolean syncthingActive = currentState == SyncthingService.State.ACTIVE; mApi = mSyncthingService.getApi();
boolean isSyncthingRunning = syncthingActive && mSyncthingService.getApi().isConfigLoaded(); boolean isSyncthingRunning = (mApi != null) &&
mApi.isConfigLoaded() &&
(currentState == SyncthingService.State.ACTIVE);
mCategorySyncthingOptions.setEnabled(isSyncthingRunning); mCategorySyncthingOptions.setEnabled(isSyncthingRunning);
mCategoryBackup.setEnabled(isSyncthingRunning); mCategoryBackup.setEnabled(isSyncthingRunning);
if (!isSyncthingRunning) if (!isSyncthingRunning)
return; return;
mApi = mSyncthingService.getApi();
mSyncthingVersion.setSummary(mApi.getVersion()); mSyncthingVersion.setSummary(mApi.getVersion());
mOptions = mApi.getOptions(); mOptions = mApi.getOptions();
mGui = mApi.getGui(); mGui = mApi.getGui();
@ -296,7 +290,7 @@ public class SettingsActivity extends SyncthingActivity {
public void onDestroy() { public void onDestroy() {
mPreferences.unregisterOnSharedPreferenceChangeListener(this); mPreferences.unregisterOnSharedPreferenceChangeListener(this);
if (mSyncthingService != null) { if (mSyncthingService != null) {
mSyncthingService.unregisterOnApiChangeListener(this); mSyncthingService.unregisterOnServiceStateChangeListener(this);
} }
super.onDestroy(); super.onDestroy();
} }
@ -382,9 +376,9 @@ public class SettingsActivity extends SyncthingActivity {
@Override @Override
public void onStop() { public void onStop() {
if (mRequireRestart) { if (mRequireRestart) {
if (mSyncthingService.getCurrentState() != SyncthingService.State.DISABLED && if (mSyncthingService != null && mApi != null &&
mSyncthingService.getApi() != null) { mSyncthingService.getCurrentState() != SyncthingService.State.DISABLED) {
mSyncthingService.getApi().restart(); mApi.restart();
mRequireRestart = false; mRequireRestart = false;
} }
} }
@ -405,7 +399,7 @@ public class SettingsActivity extends SyncthingActivity {
: R.string.always_run_in_background_disabled); : R.string.always_run_in_background_disabled);
mSyncOnlyCharging.setEnabled(value); mSyncOnlyCharging.setEnabled(value);
mSyncOnlyWifi.setEnabled(value); mSyncOnlyWifi.setEnabled(value);
mSyncOnlyOnSSIDs.setEnabled(mSyncOnlyWifi.isChecked()); mSyncOnlyOnSSIDs.setEnabled(false);
// Uncheck items when disabled, so it is clear they have no effect. // Uncheck items when disabled, so it is clear they have no effect.
if (!value) { if (!value) {
mSyncOnlyCharging.setChecked(false); mSyncOnlyCharging.setChecked(false);

View file

@ -45,7 +45,7 @@ import java.util.Map;
* ownCloud Android {@see https://github.com/owncloud/android/blob/79664304fdb762b2e04f1ac505f50d0923ddd212/src/com/owncloud/android/utils/UriUtils.java#L193} * ownCloud Android {@see https://github.com/owncloud/android/blob/79664304fdb762b2e04f1ac505f50d0923ddd212/src/com/owncloud/android/utils/UriUtils.java#L193}
*/ */
public class ShareActivity extends StateDialogActivity public class ShareActivity extends StateDialogActivity
implements SyncthingActivity.OnServiceConnectedListener, SyncthingService.OnApiChangeListener { implements SyncthingActivity.OnServiceConnectedListener, SyncthingService.OnServiceStateChangeListener {
private static final String TAG = "ShareActivity"; private static final String TAG = "ShareActivity";
private static final String PREF_PREVIOUSLY_SELECTED_SYNCTHING_FOLDER = "previously_selected_syncthing_folder"; private static final String PREF_PREVIOUSLY_SELECTED_SYNCTHING_FOLDER = "previously_selected_syncthing_folder";
@ -57,7 +57,7 @@ public class ShareActivity extends StateDialogActivity
private Spinner mFoldersSpinner; private Spinner mFoldersSpinner;
@Override @Override
public void onApiChange(SyncthingService.State currentState) { public void onServiceStateChange(SyncthingService.State currentState) {
if (currentState != SyncthingService.State.ACTIVE || getApi() == null) if (currentState != SyncthingService.State.ACTIVE || getApi() == null)
return; return;
@ -85,7 +85,7 @@ public class ShareActivity extends StateDialogActivity
@Override @Override
public void onServiceConnected() { public void onServiceConnected() {
getService().registerOnApiChangeListener(this); getService().registerOnServiceStateChangeListener(this);
} }
@Override @Override

View file

@ -12,6 +12,7 @@ import android.view.View;
import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.R;
import com.nutomic.syncthingandroid.databinding.DialogLoadingBinding; import com.nutomic.syncthingandroid.databinding.DialogLoadingBinding;
import com.nutomic.syncthingandroid.service.SyncthingService; import com.nutomic.syncthingandroid.service.SyncthingService;
import com.nutomic.syncthingandroid.service.SyncthingService.State;
import com.nutomic.syncthingandroid.util.Util; import com.nutomic.syncthingandroid.util.Util;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -23,6 +24,7 @@ public abstract class StateDialogActivity extends SyncthingActivity {
private static final long SLOW_LOADING_TIME = TimeUnit.SECONDS.toMillis(30); private static final long SLOW_LOADING_TIME = TimeUnit.SECONDS.toMillis(30);
private State mServiceState = State.INIT;
private AlertDialog mLoadingDialog; private AlertDialog mLoadingDialog;
private AlertDialog mDisabledDialog; private AlertDialog mDisabledDialog;
private boolean mIsPaused = true; private boolean mIsPaused = true;
@ -31,13 +33,20 @@ public abstract class StateDialogActivity extends SyncthingActivity {
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
registerOnServiceConnectedListener(() -> registerOnServiceConnectedListener(() ->
getService().registerOnApiChangeListener(this::onApiChange)); getService().registerOnServiceStateChangeListener(this::onServiceStateChange));
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
mIsPaused = false; mIsPaused = false;
switch (mServiceState) {
case DISABLED:
showDisabledDialog();
break;
default:
break;
}
} }
@Override @Override
@ -52,13 +61,14 @@ public abstract class StateDialogActivity extends SyncthingActivity {
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
if (getService() != null) { if (getService() != null) {
getService().unregisterOnApiChangeListener(this::onApiChange); getService().unregisterOnServiceStateChangeListener(this::onServiceStateChange);
} }
dismissDisabledDialog(); dismissDisabledDialog();
} }
private void onApiChange(SyncthingService.State currentState) { private void onServiceStateChange(SyncthingService.State currentState) {
switch (currentState) { mServiceState = currentState;
switch (mServiceState) {
case INIT: // fallthrough case INIT: // fallthrough
case STARTING: case STARTING:
dismissDisabledDialog(); dismissDisabledDialog();
@ -69,24 +79,25 @@ public abstract class StateDialogActivity extends SyncthingActivity {
dismissLoadingDialog(); dismissLoadingDialog();
break; break;
case DISABLED: case DISABLED:
dismissLoadingDialog(); if (!mIsPaused) {
if (!isFinishing()) {
showDisabledDialog(); showDisabledDialog();
} }
break; break;
case ERROR: // fallthrough
default:
break;
} }
} }
private void showDisabledDialog() { private void showDisabledDialog() {
if (mIsPaused) if (this.isFinishing() && (mDisabledDialog != null)) {
return; return;
}
mDisabledDialog = new AlertDialog.Builder(this) mDisabledDialog = new AlertDialog.Builder(this)
.setTitle(R.string.syncthing_disabled_title) .setTitle(R.string.syncthing_disabled_title)
.setMessage(R.string.syncthing_disabled_message) .setMessage(R.string.syncthing_disabled_message)
.setPositiveButton(R.string.syncthing_disabled_change_settings, .setPositiveButton(R.string.syncthing_disabled_change_settings,
(dialogInterface, i) -> { (dialogInterface, i) -> {
finish();
startActivity(new Intent(this, SettingsActivity.class)); startActivity(new Intent(this, SettingsActivity.class));
} }
) )
@ -123,7 +134,7 @@ public abstract class StateDialogActivity extends SyncthingActivity {
if (!isGeneratingKeys) { if (!isGeneratingKeys) {
new Handler().postDelayed(() -> { new Handler().postDelayed(() -> {
if (isFinishing() || mLoadingDialog == null) if (this.isFinishing() || mLoadingDialog == null)
return; return;
binding.loadingSlowMessage.setVisibility(View.VISIBLE); binding.loadingSlowMessage.setVisibility(View.VISIBLE);

View file

@ -47,7 +47,7 @@ import java.util.Properties;
* Holds a WebView that shows the web ui of the local syncthing instance. * Holds a WebView that shows the web ui of the local syncthing instance.
*/ */
public class WebGuiActivity extends StateDialogActivity public class WebGuiActivity extends StateDialogActivity
implements SyncthingService.OnWebGuiAvailableListener { implements SyncthingService.OnServiceStateChangeListener {
private static final String TAG = "WebGuiActivity"; private static final String TAG = "WebGuiActivity";
@ -140,20 +140,31 @@ public class WebGuiActivity extends StateDialogActivity
mWebView.getSettings().setDomStorageEnabled(true); mWebView.getSettings().setDomStorageEnabled(true);
mWebView.setWebViewClient(mWebViewClient); mWebView.setWebViewClient(mWebViewClient);
mWebView.clearCache(true); mWebView.clearCache(true);
// SyncthingService needs to be started from this activity as the user
// can directly launch this activity from the recent activity switcher.
startService(new Intent(this, SyncthingService.class));
} }
@Override @Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) { public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
super.onServiceConnected(componentName, iBinder); super.onServiceConnected(componentName, iBinder);
getService().registerOnWebGuiAvailableListener(WebGuiActivity.this); getService().registerOnServiceStateChangeListener(this);
} }
@Override @Override
public void onWebGuiAvailable() { public void onServiceStateChange(SyncthingService.State newState) {
if (mWebView.getUrl() == null) { Log.v(TAG, "onServiceStateChange(" + newState + ")");
mWebView.stopLoading(); if (newState == SyncthingService.State.ACTIVE) {
setWebViewProxy(mWebView.getContext().getApplicationContext(), "", 0, "localhost|0.0.0.0|127.*|[::1]"); if (mWebView == null) {
mWebView.loadUrl(getService().getWebGuiUrl().toString()); Log.v(TAG, "onWebGuiAvailable: Skipped event due to mWebView == null");
return;
}
if (mWebView.getUrl() == null) {
mWebView.stopLoading();
setWebViewProxy(mWebView.getContext().getApplicationContext(), "", 0, "localhost|0.0.0.0|127.*|[::1]");
mWebView.loadUrl(getService().getWebGuiUrl().toString());
}
} }
} }
@ -183,6 +194,10 @@ public class WebGuiActivity extends StateDialogActivity
@Override @Override
protected void onDestroy() { protected void onDestroy() {
SyncthingService mSyncthingService = getService();
if (mSyncthingService != null) {
mSyncthingService.unregisterOnServiceStateChangeListener(this);
}
mWebView.destroy(); mWebView.destroy();
mWebView = null; mWebView = null;
super.onDestroy(); super.onDestroy();

View file

@ -27,7 +27,7 @@ import java.util.TimerTask;
/** /**
* Displays a list of all existing devices. * Displays a list of all existing devices.
*/ */
public class DeviceListFragment extends ListFragment implements SyncthingService.OnApiChangeListener, public class DeviceListFragment extends ListFragment implements SyncthingService.OnServiceStateChangeListener,
ListView.OnItemClickListener { ListView.OnItemClickListener {
private final static Comparator<Device> DEVICES_COMPARATOR = (lhs, rhs) -> lhs.name.compareTo(rhs.name); private final static Comparator<Device> DEVICES_COMPARATOR = (lhs, rhs) -> lhs.name.compareTo(rhs.name);
@ -45,7 +45,7 @@ public class DeviceListFragment extends ListFragment implements SyncthingService
} }
@Override @Override
public void onApiChange(SyncthingService.State currentState) { public void onServiceStateChange(SyncthingService.State currentState) {
if (currentState != SyncthingService.State.ACTIVE) if (currentState != SyncthingService.State.ACTIVE)
return; return;

View file

@ -136,11 +136,20 @@ public class DrawerFragment extends Fragment implements View.OnClickListener {
* Invokes status callbacks. * Invokes status callbacks.
*/ */
private void updateGui() { private void updateGui() {
if (mActivity.getApi() == null || getActivity() == null || getActivity().isFinishing()) MainActivity mainActivity = (MainActivity) getActivity();
if (mainActivity == null) {
return; return;
mActivity.getApi().getSystemInfo(this::onReceiveSystemInfo); }
mActivity.getApi().getSystemVersion(this::onReceiveSystemVersion); if (mainActivity.isFinishing()) {
mActivity.getApi().getConnections(this::onReceiveConnections); return;
}
RestApi mApi = mainActivity.getApi();
if (mApi != null) {
mApi.getSystemInfo(this::onReceiveSystemInfo);
mApi.getSystemVersion(this::onReceiveSystemVersion);
mApi.getConnections(this::onReceiveConnections);
}
} }
/** /**

View file

@ -24,7 +24,7 @@ import java.util.TimerTask;
/** /**
* Displays a list of all existing folders. * Displays a list of all existing folders.
*/ */
public class FolderListFragment extends ListFragment implements SyncthingService.OnApiChangeListener, public class FolderListFragment extends ListFragment implements SyncthingService.OnServiceStateChangeListener,
AdapterView.OnItemClickListener { AdapterView.OnItemClickListener {
private FoldersAdapter mAdapter; private FoldersAdapter mAdapter;
@ -40,7 +40,7 @@ public class FolderListFragment extends ListFragment implements SyncthingService
} }
@Override @Override
public void onApiChange(SyncthingService.State currentState) { public void onServiceStateChange(SyncthingService.State currentState) {
if (currentState != SyncthingService.State.ACTIVE) if (currentState != SyncthingService.State.ACTIVE)
return; return;

View file

@ -94,13 +94,15 @@ public abstract class ApiRequest {
@Nullable OnSuccessListener listener, @Nullable OnErrorListener errorListener) { @Nullable OnSuccessListener listener, @Nullable OnErrorListener errorListener) {
Log.v(TAG, "Performing request to " + uri.toString()); Log.v(TAG, "Performing request to " + uri.toString());
StringRequest request = new StringRequest(requestMethod, uri.toString(), reply -> { StringRequest request = new StringRequest(requestMethod, uri.toString(), reply -> {
if (listener != null) if (listener != null) {
listener.onSuccess(reply); listener.onSuccess(reply);
}
}, error -> { }, error -> {
if (errorListener != null) if (errorListener != null) {
errorListener.onError(error); errorListener.onError(error);
else } else {
Log.w(TAG, "Request to " + uri + " failed", error); Log.w(TAG, "Request to " + uri + " failed, " + error.getMessage());
}
}) { }) {
@Override @Override
public Map<String, String> getHeaders() throws AuthFailureError { public Map<String, String> getHeaders() throws AuthFailureError {

View file

@ -25,9 +25,15 @@ public class PollWebGuiAvailableTask extends ApiRequest {
*/ */
private static final long WEB_GUI_POLL_INTERVAL = 100; private static final long WEB_GUI_POLL_INTERVAL = 100;
private final OnSuccessListener mListener;
private final Handler mHandler = new Handler(); private final Handler mHandler = new Handler();
private OnSuccessListener mListener;
/**
* Object that must be locked upon accessing mListener
*/
private final Object mListenerLock = new Object();
public PollWebGuiAvailableTask(Context context, URL url, String apiKey, public PollWebGuiAvailableTask(Context context, URL url, String apiKey,
OnSuccessListener listener) { OnSuccessListener listener) {
super(context, url, "", apiKey); super(context, url, "", apiKey);
@ -36,14 +42,36 @@ public class PollWebGuiAvailableTask extends ApiRequest {
performRequest(); performRequest();
} }
public void cancelRequestsAndCallback() {
synchronized(mListenerLock) {
mListener = null;
}
}
private void performRequest() { private void performRequest() {
Uri uri = buildUri(Collections.emptyMap()); Uri uri = buildUri(Collections.emptyMap());
connect(Request.Method.GET, uri, null, mListener, this::onError); connect(Request.Method.GET, uri, null, this::onSuccess, this::onError);
}
private void onSuccess(String result) {
synchronized(mListenerLock) {
if (mListener != null) {
mListener.onSuccess(result);
} else {
Log.v(TAG, "Cancelled callback and outstanding requests");
}
}
} }
private void onError(VolleyError error) { private void onError(VolleyError error) {
mHandler.postDelayed(this::performRequest, WEB_GUI_POLL_INTERVAL); synchronized(mListenerLock) {
if (mListener == null) {
Log.v(TAG, "Cancelled callback and outstanding requests");
return;
}
}
mHandler.postDelayed(this::performRequest, WEB_GUI_POLL_INTERVAL);
Throwable cause = error.getCause(); Throwable cause = error.getCause();
if (cause == null || cause.getClass().equals(ConnectException.class)) { if (cause == null || cause.getClass().equals(ConnectException.class)) {
Log.v(TAG, "Polling web gui"); Log.v(TAG, "Polling web gui");

View file

@ -8,6 +8,7 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.os.Build; import android.os.Build;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import com.nutomic.syncthingandroid.service.DeviceStateHolder; import com.nutomic.syncthingandroid.service.DeviceStateHolder;
import com.nutomic.syncthingandroid.service.SyncthingService; import com.nutomic.syncthingandroid.service.SyncthingService;
@ -17,6 +18,8 @@ import com.nutomic.syncthingandroid.service.SyncthingService;
*/ */
public class NetworkReceiver extends BroadcastReceiver { public class NetworkReceiver extends BroadcastReceiver {
private static final String TAG = "NetworkReceiver";
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (!ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) if (!ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction()))
@ -33,14 +36,21 @@ public class NetworkReceiver extends BroadcastReceiver {
ConnectivityManager cm = (ConnectivityManager) ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE); context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo ni = cm.getActiveNetworkInfo(); NetworkInfo ni = cm.getActiveNetworkInfo();
boolean isOffline = ni == null; boolean isAllowedConnectionType = false;
boolean isWifi = ni != null && ni.getType() == ConnectivityManager.TYPE_WIFI && ni.isConnected(); if (ni == null) {
boolean isNetworkMetered = (Build.VERSION.SDK_INT >= 16) ? cm.isActiveNetworkMetered() : false; Log.v(TAG, "In flight mode");
boolean isAllowedConnection = isOffline || (isWifi && !isNetworkMetered); // We still allow opening MainActivity and WebGuiActivity for local administration.
isAllowedConnectionType = true;
} else {
Log.v(TAG, "Not in flight mode");
boolean isWifi = ni.getType() == ConnectivityManager.TYPE_WIFI && ni.isConnected();
boolean isNetworkMetered = (Build.VERSION.SDK_INT >= 16) ? cm.isActiveNetworkMetered() : false;
isAllowedConnectionType = isWifi && !isNetworkMetered;
}
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context); LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
Intent intent = new Intent(DeviceStateHolder.ACTION_DEVICE_STATE_CHANGED); Intent intent = new Intent(DeviceStateHolder.ACTION_DEVICE_STATE_CHANGED);
intent.putExtra(DeviceStateHolder.EXTRA_IS_ALLOWED_NETWORK_CONNECTION, isAllowedConnection); intent.putExtra(DeviceStateHolder.EXTRA_IS_ALLOWED_NETWORK_CONNECTION, isAllowedConnectionType);
lbm.sendBroadcast(intent); lbm.sendBroadcast(intent);
} }

View file

@ -20,6 +20,7 @@ import com.nutomic.syncthingandroid.SyncthingApp;
import com.nutomic.syncthingandroid.receiver.BatteryReceiver; import com.nutomic.syncthingandroid.receiver.BatteryReceiver;
import com.nutomic.syncthingandroid.receiver.NetworkReceiver; import com.nutomic.syncthingandroid.receiver.NetworkReceiver;
import com.nutomic.syncthingandroid.receiver.PowerSaveModeChangedReceiver; import com.nutomic.syncthingandroid.receiver.PowerSaveModeChangedReceiver;
import com.nutomic.syncthingandroid.service.ReceiverManager;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -70,19 +71,26 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh
private final Context mContext; private final Context mContext;
private final LocalBroadcastManager mBroadcastManager; private final LocalBroadcastManager mBroadcastManager;
private final DeviceStateChangedReceiver mReceiver = new DeviceStateChangedReceiver();
private final OnDeviceStateChangedListener mListener;
@Inject SharedPreferences mPreferences; @Inject SharedPreferences mPreferences;
private @Nullable NetworkReceiver mNetworkReceiver = null; private @Nullable DeviceStateChangedReceiver mDeviceStateChangedReceiver = null;
private @Nullable BatteryReceiver mBatteryReceiver = null; private ReceiverManager mReceiverManager;
private @Nullable BroadcastReceiver mPowerSaveModeChangedReceiver = null;
private boolean mIsAllowedNetworkConnection; // Those receivers are managed by {@link mReceiverManager}.
private NetworkReceiver mNetworkReceiver;
private BatteryReceiver mBatteryReceiver;
private PowerSaveModeChangedReceiver mPowerSaveModeChangedReceiver;
private boolean mIsAllowedConnectionType;
private String mWifiSsid; private String mWifiSsid;
private boolean mIsCharging; private boolean mIsCharging;
private boolean mIsPowerSaving; private boolean mIsPowerSaving;
/**
* Sending callback notifications through {@link OnDeviceStateChangedListener} is enabled if not null.
*/
private @Nullable OnDeviceStateChangedListener mOnDeviceStateChangedListener = null;
/** /**
* Stores the result of the last call to {@link decideShouldRun}. * Stores the result of the last call to {@link decideShouldRun}.
*/ */
@ -92,21 +100,22 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh
Log.v(TAG, "Created new instance"); Log.v(TAG, "Created new instance");
((SyncthingApp) context.getApplicationContext()).component().inject(this); ((SyncthingApp) context.getApplicationContext()).component().inject(this);
mContext = context; mContext = context;
mBroadcastManager = LocalBroadcastManager.getInstance(mContext);
mBroadcastManager.registerReceiver(mReceiver, new IntentFilter(ACTION_DEVICE_STATE_CHANGED));
mPreferences.registerOnSharedPreferenceChangeListener(this); mPreferences.registerOnSharedPreferenceChangeListener(this);
mListener = listener; mOnDeviceStateChangedListener = listener;
updateReceivers();
mDeviceStateChangedReceiver = new DeviceStateChangedReceiver();
mBroadcastManager = LocalBroadcastManager.getInstance(mContext);
mBroadcastManager.registerReceiver(mDeviceStateChangedReceiver, new IntentFilter(ACTION_DEVICE_STATE_CHANGED));
registerChildReceivers();
} }
public void shutdown() { public void shutdown() {
Log.v(TAG, "Shutting down"); Log.v(TAG, "Shutting down");
mBroadcastManager.unregisterReceiver(mReceiver);
mPreferences.unregisterOnSharedPreferenceChangeListener(this); mPreferences.unregisterOnSharedPreferenceChangeListener(this);
mReceiverManager.unregisterAllReceivers(mContext);
unregisterReceiver(mNetworkReceiver); if (mDeviceStateChangedReceiver != null) {
unregisterReceiver(mBatteryReceiver); mBroadcastManager.unregisterReceiver(mDeviceStateChangedReceiver);
unregisterReceiver(mPowerSaveModeChangedReceiver); }
} }
@Override @Override
@ -115,64 +124,57 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh
Constants.PREF_SYNC_ONLY_WIFI, Constants.PREF_RESPECT_BATTERY_SAVING, Constants.PREF_SYNC_ONLY_WIFI, Constants.PREF_RESPECT_BATTERY_SAVING,
Constants.PREF_SYNC_ONLY_WIFI_SSIDS); Constants.PREF_SYNC_ONLY_WIFI_SSIDS);
if (watched.contains(key)) { if (watched.contains(key)) {
updateReceivers(); mReceiverManager.unregisterAllReceivers(mContext);
registerChildReceivers();
} }
} }
private void updateReceivers() { private void registerChildReceivers() {
boolean incomingBroadcastEventsExpected = false;
if (mPreferences.getBoolean(Constants.PREF_SYNC_ONLY_WIFI, false)) { if (mPreferences.getBoolean(Constants.PREF_SYNC_ONLY_WIFI, false)) {
Log.i(TAG, "Listening for network state changes"); Log.i(TAG, "Creating NetworkReceiver");
NetworkReceiver.updateNetworkStatus(mContext); NetworkReceiver.updateNetworkStatus(mContext);
mNetworkReceiver = new NetworkReceiver(); mNetworkReceiver = new NetworkReceiver();
mContext.registerReceiver(mNetworkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); ReceiverManager.registerReceiver(mContext, mNetworkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
} else { incomingBroadcastEventsExpected = true;
Log.i(TAG, "Stopped listening to network state changes");
unregisterReceiver(mNetworkReceiver);
mNetworkReceiver = null;
} }
if (mPreferences.getBoolean(Constants.PREF_SYNC_ONLY_CHARGING, false)) { if (mPreferences.getBoolean(Constants.PREF_SYNC_ONLY_CHARGING, false)) {
Log.i(TAG, "Listening to battery state changes"); Log.i(TAG, "Creating BatteryReceiver");
BatteryReceiver.updateInitialChargingStatus(mContext); BatteryReceiver.updateInitialChargingStatus(mContext);
mBatteryReceiver = new BatteryReceiver(); mBatteryReceiver = new BatteryReceiver();
IntentFilter filter = new IntentFilter(); IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_POWER_CONNECTED); filter.addAction(Intent.ACTION_POWER_CONNECTED);
filter.addAction(Intent.ACTION_POWER_DISCONNECTED); filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
mContext.registerReceiver(mBatteryReceiver, filter); ReceiverManager.registerReceiver(mContext, mBatteryReceiver, filter);
} else { incomingBroadcastEventsExpected = true;
Log.i(TAG, "Stopped listening to battery state changes");
unregisterReceiver(mBatteryReceiver);
mBatteryReceiver = null;
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
mPreferences.getBoolean("respect_battery_saving", true)) { mPreferences.getBoolean(Constants.PREF_RESPECT_BATTERY_SAVING, true)) {
Log.i(TAG, "Listening to power saving changes"); Log.i(TAG, "Creating PowerSaveModeChangedReceiver");
PowerSaveModeChangedReceiver.updatePowerSavingState(mContext); PowerSaveModeChangedReceiver.updatePowerSavingState(mContext);
mPowerSaveModeChangedReceiver = new PowerSaveModeChangedReceiver(); mPowerSaveModeChangedReceiver = new PowerSaveModeChangedReceiver();
mContext.registerReceiver(mPowerSaveModeChangedReceiver, ReceiverManager.registerReceiver(mContext, mPowerSaveModeChangedReceiver,
new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)); new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED));
} else { incomingBroadcastEventsExpected = true;
Log.i(TAG, "Stopped listening to power saving changes");
unregisterReceiver(mPowerSaveModeChangedReceiver);
mPowerSaveModeChangedReceiver = null;
} }
}
private void unregisterReceiver(BroadcastReceiver receiver) { // If no broadcast messages can be received as we didn't register an emitter,
if (receiver != null) // force an initial decision to be made.
mContext.unregisterReceiver(receiver); if (!incomingBroadcastEventsExpected) {
updateShouldRunDecision();
}
} }
private class DeviceStateChangedReceiver extends BroadcastReceiver { private class DeviceStateChangedReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
mIsAllowedNetworkConnection = mIsAllowedConnectionType =
intent.getBooleanExtra(EXTRA_IS_ALLOWED_NETWORK_CONNECTION, mIsAllowedNetworkConnection); intent.getBooleanExtra(EXTRA_IS_ALLOWED_NETWORK_CONNECTION, mIsAllowedConnectionType);
mIsCharging = intent.getBooleanExtra(EXTRA_IS_CHARGING, mIsCharging); mIsCharging = intent.getBooleanExtra(EXTRA_IS_CHARGING, mIsCharging);
mIsPowerSaving = intent.getBooleanExtra(EXTRA_IS_POWER_SAVING, mIsPowerSaving); mIsPowerSaving = intent.getBooleanExtra(EXTRA_IS_POWER_SAVING, mIsPowerSaving);
Log.i(TAG, "State updated, allowed network connection: " + mIsAllowedNetworkConnection +
", charging: " + mIsCharging + ", power saving: " + mIsPowerSaving);
updateShouldRunDecision(); updateShouldRunDecision();
} }
} }
@ -193,7 +195,9 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh
// compared to the last determined result. // compared to the last determined result.
boolean newShouldRun = decideShouldRun(); boolean newShouldRun = decideShouldRun();
if (newShouldRun != lastDeterminedShouldRun) { if (newShouldRun != lastDeterminedShouldRun) {
mListener.onDeviceStateChanged(newShouldRun); if (mOnDeviceStateChangedListener != null) {
mOnDeviceStateChangedListener.onDeviceStateChanged(newShouldRun);
}
lastDeterminedShouldRun = newShouldRun; lastDeterminedShouldRun = newShouldRun;
} }
} }
@ -202,7 +206,10 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh
* Determines if Syncthing should currently run. * Determines if Syncthing should currently run.
*/ */
private boolean decideShouldRun() { private boolean decideShouldRun() {
boolean prefRespectPowerSaving = mPreferences.getBoolean("respect_battery_saving", true); Log.v(TAG, "State updated: IsAllowedConnectionType: " + mIsAllowedConnectionType +
", IsCharging: " + mIsCharging + ", IsPowerSaving: " + mIsPowerSaving);
boolean prefRespectPowerSaving = mPreferences.getBoolean(Constants.PREF_RESPECT_BATTERY_SAVING, true);
if (prefRespectPowerSaving && mIsPowerSaving) if (prefRespectPowerSaving && mIsPowerSaving)
return false; return false;
@ -211,7 +218,7 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh
boolean prefStopNotCharging = mPreferences.getBoolean(Constants.PREF_SYNC_ONLY_CHARGING, false); boolean prefStopNotCharging = mPreferences.getBoolean(Constants.PREF_SYNC_ONLY_CHARGING, false);
updateWifiSsid(); updateWifiSsid();
if (prefStopMobileData && !isWhitelistedNetworkConnection()) if (prefStopMobileData && !isWhitelistedWifiConnection())
return false; return false;
if (prefStopNotCharging && !mIsCharging) if (prefStopNotCharging && !mIsCharging)
@ -221,8 +228,8 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh
return true; return true;
} }
private boolean isWhitelistedNetworkConnection() { private boolean isWhitelistedWifiConnection() {
boolean wifiConnected = mIsAllowedNetworkConnection; boolean wifiConnected = mIsAllowedConnectionType;
if (wifiConnected) { if (wifiConnected) {
Set<String> ssids = mPreferences.getStringSet(Constants.PREF_SYNC_ONLY_WIFI_SSIDS, new HashSet<>()); Set<String> ssids = mPreferences.getStringSet(Constants.PREF_SYNC_ONLY_WIFI_SSIDS, new HashSet<>());
if (ssids.isEmpty()) { if (ssids.isEmpty()) {

View file

@ -33,8 +33,7 @@ import javax.inject.Inject;
* *
* It uses {@link RestApi#getEvents} to read the pending events and wait for new events. * It uses {@link RestApi#getEvents} to read the pending events and wait for new events.
*/ */
public class EventProcessor implements SyncthingService.OnWebGuiAvailableListener, Runnable, public class EventProcessor implements Runnable, RestApi.OnReceiveEventListener {
RestApi.OnReceiveEventListener {
private static final String TAG = "EventProcessor"; private static final String TAG = "EventProcessor";
private static final String PREF_LAST_SYNC_ID = "last_sync_id"; private static final String PREF_LAST_SYNC_ID = "last_sync_id";
@ -67,7 +66,7 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene
@Override @Override
public void run() { public void run() {
// Restore the last event id if the event processor may have been restartet. // Restore the last event id if the event processor may have been restarted.
if (mLastEventId == 0) { if (mLastEventId == 0) {
mLastEventId = mPreferences.getLong(PREF_LAST_SYNC_ID, 0); mLastEventId = mPreferences.getLong(PREF_LAST_SYNC_ID, 0);
} }
@ -223,9 +222,8 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene
} }
} }
@Override public void start() {
public void onWebGuiAvailable() { Log.d(TAG, "Starting event processor.");
Log.d(TAG, "WebGUI available. Starting event processor.");
// Remove all pending callbacks and add a new one. This makes sure that only one // Remove all pending callbacks and add a new one. This makes sure that only one
// event poller is running at any given time. // event poller is running at any given time.
@ -236,8 +234,8 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene
} }
} }
public void shutdown() { public void stop() {
Log.d(TAG, "Shutdown event processor."); Log.d(TAG, "Stopping event processor.");
synchronized (mMainThreadHandler) { synchronized (mMainThreadHandler) {
mShutdown = true; mShutdown = true;
mMainThreadHandler.removeCallbacks(this); mMainThreadHandler.removeCallbacks(this);

View file

@ -16,6 +16,7 @@ import com.nutomic.syncthingandroid.SyncthingApp;
import com.nutomic.syncthingandroid.activities.FirstStartActivity; import com.nutomic.syncthingandroid.activities.FirstStartActivity;
import com.nutomic.syncthingandroid.activities.LogActivity; import com.nutomic.syncthingandroid.activities.LogActivity;
import com.nutomic.syncthingandroid.activities.MainActivity; import com.nutomic.syncthingandroid.activities.MainActivity;
import com.nutomic.syncthingandroid.service.SyncthingService.State;
import javax.inject.Inject; import javax.inject.Inject;
@ -101,14 +102,32 @@ public class NotificationHandler {
type = "low_priority"; type = "low_priority";
} }
boolean syncthingRunning = service.getCurrentState() == SyncthingService.State.ACTIVE || State currentServiceState = service.getCurrentState();
service.getCurrentState() == SyncthingService.State.STARTING; boolean syncthingRunning = currentServiceState == SyncthingService.State.ACTIVE ||
currentServiceState == SyncthingService.State.STARTING;
if (foreground || (syncthingRunning && !type.equals("none"))) { if (foreground || (syncthingRunning && !type.equals("none"))) {
// Launch FirstStartActivity instead of MainActivity so we can request permission if int title = R.string.syncthing_terminated;
// necessary. switch (currentServiceState) {
PendingIntent pi = PendingIntent.getActivity(mContext, 0, case ERROR:
new Intent(mContext, FirstStartActivity.class), 0); case INIT:
int title = syncthingRunning ? R.string.syncthing_active : R.string.syncthing_disabled; break;
case DISABLED:
title = R.string.syncthing_disabled;
break;
case STARTING:
case ACTIVE:
title = R.string.syncthing_active;
break;
default:
break;
}
/**
* We no longer need to launch FirstStartActivity instead of MainActivity as
* {@link SyncthingService#onStartCommand} will check for denied permissions.
*/
Intent intent = new Intent(mContext, MainActivity.class);
// Reason for two separate IDs: if one of the notification channels is hidden then // Reason for two separate IDs: if one of the notification channels is hidden then
// the startForeground() below won't update the notification but use the old one // the startForeground() below won't update the notification but use the old one
int idToShow = syncthingRunning ? ID_PERSISTENT : ID_PERSISTENT_WAITING; int idToShow = syncthingRunning ? ID_PERSISTENT : ID_PERSISTENT_WAITING;
@ -119,7 +138,7 @@ public class NotificationHandler {
.setSmallIcon(R.drawable.ic_stat_notify) .setSmallIcon(R.drawable.ic_stat_notify)
.setOngoing(true) .setOngoing(true)
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setContentIntent(pi); .setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0));
if (type.equals("low_priority")) if (type.equals("low_priority"))
builder.setPriority(NotificationCompat.PRIORITY_MIN); builder.setPriority(NotificationCompat.PRIORITY_MIN);

View file

@ -0,0 +1,50 @@
package com.nutomic.syncthingandroid.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.util.Log;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ReceiverManager {
private static final String TAG = "ReceiverManager";
private static List<BroadcastReceiver> mReceivers = new ArrayList<BroadcastReceiver>();
public static synchronized void registerReceiver(Context context, BroadcastReceiver receiver, IntentFilter intentFilter) {
mReceivers.add(receiver);
context.registerReceiver(receiver, intentFilter);
Log.v(TAG, "Registered receiver: " + receiver + " with filter: " + intentFilter);
}
public static synchronized boolean isReceiverRegistered(BroadcastReceiver receiver) {
return mReceivers.contains(receiver);
}
public static synchronized void unregisterAllReceivers(Context context) {
if (context == null) {
Log.e(TAG, "unregisterReceiver: context is null");
return;
}
Iterator<BroadcastReceiver> iter = mReceivers.iterator();
while (iter.hasNext()) {
BroadcastReceiver receiver = iter.next();
if (isReceiverRegistered(receiver)) {
try {
context.unregisterReceiver(receiver);
Log.v(TAG, "Unregistered receiver: " + receiver);
} catch(IllegalArgumentException e) {
// We have to catch the race condition a registration is still pending in android
// according to https://stackoverflow.com/a/3568906
Log.w(TAG, "unregisterReceiver(" + receiver + ") threw IllegalArgumentException");
}
iter.remove();
}
}
}
}

View file

@ -52,7 +52,7 @@ import javax.inject.Inject;
/** /**
* Provides functions to interact with the syncthing REST API. * Provides functions to interact with the syncthing REST API.
*/ */
public class RestApi implements SyncthingService.OnWebGuiAvailableListener { public class RestApi {
private static final String TAG = "RestApi"; private static final String TAG = "RestApi";
@ -97,6 +97,24 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
*/ */
private long mPreviousConnectionTime = 0; private long mPreviousConnectionTime = 0;
/**
* In the last-finishing {@link readConfigFromRestApi} callback, we have to call
* {@link SyncthingService#onApiAvailable} to indicate that the RestApi class is fully initialized.
* We do this to avoid getting stuck with our main thread due to synchronous REST queries.
* The correct indication of full initialisation is crucial to stability as other listeners of
* {@link SettingsActivity#onServiceStateChange} needs cached config and system information available.
* e.g. SettingsFragment need "mLocalDeviceId"
*/
private Boolean asyncQueryConfigComplete = false;
private Boolean asyncQueryVersionComplete = false;
private Boolean asyncQuerySystemInfoComplete = false;
/**
* Object that must be locked upon accessing the following variables:
* asyncQueryConfigComplete, asyncQueryVersionComplete, asyncQuerySystemInfoComplete
*/
private final Object mAsyncQueryCompleteLock = new Object();
/** /**
* Stores the latest result of {@link #getFolderStatus} for each folder * Stores the latest result of {@link #getFolderStatus} for each folder
*/ */
@ -119,16 +137,6 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
mOnConfigChangedListener = configListener; mOnConfigChangedListener = configListener;
} }
/**
* Number of previous calls to {@link #tryIsAvailable()}.
*/
private final AtomicInteger mAvailableCount = new AtomicInteger(0);
/**
* Number of asynchronous calls performed in {@link #onWebGuiAvailable()}.
*/
private static final int TOTAL_STARTUP_CALLS = 3;
public interface OnApiAvailableListener { public interface OnApiAvailableListener {
void onApiAvailable(); void onApiAvailable();
} }
@ -140,26 +148,46 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
/** /**
* Gets local device ID, syncthing version and config, then calls all OnApiAvailableListeners. * Gets local device ID, syncthing version and config, then calls all OnApiAvailableListeners.
*/ */
@Override public void readConfigFromRestApi() {
public void onWebGuiAvailable() { Log.v(TAG, "Reading config from REST ...");
mAvailableCount.set(0); synchronized (mAsyncQueryCompleteLock) {
asyncQueryVersionComplete = false;
asyncQueryConfigComplete = false;
asyncQuerySystemInfoComplete = false;
}
new GetRequest(mContext, mUrl, GetRequest.URI_VERSION, mApiKey, null, result -> { new GetRequest(mContext, mUrl, GetRequest.URI_VERSION, mApiKey, null, result -> {
JsonObject json = new JsonParser().parse(result).getAsJsonObject(); JsonObject json = new JsonParser().parse(result).getAsJsonObject();
mVersion = json.get("version").getAsString(); mVersion = json.get("version").getAsString();
Log.i(TAG, "Syncthing version is " + mVersion); Log.i(TAG, "Syncthing version is " + mVersion);
tryIsAvailable();
updateDebugFacilitiesCache(); updateDebugFacilitiesCache();
synchronized (mAsyncQueryCompleteLock) {
asyncQueryVersionComplete = true;
checkReadConfigFromRestApiCompleted();
}
}); });
new GetRequest(mContext, mUrl, GetRequest.URI_CONFIG, mApiKey, null, result -> { new GetRequest(mContext, mUrl, GetRequest.URI_CONFIG, mApiKey, null, result -> {
onReloadConfigComplete(result); onReloadConfigComplete(result);
tryIsAvailable(); synchronized (mAsyncQueryCompleteLock) {
asyncQueryConfigComplete = true;
checkReadConfigFromRestApiCompleted();
}
}); });
getSystemInfo(info -> { getSystemInfo(info -> {
mLocalDeviceId = info.myID; mLocalDeviceId = info.myID;
tryIsAvailable(); synchronized (mAsyncQueryCompleteLock) {
asyncQuerySystemInfoComplete = true;
checkReadConfigFromRestApiCompleted();
}
}); });
} }
private void checkReadConfigFromRestApiCompleted() {
if (asyncQueryVersionComplete && asyncQueryConfigComplete && asyncQuerySystemInfoComplete) {
Log.v(TAG, "Reading config from REST completed.");
mOnApiAvailableListener.onApiAvailable();
}
}
public void reloadConfig() { public void reloadConfig() {
new GetRequest(mContext, mUrl, GetRequest.URI_CONFIG, mApiKey, null, this::onReloadConfigComplete); new GetRequest(mContext, mUrl, GetRequest.URI_CONFIG, mApiKey, null, this::onReloadConfigComplete);
} }
@ -209,20 +237,6 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
} }
} }
/**
* Increments mAvailableCount by one, and, if it reached TOTAL_STARTUP_CALLS,
* calls {@link SyncthingService#onApiChange}.
*/
private void tryIsAvailable() {
int value = mAvailableCount.incrementAndGet();
if (BuildConfig.DEBUG && value > TOTAL_STARTUP_CALLS) {
throw new AssertionError("Too many startup calls");
}
if (value == TOTAL_STARTUP_CALLS) {
mOnApiAvailableListener.onApiAvailable();
}
}
/** /**
* Sends current config to Syncthing. * Sends current config to Syncthing.
* Will result in a "ConfigSaved" event. * Will result in a "ConfigSaved" event.
@ -311,12 +325,17 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener {
} }
public Device getLocalDevice() { public Device getLocalDevice() {
for (Device d : getDevices(true)) { List<Device> devices = getDevices(true);
if (devices.isEmpty()) {
throw new RuntimeException("RestApi.getLocalDevice: devices is empty.");
}
Log.v(TAG, "getLocalDevice: Looking for local device ID " + mLocalDeviceId);
for (Device d : devices) {
if (d.deviceID.equals(mLocalDeviceId)) { if (d.deviceID.equals(mLocalDeviceId)) {
return deepCopy(d, Device.class); return deepCopy(d, Device.class);
} }
} }
throw new RuntimeException(); throw new RuntimeException("RestApi.getLocalDevice: Failed to get the local device crucial to continuing execution.");
} }
public void addDevice(Device device, OnResultListener1<String> errorListener) { public void addDevice(Device device, OnResultListener1<String> errorListener) {

View file

@ -263,48 +263,43 @@ public class SyncthingRunnable implements Runnable {
* Look for a running libsyncthing.so process and nice its IO. * Look for a running libsyncthing.so process and nice its IO.
*/ */
private void niceSyncthing() { private void niceSyncthing() {
new Thread() { Process nice = null;
public void run() { DataOutputStream niceOut = null;
Process nice = null; int ret = 1;
DataOutputStream niceOut = null; try {
int ret = 1; List<String> syncthingPIDs = getSyncthingPIDs();
try { if (syncthingPIDs.isEmpty()) {
Thread.sleep(1000); // Wait a second before getting the pid Log.w(TAG, "niceSyncthing: Found no running instances of " + Constants.FILENAME_SYNCTHING_BINARY);
List<String> syncthingPIDs = getSyncthingPIDs(); } else {
if (syncthingPIDs.isEmpty()) { nice = Runtime.getRuntime().exec((mUseRoot) ? "su" : "sh");
Log.w(TAG, "niceSyncthing: Found no running instances of " + Constants.FILENAME_SYNCTHING_BINARY); niceOut = new DataOutputStream(nice.getOutputStream());
} else { for (String syncthingPID : syncthingPIDs) {
nice = Runtime.getRuntime().exec((mUseRoot) ? "su" : "sh"); // Set best-effort, low priority using ionice.
niceOut = new DataOutputStream(nice.getOutputStream()); niceOut.writeBytes("/system/bin/ionice " + syncthingPID + " be 7\n");
for (String syncthingPID : syncthingPIDs) {
// Set best-effort, low priority using ionice.
niceOut.writeBytes("ionice " + syncthingPID + " be 7\n");
}
niceOut.writeBytes("exit\n");
log(nice.getErrorStream(), Log.WARN, false);
niceOut.flush();
ret = nice.waitFor();
Log.i(TAG_NICE, "ionice performed on " + Constants.FILENAME_SYNCTHING_BINARY);
}
} catch (IOException | InterruptedException e) {
Log.e(TAG_NICE, "Failed to execute ionice binary", e);
} finally {
try {
if (niceOut != null) {
niceOut.close();
}
} catch (IOException e) {
Log.w(TAG_NICE, "Failed to close shell stream", e);
}
if (nice != null) {
nice.destroy();
}
if (ret != 0) {
Log.e(TAG_NICE, "Failed to set ionice " + Integer.toString(ret));
}
} }
niceOut.writeBytes("exit\n");
log(nice.getErrorStream(), Log.WARN, false);
niceOut.flush();
ret = nice.waitFor();
Log.i(TAG_NICE, "ionice performed on " + Constants.FILENAME_SYNCTHING_BINARY);
} }
}.start(); } catch (IOException | InterruptedException e) {
Log.e(TAG_NICE, "Failed to execute ionice binary", e);
} finally {
try {
if (niceOut != null) {
niceOut.close();
}
} catch (IOException e) {
Log.w(TAG_NICE, "Failed to close shell stream", e);
}
if (nice != null) {
nice.destroy();
}
if (ret != 0) {
Log.e(TAG_NICE, "Failed to set ionice " + Integer.toString(ret));
}
}
} }
public interface OnSyncthingKilled { public interface OnSyncthingKilled {
@ -314,21 +309,18 @@ public class SyncthingRunnable implements Runnable {
* Look for running libsyncthing.so processes and kill them. * Look for running libsyncthing.so processes and kill them.
* Try a SIGINT first, then try again with SIGKILL. * Try a SIGINT first, then try again with SIGKILL.
*/ */
public void killSyncthing(OnSyncthingKilled onKilledListener) { public void killSyncthing() {
new Thread(() -> { for (int i = 0; i < 2; i++) {
for (int i = 0; i < 2; i++) { List<String> syncthingPIDs = getSyncthingPIDs();
List<String> syncthingPIDs = getSyncthingPIDs(); if (syncthingPIDs.isEmpty()) {
if (syncthingPIDs.isEmpty()) { Log.d(TAG, "killSyncthing: Found no more running instances of " + Constants.FILENAME_SYNCTHING_BINARY);
Log.d(TAG, "killSyncthing: Found no more running instances of " + Constants.FILENAME_SYNCTHING_BINARY); break;
break;
}
for (String syncthingPID : syncthingPIDs) {
killProcessId(syncthingPID, i > 0);
}
} }
onKilledListener.onKilled();
}).start(); for (String syncthingPID : syncthingPIDs) {
killProcessId(syncthingPID, i > 0);
}
}
} }
/** /**

View file

@ -59,18 +59,8 @@ public class SyncthingService extends Service {
public static final String ACTION_REFRESH_NETWORK_INFO = public static final String ACTION_REFRESH_NETWORK_INFO =
"com.nutomic.syncthingandroid.service.SyncthingService.REFRESH_NETWORK_INFO"; "com.nutomic.syncthingandroid.service.SyncthingService.REFRESH_NETWORK_INFO";
/** public interface OnServiceStateChangeListener {
* Callback for when the Syncthing web interface becomes first available after service start. void onServiceStateChange(State currentState);
*/
public interface OnWebGuiAvailableListener {
void onWebGuiAvailable();
}
private final HashSet<OnWebGuiAvailableListener> mOnWebGuiAvailableListeners =
new HashSet<>();
public interface OnApiChangeListener {
void onApiChange(State currentState);
} }
/** /**
@ -81,40 +71,54 @@ public class SyncthingService extends Service {
INIT, INIT,
/** Syncthing binary is starting. */ /** Syncthing binary is starting. */
STARTING, STARTING,
/** Syncthing binary is running, API is available. */ /** Syncthing binary is running,
* Rest API is available,
* RestApi class read the config and is fully initialized.
*/
ACTIVE, ACTIVE,
/** Syncthing is stopped according to user preferences. */ /** Syncthing binary is shutting down. */
DISABLED, DISABLED,
/** There is some problem that prevents Syncthing from running. */ /** There is some problem that prevents Syncthing from running. */
ERROR, ERROR,
} }
private State mCurrentState = State.INIT; /**
* Initialize the service with State.DISABLED as {@link DeviceStateHolder} will
* send an update if we should run the binary after it got instantiated in
* {@link onStartCommand}.
*/
private State mCurrentState = State.DISABLED;
private ConfigXml mConfig; private ConfigXml mConfig;
private RestApi mApi; private @Nullable PollWebGuiAvailableTask mPollWebGuiAvailableTask = null;
private EventProcessor mEventProcessor; private @Nullable RestApi mApi = null;
private @Nullable EventProcessor mEventProcessor = null;
private @Nullable DeviceStateHolder mDeviceStateHolder = null; private @Nullable DeviceStateHolder mDeviceStateHolder = null;
private SyncthingRunnable mSyncthingRunnable; private @Nullable SyncthingRunnable mSyncthingRunnable = null;
private Thread mSyncthingRunnableThread = null;
private Handler mHandler; private Handler mHandler;
private final HashSet<OnApiChangeListener> mOnApiChangeListeners = new HashSet<>(); private final HashSet<OnServiceStateChangeListener> mOnServiceStateChangeListeners = new HashSet<>();
private final SyncthingServiceBinder mBinder = new SyncthingServiceBinder(this); private final SyncthingServiceBinder mBinder = new SyncthingServiceBinder(this);
@Inject NotificationHandler mNotificationHandler; @Inject NotificationHandler mNotificationHandler;
@Inject SharedPreferences mPreferences; @Inject SharedPreferences mPreferences;
/** /**
* Object that can be locked upon when accessing mCurrentState * Object that must be locked upon accessing mCurrentState
* Currently used to male onDestroy() and PollWebGuiAvailableTaskImpl.onPostExcecute() tread-safe
*/ */
private final Object mStateLock = new Object(); private final Object mStateLock = new Object();
/** /**
* True if a stop was requested while syncthing is starting, in that case, perform stop in * Stores the result of the last should run decision received by OnDeviceStateChangedListener.
* {@link #pollWebGui}.
*/ */
private boolean mStopScheduled = false; private boolean mLastDeterminedShouldRun = false;
/**
* True if a service {@link onDestroy} was requested while syncthing is starting,
* in that case, perform stop in {@link onApiAvailable}.
*/
private boolean mDestroyScheduled = false;
/** /**
* True if the user granted the storage permission. * True if the user granted the storage permission.
@ -126,6 +130,7 @@ public class SyncthingService extends Service {
*/ */
@Override @Override
public void onCreate() { public void onCreate() {
Log.v(TAG, "onCreate");
super.onCreate(); super.onCreate();
PRNGFixes.apply(); PRNGFixes.apply();
((SyncthingApp) getApplication()).component().inject(this); ((SyncthingApp) getApplication()).component().inject(this);
@ -148,6 +153,7 @@ public class SyncthingService extends Service {
*/ */
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
Log.v(TAG, "onStartCommand");
if (!mStoragePermissionGranted) { if (!mStoragePermissionGranted) {
Log.e(TAG, "User revoked storage permission. Stopping service."); Log.e(TAG, "User revoked storage permission. Stopping service.");
if (mNotificationHandler != null) { if (mNotificationHandler != null) {
@ -157,7 +163,27 @@ public class SyncthingService extends Service {
return START_NOT_STICKY; return START_NOT_STICKY;
} }
mDeviceStateHolder = new DeviceStateHolder(SyncthingService.this, this::onUpdatedShouldRunDecision); /**
* Send current service state to listening endpoints.
* This is required that components know about the service State.DISABLED
* if DeviceStateHolder does not send a "shouldRun = true" callback
* to start the binary according to preferences shortly after its creation.
* See {@link mLastDeterminedShouldRun} defaulting to "false".
*/
if (mCurrentState == State.DISABLED) {
synchronized(mStateLock) {
onServiceStateChange(mCurrentState);
}
}
if (mDeviceStateHolder == null) {
/**
* Instantiate the run condition monitor on first onStartCommand and
* enable callback on run condition change affecting the final decision to
* run/terminate syncthing. After initial run conditions are collected
* the first decision is sent to {@link onUpdatedShouldRunDecision}.
*/
mDeviceStateHolder = new DeviceStateHolder(SyncthingService.this, this::onUpdatedShouldRunDecision);
}
mNotificationHandler.updatePersistentNotification(this); mNotificationHandler.updatePersistentNotification(this);
if (intent == null) if (intent == null)
@ -185,35 +211,41 @@ public class SyncthingService extends Service {
* After run conditions monitored by {@link DeviceStateHolder} changed and * After run conditions monitored by {@link DeviceStateHolder} changed and
* it had an influence on the decision to run/terminate syncthing, this * 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. * function is called to notify this class to run/terminate the syncthing binary.
* {@link #onApiChange} is called while applying the decision change. * {@link #onServiceStateChange} is called while applying the decision change.
*/ */
private void onUpdatedShouldRunDecision(boolean shouldRun) { private void onUpdatedShouldRunDecision(boolean newShouldRunDecision) {
if (shouldRun) { if (newShouldRunDecision != mLastDeterminedShouldRun) {
// Start syncthing. Log.i(TAG, "shouldRun decision changed to " + newShouldRunDecision + " according to configured run conditions.");
switch (mCurrentState) { mLastDeterminedShouldRun = newShouldRunDecision;
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;
}
} else {
// Stop syncthing.
if (mCurrentState == State.DISABLED)
return;
Log.i(TAG, "Stopping syncthing according to current state and preferences"); // React to the shouldRun condition change.
shutdown(State.DISABLED, () -> {}); if (newShouldRunDecision) {
// 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.v(TAG, "Starting syncthing");
new StartupTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
});
break;
case STARTING:
case ACTIVE:
case ERROR:
break;
default:
break;
}
} else {
// Stop syncthing.
if (mCurrentState == State.DISABLED) {
return;
}
Log.v(TAG, "Stopping syncthing");
shutdown(State.DISABLED, () -> {});
}
} }
} }
@ -223,8 +255,16 @@ public class SyncthingService extends Service {
*/ */
private class StartupTask extends AsyncTask<Void, Void, Void> { private class StartupTask extends AsyncTask<Void, Void, Void> {
public StartupTask() { @Override
onApiChange(State.STARTING); protected void onPreExecute() {
synchronized(mStateLock) {
if (mCurrentState != State.INIT) {
Log.e(TAG, "StartupTask: Wrong state " + mCurrentState + " detected. Cancelling.");
cancel(true);
return;
}
onServiceStateChange(State.STARTING);
}
} }
@Override @Override
@ -234,7 +274,9 @@ public class SyncthingService extends Service {
mConfig.updateIfNeeded(); mConfig.updateIfNeeded();
} catch (ConfigXml.OpenConfigException e) { } catch (ConfigXml.OpenConfigException e) {
mNotificationHandler.showCrashedNotification(R.string.config_create_failed, true); mNotificationHandler.showCrashedNotification(R.string.config_create_failed, true);
onApiChange(State.ERROR); synchronized (mStateLock) {
onServiceStateChange(State.ERROR);
}
cancel(true); cancel(true);
} }
return null; return null;
@ -242,26 +284,77 @@ public class SyncthingService extends Service {
@Override @Override
protected void onPostExecute(Void aVoid) { protected void onPostExecute(Void aVoid) {
mApi = new RestApi(SyncthingService.this, mConfig.getWebGuiUrl(), mConfig.getApiKey(), if (mApi == null) {
SyncthingService.this::onSyncthingStarted, () -> onApiChange(mCurrentState)); mApi = new RestApi(SyncthingService.this, mConfig.getWebGuiUrl(), mConfig.getApiKey(),
SyncthingService.this::onApiAvailable, () -> onServiceStateChange(mCurrentState));
Log.i(TAG, "Web GUI will be available at " + mConfig.getWebGuiUrl());
}
mEventProcessor = new EventProcessor(SyncthingService.this, mApi); // Start the syncthing binary.
if (mSyncthingRunnable != null || mSyncthingRunnableThread != null) {
if (mApi != null) Log.e(TAG, "StartupTask/onPostExecute: Syncthing binary lifecycle violated");
registerOnWebGuiAvailableListener(mApi); return;
if (mEventProcessor != null) }
registerOnWebGuiAvailableListener(mEventProcessor);
Log.i(TAG, "Web GUI will be available at " + mConfig.getWebGuiUrl());
pollWebGui();
mSyncthingRunnable = new SyncthingRunnable(SyncthingService.this, SyncthingRunnable.Command.main); mSyncthingRunnable = new SyncthingRunnable(SyncthingService.this, SyncthingRunnable.Command.main);
new Thread(mSyncthingRunnable).start(); mSyncthingRunnableThread = new Thread(mSyncthingRunnable);
mSyncthingRunnableThread.start();
/**
* Wait for the web-gui of the native syncthing binary to come online.
*
* In case the binary is to be stopped, also be aware that another thread could request
* to stop the binary in the time while waiting for the GUI to become active. See the comment
* for SyncthingService.onDestroy for details.
*/
if (mPollWebGuiAvailableTask == null) {
mPollWebGuiAvailableTask = new PollWebGuiAvailableTask(
SyncthingService.this,
getWebGuiUrl(),
mConfig.getApiKey(),
result -> {
Log.i(TAG, "Web GUI has come online at " + mConfig.getWebGuiUrl());
if (mApi != null) {
mApi.readConfigFromRestApi();
}
});
}
} }
} }
private void onSyncthingStarted() { /**
onApiChange(State.ACTIVE); * Called when {@link PollWebGuiAvailableTask} confirmed the REST API is available.
Log.i(TAG, "onSyncthingStarted(): State.ACTIVE reached."); * We can assume mApi being available under normal conditions.
* UI stressing results in mApi getting null on simultaneous shutdown, so
* we check it for safety.
*/
private void onApiAvailable() {
if (mApi == null) {
Log.e(TAG, "onApiAvailable: Did we stop the binary during startup? mApi == null");
return;
}
synchronized (mStateLock) {
if (mCurrentState != State.STARTING) {
Log.e(TAG, "onApiAvailable: Wrong state " + mCurrentState + " detected. Cancelling callback.");
return;
}
onServiceStateChange(State.ACTIVE);
}
/**
* If the service instance got an onDestroy() event while being in
* State.STARTING we'll trigger the service onDestroy() now. this
* allows the syncthing binary to get gracefully stopped.
*/
if (mDestroyScheduled) {
mDestroyScheduled = false;
stopSelf();
return;
}
if (mEventProcessor == null) {
mEventProcessor = new EventProcessor(SyncthingService.this, mApi);
mEventProcessor.start();
}
} }
@Override @Override
@ -271,18 +364,23 @@ public class SyncthingService extends Service {
/** /**
* Stops the native binary. * Stops the native binary.
* * Shuts down DeviceStateHolder instance.
* The native binary crashes if stopped before it is fully active. In that case signal the
* stop request to PollWebGuiAvailableTaskImpl that is active in that situation and terminate
* the service there.
*/ */
@Override @Override
public void onDestroy() { public void onDestroy() {
Log.v(TAG, "onDestroy");
if (mDeviceStateHolder != null) {
/**
* Shut down the OnDeviceStateChangedListener so we won't get interrupted by run
* condition events that occur during shutdown.
*/
mDeviceStateHolder.shutdown();
}
if (mStoragePermissionGranted) { if (mStoragePermissionGranted) {
synchronized (mStateLock) { synchronized (mStateLock) {
if (mCurrentState == State.INIT || mCurrentState == State.STARTING) { if (mCurrentState == State.STARTING) {
Log.i(TAG, "Delay shutting down synchting binary until initialisation finished"); Log.i(TAG, "Delay shutting down synchting binary until initialisation finished");
mStopScheduled = true; mDestroyScheduled = true;
} else { } else {
Log.i(TAG, "Shutting down syncthing binary immediately"); Log.i(TAG, "Shutting down syncthing binary immediately");
shutdown(State.DISABLED, () -> {}); shutdown(State.DISABLED, () -> {});
@ -294,10 +392,7 @@ public class SyncthingService extends Service {
Log.i(TAG, "Shutting down syncthing binary due to missing storage permission."); Log.i(TAG, "Shutting down syncthing binary due to missing storage permission.");
shutdown(State.DISABLED, () -> {}); shutdown(State.DISABLED, () -> {});
} }
super.onDestroy();
if (mDeviceStateHolder != null) {
mDeviceStateHolder.shutdown();
}
} }
/** /**
@ -307,37 +402,44 @@ public class SyncthingService extends Service {
*/ */
private void shutdown(State newState, SyncthingRunnable.OnSyncthingKilled onKilledListener) { private void shutdown(State newState, SyncthingRunnable.OnSyncthingKilled onKilledListener) {
Log.i(TAG, "Shutting down background service"); Log.i(TAG, "Shutting down background service");
onApiChange(newState); synchronized(mStateLock) {
onServiceStateChange(newState);
}
if (mEventProcessor != null) if (mPollWebGuiAvailableTask != null) {
mEventProcessor.shutdown(); mPollWebGuiAvailableTask.cancelRequestsAndCallback();
mPollWebGuiAvailableTask = null;
}
if (mApi != null) if (mEventProcessor != null) {
mEventProcessor.stop();
mEventProcessor = null;
}
if (mApi != null) {
mApi.shutdown(); mApi.shutdown();
mApi = null;
}
if (mNotificationHandler != null) if (mNotificationHandler != null) {
mNotificationHandler.cancelPersistentNotification(this); mNotificationHandler.cancelPersistentNotification(this);
}
if (mSyncthingRunnable != null) { if (mSyncthingRunnable != null) {
mSyncthingRunnable.killSyncthing(onKilledListener); mSyncthingRunnable.killSyncthing();
if (mSyncthingRunnableThread != null) {
Log.v(TAG, "Waiting for mSyncthingRunnableThread to finish after killSyncthing ...");
try {
mSyncthingRunnableThread.join();
} catch (InterruptedException e) {
Log.w(TAG, "mSyncthingRunnableThread InterruptedException");
}
Log.v(TAG, "Finished mSyncthingRunnableThread.");
mSyncthingRunnableThread = null;
}
mSyncthingRunnable = null; mSyncthingRunnable = null;
} else {
onKilledListener.onKilled();
}
}
/**
* Register a listener for the web gui becoming available..
*
* If the web gui is already available, listener will be called immediately.
* Listeners are unregistered automatically after being called.
*/
public void registerOnWebGuiAvailableListener(OnWebGuiAvailableListener listener) {
if (mCurrentState == State.ACTIVE) {
listener.onWebGuiAvailable();
} else {
mOnWebGuiAvailableListeners.add(listener);
} }
onKilledListener.onKilled();
} }
public @Nullable RestApi getApi() { public @Nullable RestApi getApi() {
@ -350,60 +452,37 @@ public class SyncthingService extends Service {
* The listener is called immediately with the current state, and again whenever the state * The listener is called immediately with the current state, and again whenever the state
* changes. The call is always from the GUI thread. * changes. The call is always from the GUI thread.
* *
* @see #unregisterOnApiChangeListener * @see #unregisterOnServiceStateChangeListener
*/ */
public void registerOnApiChangeListener(OnApiChangeListener listener) { public void registerOnServiceStateChangeListener(OnServiceStateChangeListener listener) {
// Make sure we don't send an invalid state or syncthing might show a "disabled" message // Make sure we don't send an invalid state or syncthing might show a "disabled" message
// when it's just starting up. // when it's just starting up.
listener.onApiChange(mCurrentState); listener.onServiceStateChange(mCurrentState);
mOnApiChangeListeners.add(listener); mOnServiceStateChangeListeners.add(listener);
} }
/** /**
* Unregisters a previously registered listener. * Unregisters a previously registered listener.
* *
* @see #registerOnApiChangeListener * @see #registerOnServiceStateChangeListener
*/ */
public void unregisterOnApiChangeListener(OnApiChangeListener listener) { public void unregisterOnServiceStateChangeListener(OnServiceStateChangeListener listener) {
mOnApiChangeListeners.remove(listener); mOnServiceStateChangeListeners.remove(listener);
}
/**
* Wait for the web-gui of the native syncthing binary to come online.
*
* In case the binary is to be stopped, also be aware that another thread could request
* to stop the binary in the time while waiting for the GUI to become active. See the comment
* for SyncthingService.onDestroy for details.
*/
private void pollWebGui() {
new PollWebGuiAvailableTask(this, getWebGuiUrl(), mConfig.getApiKey(), result -> {
synchronized (mStateLock) {
if (mStopScheduled) {
shutdown(State.DISABLED, () -> {});
mStopScheduled = false;
stopSelf();
return;
}
}
Log.i(TAG, "Web GUI has come online at " + mConfig.getWebGuiUrl());
onApiChange(State.STARTING);
Stream.of(mOnWebGuiAvailableListeners).forEach(OnWebGuiAvailableListener::onWebGuiAvailable);
mOnWebGuiAvailableListeners.clear();
});
} }
/** /**
* Called to notifiy listeners of an API change. * Called to notifiy listeners of an API change.
*/ */
private void onApiChange(State newState) { private void onServiceStateChange(State newState) {
Log.v(TAG, "onServiceStateChange: from " + mCurrentState + " to " + newState);
mCurrentState = newState;
mHandler.post(() -> { mHandler.post(() -> {
mCurrentState = newState;
mNotificationHandler.updatePersistentNotification(this); mNotificationHandler.updatePersistentNotification(this);
for (Iterator<OnApiChangeListener> i = mOnApiChangeListeners.iterator(); for (Iterator<OnServiceStateChangeListener> i = mOnServiceStateChangeListeners.iterator();
i.hasNext(); ) { i.hasNext(); ) {
OnApiChangeListener listener = i.next(); OnServiceStateChangeListener listener = i.next();
if (listener != null) { if (listener != null) {
listener.onApiChange(mCurrentState); listener.onServiceStateChange(mCurrentState);
} else { } else {
i.remove(); i.remove();
} }