From e9eef4332b058ea09cceb9900d72d6294efcf363 Mon Sep 17 00:00:00 2001 From: Catfriend1 Date: Sun, 10 Jun 2018 00:39:42 +0200 Subject: [PATCH] Refactor SyncthingService (lifecycle), DeviceStateHolder, RestApi, multiple fixes (#1119) --- .../activities/DeviceActivity.java | 9 +- .../activities/FirstStartActivity.java | 3 - .../activities/FolderActivity.java | 13 +- .../activities/FolderPickerActivity.java | 11 +- .../activities/MainActivity.java | 39 +- .../activities/SettingsActivity.java | 32 +- .../activities/ShareActivity.java | 6 +- .../activities/StateDialogActivity.java | 31 +- .../activities/WebGuiActivity.java | 29 +- .../fragments/DeviceListFragment.java | 4 +- .../fragments/DrawerFragment.java | 17 +- .../fragments/FolderListFragment.java | 4 +- .../syncthingandroid/http/ApiRequest.java | 10 +- .../http/PollWebGuiAvailableTask.java | 34 +- .../receiver/NetworkReceiver.java | 20 +- .../service/DeviceStateHolder.java | 105 +++--- .../service/EventProcessor.java | 14 +- .../service/NotificationHandler.java | 35 +- .../service/ReceiverManager.java | 50 +++ .../syncthingandroid/service/RestApi.java | 85 +++-- .../service/SyncthingRunnable.java | 100 +++-- .../service/SyncthingService.java | 355 +++++++++++------- 22 files changed, 625 insertions(+), 381 deletions(-) create mode 100644 app/src/main/java/com/nutomic/syncthingandroid/service/ReceiverManager.java diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/DeviceActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/DeviceActivity.java index 97152d5b..cc2eb482 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/DeviceActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/DeviceActivity.java @@ -213,8 +213,9 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis @Override public void onDestroy() { super.onDestroy(); - if (getService() != null) { - getService().unregisterOnApiChangeListener(this::onApiChange); + SyncthingService syncthingService = getService(); + if (syncthingService != null) { + syncthingService.unregisterOnServiceStateChangeListener(this::onServiceStateChange); } mIdView.removeTextChangedListener(mIdTextWatcher); mNameView.removeTextChangedListener(mNameTextWatcher); @@ -252,7 +253,7 @@ public class DeviceActivity extends SyncthingActivity implements View.OnClickLis } 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) { finish(); return; diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/FirstStartActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/FirstStartActivity.java index 6b35f9e9..6d418646 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/FirstStartActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/FirstStartActivity.java @@ -16,7 +16,6 @@ import android.widget.Toast; import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.SyncthingApp; -import com.nutomic.syncthingandroid.service.SyncthingService; import javax.inject.Inject; @@ -66,8 +65,6 @@ public class FirstStartActivity extends Activity implements Button.OnClickListen 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 // navigation works as expected. Intent mainIntent = new Intent(this, MainActivity.class); diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderActivity.java index 0ab47126..5cfb9b10 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderActivity.java @@ -51,7 +51,7 @@ import static com.nutomic.syncthingandroid.service.SyncthingService.State.ACTIVE * Shows folder details and allows changing them. */ public class FolderActivity extends SyncthingActivity - implements SyncthingActivity.OnServiceConnectedListener, SyncthingService.OnApiChangeListener { + implements SyncthingActivity.OnServiceConnectedListener, SyncthingService.OnServiceStateChangeListener { public static final String EXTRA_IS_CREATE = "com.nutomic.syncthingandroid.activities.DeviceActivity.IS_CREATE"; @@ -234,8 +234,9 @@ public class FolderActivity extends SyncthingActivity @Override public void onDestroy() { super.onDestroy(); - if (getService() != null) { - getService().unregisterOnApiChangeListener(this); + SyncthingService syncthingService = getService(); + if (syncthingService != null) { + syncthingService.unregisterOnServiceStateChangeListener(this::onServiceStateChange); } mLabelView.removeTextChangedListener(mTextWatcher); mIdView.removeTextChangedListener(mTextWatcher); @@ -270,11 +271,11 @@ public class FolderActivity extends SyncthingActivity */ @Override public void onServiceConnected() { - getService().registerOnApiChangeListener(this); + getService().registerOnServiceStateChangeListener(this); } @Override - public void onApiChange(SyncthingService.State currentState) { + public void onServiceStateChange(SyncthingService.State currentState) { if (currentState != ACTIVE) { finish(); 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 // 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() { if (mFolder != null && mVersioning != null){ mFolder.versioning = mVersioning; diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderPickerActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderPickerActivity.java index 26b0e387..9cb9eecb 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderPickerActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/FolderPickerActivity.java @@ -44,7 +44,7 @@ import java.util.Iterator; * Activity that allows selecting a directory in the local file system. */ public class FolderPickerActivity extends SyncthingActivity - implements AdapterView.OnItemClickListener, SyncthingService.OnApiChangeListener { + implements AdapterView.OnItemClickListener, SyncthingService.OnServiceStateChangeListener { private static final String EXTRA_INITIAL_DIRECTORY = "com.nutomic.syncthingandroid.activities.FolderPickerActivity.INITIAL_DIRECTORY"; @@ -156,13 +156,16 @@ public class FolderPickerActivity extends SyncthingActivity @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { super.onServiceConnected(componentName, iBinder); - getService().registerOnApiChangeListener(this); + getService().registerOnServiceStateChangeListener(this); } @Override protected void onDestroy() { super.onDestroy(); - getService().unregisterOnApiChangeListener(this); + SyncthingService syncthingService = getService(); + if (syncthingService != null) { + syncthingService.unregisterOnServiceStateChangeListener(this::onServiceStateChange); + } } @Override @@ -323,7 +326,7 @@ public class FolderPickerActivity extends SyncthingActivity } @Override - public void onApiChange(SyncthingService.State currentState) { + public void onServiceStateChange(SyncthingService.State currentState) { if (!isFinishing() && currentState != SyncthingService.State.ACTIVE) { setResult(Activity.RESULT_CANCELED); finish(); diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java index fca5ac1f..148fce1c 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/MainActivity.java @@ -66,7 +66,7 @@ import static java.lang.Math.min; * {@link DrawerFragment} in the navigation drawer. */ public class MainActivity extends StateDialogActivity - implements SyncthingService.OnApiChangeListener { + implements SyncthingService.OnServiceStateChangeListener { private static final String TAG = "MainActivity"; 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. */ @Override - public void onApiChange(SyncthingService.State currentState) { + public void onServiceStateChange(SyncthingService.State currentState) { switch (currentState) { case STARTING: break; @@ -254,6 +254,10 @@ public class MainActivity extends StateDialogActivity mDrawerLayout.addDrawerListener(mDrawerToggle); 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()); } @@ -271,19 +275,20 @@ public class MainActivity extends StateDialogActivity @Override public void onDestroy() { super.onDestroy(); - if (getService() != null) { - getService().unregisterOnApiChangeListener(this); - getService().unregisterOnApiChangeListener(mFolderListFragment); - getService().unregisterOnApiChangeListener(mDeviceListFragment); + SyncthingService mSyncthingService = getService(); + if (mSyncthingService != null) { + mSyncthingService.unregisterOnServiceStateChangeListener(this); + mSyncthingService.unregisterOnServiceStateChangeListener(mFolderListFragment); + mSyncthingService.unregisterOnServiceStateChangeListener(mDeviceListFragment); } } @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { super.onServiceConnected(componentName, iBinder); - getService().registerOnApiChangeListener(this); - getService().registerOnApiChangeListener(mFolderListFragment); - getService().registerOnApiChangeListener(mDeviceListFragment); + getService().registerOnServiceStateChangeListener(this); + getService().registerOnServiceStateChangeListener(mFolderListFragment); + getService().registerOnServiceStateChangeListener(mDeviceListFragment); } /** @@ -430,15 +435,19 @@ public class MainActivity extends StateDialogActivity return super.onKeyDown(keyCode, e); } - /** - * Close drawer on back button press. - */ @Override public void onBackPressed() { - if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) + if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) { + // Close drawer on back button press. closeDrawer(); - else - super.onBackPressed(); + } else { + /** + * 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); + } } /** diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/SettingsActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/SettingsActivity.java index e43c77fd..027ff1ce 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/SettingsActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/SettingsActivity.java @@ -74,7 +74,7 @@ public class SettingsActivity extends SyncthingActivity { public static class SettingsFragment extends PreferenceFragment implements SyncthingActivity.OnServiceConnectedListener, - SyncthingService.OnApiChangeListener, Preference.OnPreferenceChangeListener, + SyncthingService.OnServiceStateChangeListener, Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener, SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = "SettingsFragment"; @@ -246,32 +246,26 @@ public class SettingsActivity extends SyncthingActivity { @Override public void onServiceConnected() { + Log.v(TAG, "onServiceConnected"); if (getActivity() == null) return; mSyncthingService = ((SyncthingActivity) getActivity()).getService(); - mSyncthingService.registerOnApiChangeListener(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(); - } - - }); + mSyncthingService.registerOnServiceStateChangeListener(this); } @Override - public void onApiChange(SyncthingService.State currentState) { - boolean syncthingActive = currentState == SyncthingService.State.ACTIVE; - boolean isSyncthingRunning = syncthingActive && mSyncthingService.getApi().isConfigLoaded(); + public void onServiceStateChange(SyncthingService.State currentState) { + mApi = mSyncthingService.getApi(); + boolean isSyncthingRunning = (mApi != null) && + mApi.isConfigLoaded() && + (currentState == SyncthingService.State.ACTIVE); mCategorySyncthingOptions.setEnabled(isSyncthingRunning); mCategoryBackup.setEnabled(isSyncthingRunning); if (!isSyncthingRunning) return; - mApi = mSyncthingService.getApi(); mSyncthingVersion.setSummary(mApi.getVersion()); mOptions = mApi.getOptions(); mGui = mApi.getGui(); @@ -296,7 +290,7 @@ public class SettingsActivity extends SyncthingActivity { public void onDestroy() { mPreferences.unregisterOnSharedPreferenceChangeListener(this); if (mSyncthingService != null) { - mSyncthingService.unregisterOnApiChangeListener(this); + mSyncthingService.unregisterOnServiceStateChangeListener(this); } super.onDestroy(); } @@ -382,9 +376,9 @@ public class SettingsActivity extends SyncthingActivity { @Override public void onStop() { if (mRequireRestart) { - if (mSyncthingService.getCurrentState() != SyncthingService.State.DISABLED && - mSyncthingService.getApi() != null) { - mSyncthingService.getApi().restart(); + if (mSyncthingService != null && mApi != null && + mSyncthingService.getCurrentState() != SyncthingService.State.DISABLED) { + mApi.restart(); mRequireRestart = false; } } @@ -405,7 +399,7 @@ public class SettingsActivity extends SyncthingActivity { : R.string.always_run_in_background_disabled); mSyncOnlyCharging.setEnabled(value); mSyncOnlyWifi.setEnabled(value); - mSyncOnlyOnSSIDs.setEnabled(mSyncOnlyWifi.isChecked()); + mSyncOnlyOnSSIDs.setEnabled(false); // Uncheck items when disabled, so it is clear they have no effect. if (!value) { mSyncOnlyCharging.setChecked(false); diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/ShareActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/ShareActivity.java index 2fbf467f..bb5fa253 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/ShareActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/ShareActivity.java @@ -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} */ 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 PREF_PREVIOUSLY_SELECTED_SYNCTHING_FOLDER = "previously_selected_syncthing_folder"; @@ -57,7 +57,7 @@ public class ShareActivity extends StateDialogActivity private Spinner mFoldersSpinner; @Override - public void onApiChange(SyncthingService.State currentState) { + public void onServiceStateChange(SyncthingService.State currentState) { if (currentState != SyncthingService.State.ACTIVE || getApi() == null) return; @@ -85,7 +85,7 @@ public class ShareActivity extends StateDialogActivity @Override public void onServiceConnected() { - getService().registerOnApiChangeListener(this); + getService().registerOnServiceStateChangeListener(this); } @Override diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/StateDialogActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/StateDialogActivity.java index f90ab337..7a0fe7e7 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/StateDialogActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/StateDialogActivity.java @@ -12,6 +12,7 @@ import android.view.View; import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.databinding.DialogLoadingBinding; import com.nutomic.syncthingandroid.service.SyncthingService; +import com.nutomic.syncthingandroid.service.SyncthingService.State; import com.nutomic.syncthingandroid.util.Util; 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 State mServiceState = State.INIT; private AlertDialog mLoadingDialog; private AlertDialog mDisabledDialog; private boolean mIsPaused = true; @@ -31,13 +33,20 @@ public abstract class StateDialogActivity extends SyncthingActivity { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); registerOnServiceConnectedListener(() -> - getService().registerOnApiChangeListener(this::onApiChange)); + getService().registerOnServiceStateChangeListener(this::onServiceStateChange)); } @Override protected void onResume() { super.onResume(); mIsPaused = false; + switch (mServiceState) { + case DISABLED: + showDisabledDialog(); + break; + default: + break; + } } @Override @@ -52,13 +61,14 @@ public abstract class StateDialogActivity extends SyncthingActivity { protected void onDestroy() { super.onDestroy(); if (getService() != null) { - getService().unregisterOnApiChangeListener(this::onApiChange); + getService().unregisterOnServiceStateChangeListener(this::onServiceStateChange); } dismissDisabledDialog(); } - private void onApiChange(SyncthingService.State currentState) { - switch (currentState) { + private void onServiceStateChange(SyncthingService.State currentState) { + mServiceState = currentState; + switch (mServiceState) { case INIT: // fallthrough case STARTING: dismissDisabledDialog(); @@ -69,24 +79,25 @@ public abstract class StateDialogActivity extends SyncthingActivity { dismissLoadingDialog(); break; case DISABLED: - dismissLoadingDialog(); - if (!isFinishing()) { + if (!mIsPaused) { showDisabledDialog(); } break; + case ERROR: // fallthrough + default: + break; } } private void showDisabledDialog() { - if (mIsPaused) + if (this.isFinishing() && (mDisabledDialog != null)) { return; - + } mDisabledDialog = new AlertDialog.Builder(this) .setTitle(R.string.syncthing_disabled_title) .setMessage(R.string.syncthing_disabled_message) .setPositiveButton(R.string.syncthing_disabled_change_settings, (dialogInterface, i) -> { - finish(); startActivity(new Intent(this, SettingsActivity.class)); } ) @@ -123,7 +134,7 @@ public abstract class StateDialogActivity extends SyncthingActivity { if (!isGeneratingKeys) { new Handler().postDelayed(() -> { - if (isFinishing() || mLoadingDialog == null) + if (this.isFinishing() || mLoadingDialog == null) return; binding.loadingSlowMessage.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java b/app/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java index 17b3bbcf..5512d2bb 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java @@ -47,7 +47,7 @@ import java.util.Properties; * Holds a WebView that shows the web ui of the local syncthing instance. */ public class WebGuiActivity extends StateDialogActivity - implements SyncthingService.OnWebGuiAvailableListener { + implements SyncthingService.OnServiceStateChangeListener { private static final String TAG = "WebGuiActivity"; @@ -140,20 +140,31 @@ public class WebGuiActivity extends StateDialogActivity mWebView.getSettings().setDomStorageEnabled(true); mWebView.setWebViewClient(mWebViewClient); 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 public void onServiceConnected(ComponentName componentName, IBinder iBinder) { super.onServiceConnected(componentName, iBinder); - getService().registerOnWebGuiAvailableListener(WebGuiActivity.this); + getService().registerOnServiceStateChangeListener(this); } @Override - public void onWebGuiAvailable() { - if (mWebView.getUrl() == null) { - mWebView.stopLoading(); - setWebViewProxy(mWebView.getContext().getApplicationContext(), "", 0, "localhost|0.0.0.0|127.*|[::1]"); - mWebView.loadUrl(getService().getWebGuiUrl().toString()); + public void onServiceStateChange(SyncthingService.State newState) { + Log.v(TAG, "onServiceStateChange(" + newState + ")"); + if (newState == SyncthingService.State.ACTIVE) { + if (mWebView == null) { + 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 protected void onDestroy() { + SyncthingService mSyncthingService = getService(); + if (mSyncthingService != null) { + mSyncthingService.unregisterOnServiceStateChangeListener(this); + } mWebView.destroy(); mWebView = null; super.onDestroy(); diff --git a/app/src/main/java/com/nutomic/syncthingandroid/fragments/DeviceListFragment.java b/app/src/main/java/com/nutomic/syncthingandroid/fragments/DeviceListFragment.java index b46a43bf..54cc5d3e 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/fragments/DeviceListFragment.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/fragments/DeviceListFragment.java @@ -27,7 +27,7 @@ import java.util.TimerTask; /** * 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 { private final static Comparator DEVICES_COMPARATOR = (lhs, rhs) -> lhs.name.compareTo(rhs.name); @@ -45,7 +45,7 @@ public class DeviceListFragment extends ListFragment implements SyncthingService } @Override - public void onApiChange(SyncthingService.State currentState) { + public void onServiceStateChange(SyncthingService.State currentState) { if (currentState != SyncthingService.State.ACTIVE) return; diff --git a/app/src/main/java/com/nutomic/syncthingandroid/fragments/DrawerFragment.java b/app/src/main/java/com/nutomic/syncthingandroid/fragments/DrawerFragment.java index e2a7ae69..dd55dd7c 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/fragments/DrawerFragment.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/fragments/DrawerFragment.java @@ -136,11 +136,20 @@ public class DrawerFragment extends Fragment implements View.OnClickListener { * Invokes status callbacks. */ private void updateGui() { - if (mActivity.getApi() == null || getActivity() == null || getActivity().isFinishing()) + MainActivity mainActivity = (MainActivity) getActivity(); + if (mainActivity == null) { return; - mActivity.getApi().getSystemInfo(this::onReceiveSystemInfo); - mActivity.getApi().getSystemVersion(this::onReceiveSystemVersion); - mActivity.getApi().getConnections(this::onReceiveConnections); + } + if (mainActivity.isFinishing()) { + return; + } + + RestApi mApi = mainActivity.getApi(); + if (mApi != null) { + mApi.getSystemInfo(this::onReceiveSystemInfo); + mApi.getSystemVersion(this::onReceiveSystemVersion); + mApi.getConnections(this::onReceiveConnections); + } } /** diff --git a/app/src/main/java/com/nutomic/syncthingandroid/fragments/FolderListFragment.java b/app/src/main/java/com/nutomic/syncthingandroid/fragments/FolderListFragment.java index 628c833a..72212a50 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/fragments/FolderListFragment.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/fragments/FolderListFragment.java @@ -24,7 +24,7 @@ import java.util.TimerTask; /** * 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 { private FoldersAdapter mAdapter; @@ -40,7 +40,7 @@ public class FolderListFragment extends ListFragment implements SyncthingService } @Override - public void onApiChange(SyncthingService.State currentState) { + public void onServiceStateChange(SyncthingService.State currentState) { if (currentState != SyncthingService.State.ACTIVE) return; diff --git a/app/src/main/java/com/nutomic/syncthingandroid/http/ApiRequest.java b/app/src/main/java/com/nutomic/syncthingandroid/http/ApiRequest.java index fdce9cf7..9c54dcf5 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/http/ApiRequest.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/http/ApiRequest.java @@ -94,13 +94,15 @@ public abstract class ApiRequest { @Nullable OnSuccessListener listener, @Nullable OnErrorListener errorListener) { Log.v(TAG, "Performing request to " + uri.toString()); StringRequest request = new StringRequest(requestMethod, uri.toString(), reply -> { - if (listener != null) + if (listener != null) { listener.onSuccess(reply); + } }, error -> { - if (errorListener != null) + if (errorListener != null) { errorListener.onError(error); - else - Log.w(TAG, "Request to " + uri + " failed", error); + } else { + Log.w(TAG, "Request to " + uri + " failed, " + error.getMessage()); + } }) { @Override public Map getHeaders() throws AuthFailureError { diff --git a/app/src/main/java/com/nutomic/syncthingandroid/http/PollWebGuiAvailableTask.java b/app/src/main/java/com/nutomic/syncthingandroid/http/PollWebGuiAvailableTask.java index 814e4d53..554007f1 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/http/PollWebGuiAvailableTask.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/http/PollWebGuiAvailableTask.java @@ -25,9 +25,15 @@ public class PollWebGuiAvailableTask extends ApiRequest { */ private static final long WEB_GUI_POLL_INTERVAL = 100; - private final OnSuccessListener mListener; 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, OnSuccessListener listener) { super(context, url, "", apiKey); @@ -36,14 +42,36 @@ public class PollWebGuiAvailableTask extends ApiRequest { performRequest(); } + public void cancelRequestsAndCallback() { + synchronized(mListenerLock) { + mListener = null; + } + } + private void performRequest() { 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) { - 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(); if (cause == null || cause.getClass().equals(ConnectException.class)) { Log.v(TAG, "Polling web gui"); diff --git a/app/src/main/java/com/nutomic/syncthingandroid/receiver/NetworkReceiver.java b/app/src/main/java/com/nutomic/syncthingandroid/receiver/NetworkReceiver.java index 91fc46ba..9b772277 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/receiver/NetworkReceiver.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/receiver/NetworkReceiver.java @@ -8,6 +8,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Build; import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; import com.nutomic.syncthingandroid.service.DeviceStateHolder; import com.nutomic.syncthingandroid.service.SyncthingService; @@ -17,6 +18,8 @@ import com.nutomic.syncthingandroid.service.SyncthingService; */ public class NetworkReceiver extends BroadcastReceiver { + private static final String TAG = "NetworkReceiver"; + @Override public void onReceive(Context context, Intent intent) { if (!ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) @@ -33,14 +36,21 @@ public class NetworkReceiver extends BroadcastReceiver { ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo ni = cm.getActiveNetworkInfo(); - boolean isOffline = ni == null; - boolean isWifi = ni != null && ni.getType() == ConnectivityManager.TYPE_WIFI && ni.isConnected(); - boolean isNetworkMetered = (Build.VERSION.SDK_INT >= 16) ? cm.isActiveNetworkMetered() : false; - boolean isAllowedConnection = isOffline || (isWifi && !isNetworkMetered); + boolean isAllowedConnectionType = false; + if (ni == null) { + Log.v(TAG, "In flight mode"); + // 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); 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); } diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/DeviceStateHolder.java b/app/src/main/java/com/nutomic/syncthingandroid/service/DeviceStateHolder.java index e2f33446..a9d384dd 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/DeviceStateHolder.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/DeviceStateHolder.java @@ -20,6 +20,7 @@ import com.nutomic.syncthingandroid.SyncthingApp; import com.nutomic.syncthingandroid.receiver.BatteryReceiver; import com.nutomic.syncthingandroid.receiver.NetworkReceiver; import com.nutomic.syncthingandroid.receiver.PowerSaveModeChangedReceiver; +import com.nutomic.syncthingandroid.service.ReceiverManager; import java.util.HashSet; import java.util.List; @@ -70,19 +71,26 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh private final Context mContext; private final LocalBroadcastManager mBroadcastManager; - private final DeviceStateChangedReceiver mReceiver = new DeviceStateChangedReceiver(); - private final OnDeviceStateChangedListener mListener; @Inject SharedPreferences mPreferences; - private @Nullable NetworkReceiver mNetworkReceiver = null; - private @Nullable BatteryReceiver mBatteryReceiver = null; - private @Nullable BroadcastReceiver mPowerSaveModeChangedReceiver = null; + private @Nullable DeviceStateChangedReceiver mDeviceStateChangedReceiver = null; + private ReceiverManager mReceiverManager; - 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 boolean mIsCharging; 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}. */ @@ -92,21 +100,22 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh Log.v(TAG, "Created new instance"); ((SyncthingApp) context.getApplicationContext()).component().inject(this); mContext = context; - mBroadcastManager = LocalBroadcastManager.getInstance(mContext); - mBroadcastManager.registerReceiver(mReceiver, new IntentFilter(ACTION_DEVICE_STATE_CHANGED)); mPreferences.registerOnSharedPreferenceChangeListener(this); - mListener = listener; - updateReceivers(); + mOnDeviceStateChangedListener = listener; + + mDeviceStateChangedReceiver = new DeviceStateChangedReceiver(); + mBroadcastManager = LocalBroadcastManager.getInstance(mContext); + mBroadcastManager.registerReceiver(mDeviceStateChangedReceiver, new IntentFilter(ACTION_DEVICE_STATE_CHANGED)); + registerChildReceivers(); } public void shutdown() { Log.v(TAG, "Shutting down"); - mBroadcastManager.unregisterReceiver(mReceiver); mPreferences.unregisterOnSharedPreferenceChangeListener(this); - - unregisterReceiver(mNetworkReceiver); - unregisterReceiver(mBatteryReceiver); - unregisterReceiver(mPowerSaveModeChangedReceiver); + mReceiverManager.unregisterAllReceivers(mContext); + if (mDeviceStateChangedReceiver != null) { + mBroadcastManager.unregisterReceiver(mDeviceStateChangedReceiver); + } } @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_SSIDS); 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)) { - Log.i(TAG, "Listening for network state changes"); + Log.i(TAG, "Creating NetworkReceiver"); NetworkReceiver.updateNetworkStatus(mContext); mNetworkReceiver = new NetworkReceiver(); - mContext.registerReceiver(mNetworkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); - } else { - Log.i(TAG, "Stopped listening to network state changes"); - unregisterReceiver(mNetworkReceiver); - mNetworkReceiver = null; + ReceiverManager.registerReceiver(mContext, mNetworkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + incomingBroadcastEventsExpected = true; } 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); mBatteryReceiver = new BatteryReceiver(); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_POWER_CONNECTED); filter.addAction(Intent.ACTION_POWER_DISCONNECTED); - mContext.registerReceiver(mBatteryReceiver, filter); - } else { - Log.i(TAG, "Stopped listening to battery state changes"); - unregisterReceiver(mBatteryReceiver); - mBatteryReceiver = null; + ReceiverManager.registerReceiver(mContext, mBatteryReceiver, filter); + incomingBroadcastEventsExpected = true; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && - mPreferences.getBoolean("respect_battery_saving", true)) { - Log.i(TAG, "Listening to power saving changes"); + mPreferences.getBoolean(Constants.PREF_RESPECT_BATTERY_SAVING, true)) { + Log.i(TAG, "Creating PowerSaveModeChangedReceiver"); PowerSaveModeChangedReceiver.updatePowerSavingState(mContext); mPowerSaveModeChangedReceiver = new PowerSaveModeChangedReceiver(); - mContext.registerReceiver(mPowerSaveModeChangedReceiver, + ReceiverManager.registerReceiver(mContext, mPowerSaveModeChangedReceiver, new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)); - } else { - Log.i(TAG, "Stopped listening to power saving changes"); - unregisterReceiver(mPowerSaveModeChangedReceiver); - mPowerSaveModeChangedReceiver = null; + incomingBroadcastEventsExpected = true; } - } - private void unregisterReceiver(BroadcastReceiver receiver) { - if (receiver != null) - mContext.unregisterReceiver(receiver); + // If no broadcast messages can be received as we didn't register an emitter, + // force an initial decision to be made. + if (!incomingBroadcastEventsExpected) { + updateShouldRunDecision(); + } } private class DeviceStateChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - mIsAllowedNetworkConnection = - intent.getBooleanExtra(EXTRA_IS_ALLOWED_NETWORK_CONNECTION, mIsAllowedNetworkConnection); + mIsAllowedConnectionType = + intent.getBooleanExtra(EXTRA_IS_ALLOWED_NETWORK_CONNECTION, mIsAllowedConnectionType); mIsCharging = intent.getBooleanExtra(EXTRA_IS_CHARGING, mIsCharging); mIsPowerSaving = intent.getBooleanExtra(EXTRA_IS_POWER_SAVING, mIsPowerSaving); - Log.i(TAG, "State updated, allowed network connection: " + mIsAllowedNetworkConnection + - ", charging: " + mIsCharging + ", power saving: " + mIsPowerSaving); updateShouldRunDecision(); } } @@ -193,7 +195,9 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh // compared to the last determined result. boolean newShouldRun = decideShouldRun(); if (newShouldRun != lastDeterminedShouldRun) { - mListener.onDeviceStateChanged(newShouldRun); + if (mOnDeviceStateChangedListener != null) { + mOnDeviceStateChangedListener.onDeviceStateChanged(newShouldRun); + } lastDeterminedShouldRun = newShouldRun; } } @@ -202,7 +206,10 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh * Determines if Syncthing should currently run. */ 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) return false; @@ -211,7 +218,7 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh boolean prefStopNotCharging = mPreferences.getBoolean(Constants.PREF_SYNC_ONLY_CHARGING, false); updateWifiSsid(); - if (prefStopMobileData && !isWhitelistedNetworkConnection()) + if (prefStopMobileData && !isWhitelistedWifiConnection()) return false; if (prefStopNotCharging && !mIsCharging) @@ -221,8 +228,8 @@ public class DeviceStateHolder implements SharedPreferences.OnSharedPreferenceCh return true; } - private boolean isWhitelistedNetworkConnection() { - boolean wifiConnected = mIsAllowedNetworkConnection; + private boolean isWhitelistedWifiConnection() { + boolean wifiConnected = mIsAllowedConnectionType; if (wifiConnected) { Set ssids = mPreferences.getStringSet(Constants.PREF_SYNC_ONLY_WIFI_SSIDS, new HashSet<>()); if (ssids.isEmpty()) { diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java b/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java index ba3a1298..ba8acff7 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/EventProcessor.java @@ -33,8 +33,7 @@ import javax.inject.Inject; * * It uses {@link RestApi#getEvents} to read the pending events and wait for new events. */ -public class EventProcessor implements SyncthingService.OnWebGuiAvailableListener, Runnable, - RestApi.OnReceiveEventListener { +public class EventProcessor implements Runnable, RestApi.OnReceiveEventListener { private static final String TAG = "EventProcessor"; private static final String PREF_LAST_SYNC_ID = "last_sync_id"; @@ -67,7 +66,7 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene @Override 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) { mLastEventId = mPreferences.getLong(PREF_LAST_SYNC_ID, 0); } @@ -223,9 +222,8 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene } } - @Override - public void onWebGuiAvailable() { - Log.d(TAG, "WebGUI available. Starting event processor."); + public void start() { + Log.d(TAG, "Starting event processor."); // Remove all pending callbacks and add a new one. This makes sure that only one // event poller is running at any given time. @@ -236,8 +234,8 @@ public class EventProcessor implements SyncthingService.OnWebGuiAvailableListene } } - public void shutdown() { - Log.d(TAG, "Shutdown event processor."); + public void stop() { + Log.d(TAG, "Stopping event processor."); synchronized (mMainThreadHandler) { mShutdown = true; mMainThreadHandler.removeCallbacks(this); diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/NotificationHandler.java b/app/src/main/java/com/nutomic/syncthingandroid/service/NotificationHandler.java index 8ea69a81..ee2f703d 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/NotificationHandler.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/NotificationHandler.java @@ -16,6 +16,7 @@ import com.nutomic.syncthingandroid.SyncthingApp; import com.nutomic.syncthingandroid.activities.FirstStartActivity; import com.nutomic.syncthingandroid.activities.LogActivity; import com.nutomic.syncthingandroid.activities.MainActivity; +import com.nutomic.syncthingandroid.service.SyncthingService.State; import javax.inject.Inject; @@ -101,14 +102,32 @@ public class NotificationHandler { type = "low_priority"; } - boolean syncthingRunning = service.getCurrentState() == SyncthingService.State.ACTIVE || - service.getCurrentState() == SyncthingService.State.STARTING; + State currentServiceState = service.getCurrentState(); + boolean syncthingRunning = currentServiceState == SyncthingService.State.ACTIVE || + currentServiceState == SyncthingService.State.STARTING; if (foreground || (syncthingRunning && !type.equals("none"))) { - // Launch FirstStartActivity instead of MainActivity so we can request permission if - // necessary. - PendingIntent pi = PendingIntent.getActivity(mContext, 0, - new Intent(mContext, FirstStartActivity.class), 0); - int title = syncthingRunning ? R.string.syncthing_active : R.string.syncthing_disabled; + int title = R.string.syncthing_terminated; + switch (currentServiceState) { + case ERROR: + case INIT: + 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 // the startForeground() below won't update the notification but use the old one int idToShow = syncthingRunning ? ID_PERSISTENT : ID_PERSISTENT_WAITING; @@ -119,7 +138,7 @@ public class NotificationHandler { .setSmallIcon(R.drawable.ic_stat_notify) .setOngoing(true) .setOnlyAlertOnce(true) - .setContentIntent(pi); + .setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0)); if (type.equals("low_priority")) builder.setPriority(NotificationCompat.PRIORITY_MIN); diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/ReceiverManager.java b/app/src/main/java/com/nutomic/syncthingandroid/service/ReceiverManager.java new file mode 100644 index 00000000..0ed0679b --- /dev/null +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/ReceiverManager.java @@ -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 mReceivers = new ArrayList(); + + 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 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(); + } + } + } +} diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java b/app/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java index 8f3f49cc..a130655d 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/RestApi.java @@ -52,7 +52,7 @@ import javax.inject.Inject; /** * Provides functions to interact with the syncthing REST API. */ -public class RestApi implements SyncthingService.OnWebGuiAvailableListener { +public class RestApi { private static final String TAG = "RestApi"; @@ -97,6 +97,24 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { */ 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 */ @@ -119,16 +137,6 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { 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 { void onApiAvailable(); } @@ -140,26 +148,46 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { /** * Gets local device ID, syncthing version and config, then calls all OnApiAvailableListeners. */ - @Override - public void onWebGuiAvailable() { - mAvailableCount.set(0); + public void readConfigFromRestApi() { + Log.v(TAG, "Reading config from REST ..."); + synchronized (mAsyncQueryCompleteLock) { + asyncQueryVersionComplete = false; + asyncQueryConfigComplete = false; + asyncQuerySystemInfoComplete = false; + } new GetRequest(mContext, mUrl, GetRequest.URI_VERSION, mApiKey, null, result -> { JsonObject json = new JsonParser().parse(result).getAsJsonObject(); mVersion = json.get("version").getAsString(); Log.i(TAG, "Syncthing version is " + mVersion); - tryIsAvailable(); updateDebugFacilitiesCache(); + synchronized (mAsyncQueryCompleteLock) { + asyncQueryVersionComplete = true; + checkReadConfigFromRestApiCompleted(); + } }); new GetRequest(mContext, mUrl, GetRequest.URI_CONFIG, mApiKey, null, result -> { onReloadConfigComplete(result); - tryIsAvailable(); + synchronized (mAsyncQueryCompleteLock) { + asyncQueryConfigComplete = true; + checkReadConfigFromRestApiCompleted(); + } }); getSystemInfo(info -> { 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() { 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. * Will result in a "ConfigSaved" event. @@ -311,12 +325,17 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { } public Device getLocalDevice() { - for (Device d : getDevices(true)) { + List 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)) { 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 errorListener) { diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingRunnable.java b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingRunnable.java index b121516d..81b5e029 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingRunnable.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingRunnable.java @@ -263,48 +263,43 @@ public class SyncthingRunnable implements Runnable { * Look for a running libsyncthing.so process and nice its IO. */ private void niceSyncthing() { - new Thread() { - public void run() { - Process nice = null; - DataOutputStream niceOut = null; - int ret = 1; - try { - Thread.sleep(1000); // Wait a second before getting the pid - List syncthingPIDs = getSyncthingPIDs(); - if (syncthingPIDs.isEmpty()) { - Log.w(TAG, "niceSyncthing: Found no running instances of " + Constants.FILENAME_SYNCTHING_BINARY); - } else { - nice = Runtime.getRuntime().exec((mUseRoot) ? "su" : "sh"); - niceOut = new DataOutputStream(nice.getOutputStream()); - 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)); - } + Process nice = null; + DataOutputStream niceOut = null; + int ret = 1; + try { + List syncthingPIDs = getSyncthingPIDs(); + if (syncthingPIDs.isEmpty()) { + Log.w(TAG, "niceSyncthing: Found no running instances of " + Constants.FILENAME_SYNCTHING_BINARY); + } else { + nice = Runtime.getRuntime().exec((mUseRoot) ? "su" : "sh"); + niceOut = new DataOutputStream(nice.getOutputStream()); + for (String syncthingPID : syncthingPIDs) { + // Set best-effort, low priority using ionice. + niceOut.writeBytes("/system/bin/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); } - }.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 { @@ -314,21 +309,18 @@ public class SyncthingRunnable implements Runnable { * Look for running libsyncthing.so processes and kill them. * Try a SIGINT first, then try again with SIGKILL. */ - public void killSyncthing(OnSyncthingKilled onKilledListener) { - new Thread(() -> { - for (int i = 0; i < 2; i++) { - List syncthingPIDs = getSyncthingPIDs(); - if (syncthingPIDs.isEmpty()) { - Log.d(TAG, "killSyncthing: Found no more running instances of " + Constants.FILENAME_SYNCTHING_BINARY); - break; - } - - for (String syncthingPID : syncthingPIDs) { - killProcessId(syncthingPID, i > 0); - } + public void killSyncthing() { + for (int i = 0; i < 2; i++) { + List syncthingPIDs = getSyncthingPIDs(); + if (syncthingPIDs.isEmpty()) { + Log.d(TAG, "killSyncthing: Found no more running instances of " + Constants.FILENAME_SYNCTHING_BINARY); + break; } - onKilledListener.onKilled(); - }).start(); + + for (String syncthingPID : syncthingPIDs) { + killProcessId(syncthingPID, i > 0); + } + } } /** diff --git a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java index bd904719..f30d4e32 100644 --- a/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java +++ b/app/src/main/java/com/nutomic/syncthingandroid/service/SyncthingService.java @@ -59,18 +59,8 @@ public class SyncthingService extends Service { public static final String ACTION_REFRESH_NETWORK_INFO = "com.nutomic.syncthingandroid.service.SyncthingService.REFRESH_NETWORK_INFO"; - /** - * Callback for when the Syncthing web interface becomes first available after service start. - */ - public interface OnWebGuiAvailableListener { - void onWebGuiAvailable(); - } - - private final HashSet mOnWebGuiAvailableListeners = - new HashSet<>(); - - public interface OnApiChangeListener { - void onApiChange(State currentState); + public interface OnServiceStateChangeListener { + void onServiceStateChange(State currentState); } /** @@ -81,40 +71,54 @@ public class SyncthingService extends Service { INIT, /** Syncthing binary is 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, - /** Syncthing is stopped according to user preferences. */ + /** Syncthing binary is shutting down. */ DISABLED, /** There is some problem that prevents Syncthing from running. */ 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 RestApi mApi; - private EventProcessor mEventProcessor; + private @Nullable PollWebGuiAvailableTask mPollWebGuiAvailableTask = null; + private @Nullable RestApi mApi = null; + private @Nullable EventProcessor mEventProcessor = null; private @Nullable DeviceStateHolder mDeviceStateHolder = null; - private SyncthingRunnable mSyncthingRunnable; + private @Nullable SyncthingRunnable mSyncthingRunnable = null; + private Thread mSyncthingRunnableThread = null; private Handler mHandler; - private final HashSet mOnApiChangeListeners = new HashSet<>(); + private final HashSet mOnServiceStateChangeListeners = new HashSet<>(); private final SyncthingServiceBinder mBinder = new SyncthingServiceBinder(this); @Inject NotificationHandler mNotificationHandler; @Inject SharedPreferences mPreferences; /** - * Object that can be locked upon when accessing mCurrentState - * Currently used to male onDestroy() and PollWebGuiAvailableTaskImpl.onPostExcecute() tread-safe + * Object that must be locked upon accessing mCurrentState */ private final Object mStateLock = new Object(); /** - * True if a stop was requested while syncthing is starting, in that case, perform stop in - * {@link #pollWebGui}. + * Stores the result of the last should run decision received by OnDeviceStateChangedListener. */ - 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. @@ -126,6 +130,7 @@ public class SyncthingService extends Service { */ @Override public void onCreate() { + Log.v(TAG, "onCreate"); super.onCreate(); PRNGFixes.apply(); ((SyncthingApp) getApplication()).component().inject(this); @@ -148,6 +153,7 @@ public class SyncthingService extends Service { */ @Override public int onStartCommand(Intent intent, int flags, int startId) { + Log.v(TAG, "onStartCommand"); if (!mStoragePermissionGranted) { Log.e(TAG, "User revoked storage permission. Stopping service."); if (mNotificationHandler != null) { @@ -157,7 +163,27 @@ public class SyncthingService extends Service { 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); if (intent == null) @@ -185,35 +211,41 @@ public class SyncthingService extends Service { * 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. + * {@link #onServiceStateChange} is called while applying the decision change. */ - 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; - } - } else { - // Stop syncthing. - if (mCurrentState == State.DISABLED) - return; + private void onUpdatedShouldRunDecision(boolean newShouldRunDecision) { + if (newShouldRunDecision != mLastDeterminedShouldRun) { + Log.i(TAG, "shouldRun decision changed to " + newShouldRunDecision + " according to configured run conditions."); + mLastDeterminedShouldRun = newShouldRunDecision; - Log.i(TAG, "Stopping syncthing according to current state and preferences"); - shutdown(State.DISABLED, () -> {}); + // React to the shouldRun condition change. + 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 { - public StartupTask() { - onApiChange(State.STARTING); + @Override + 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 @@ -234,7 +274,9 @@ public class SyncthingService extends Service { mConfig.updateIfNeeded(); } catch (ConfigXml.OpenConfigException e) { mNotificationHandler.showCrashedNotification(R.string.config_create_failed, true); - onApiChange(State.ERROR); + synchronized (mStateLock) { + onServiceStateChange(State.ERROR); + } cancel(true); } return null; @@ -242,26 +284,77 @@ public class SyncthingService extends Service { @Override protected void onPostExecute(Void aVoid) { - mApi = new RestApi(SyncthingService.this, mConfig.getWebGuiUrl(), mConfig.getApiKey(), - SyncthingService.this::onSyncthingStarted, () -> onApiChange(mCurrentState)); + if (mApi == null) { + 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); - - if (mApi != null) - registerOnWebGuiAvailableListener(mApi); - if (mEventProcessor != null) - registerOnWebGuiAvailableListener(mEventProcessor); - Log.i(TAG, "Web GUI will be available at " + mConfig.getWebGuiUrl()); - - pollWebGui(); + // Start the syncthing binary. + if (mSyncthingRunnable != null || mSyncthingRunnableThread != null) { + Log.e(TAG, "StartupTask/onPostExecute: Syncthing binary lifecycle violated"); + return; + } 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); - Log.i(TAG, "onSyncthingStarted(): State.ACTIVE reached."); + /** + * Called when {@link PollWebGuiAvailableTask} confirmed the REST API is available. + * 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 @@ -271,18 +364,23 @@ public class SyncthingService extends Service { /** * Stops the native binary. - * - * 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. + * Shuts down DeviceStateHolder instance. */ @Override 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) { synchronized (mStateLock) { - if (mCurrentState == State.INIT || mCurrentState == State.STARTING) { + if (mCurrentState == State.STARTING) { Log.i(TAG, "Delay shutting down synchting binary until initialisation finished"); - mStopScheduled = true; + mDestroyScheduled = true; } else { Log.i(TAG, "Shutting down syncthing binary immediately"); shutdown(State.DISABLED, () -> {}); @@ -294,10 +392,7 @@ public class SyncthingService extends Service { Log.i(TAG, "Shutting down syncthing binary due to missing storage permission."); shutdown(State.DISABLED, () -> {}); } - - if (mDeviceStateHolder != null) { - mDeviceStateHolder.shutdown(); - } + super.onDestroy(); } /** @@ -307,37 +402,44 @@ public class SyncthingService extends Service { */ private void shutdown(State newState, SyncthingRunnable.OnSyncthingKilled onKilledListener) { Log.i(TAG, "Shutting down background service"); - onApiChange(newState); + synchronized(mStateLock) { + onServiceStateChange(newState); + } - if (mEventProcessor != null) - mEventProcessor.shutdown(); + if (mPollWebGuiAvailableTask != null) { + mPollWebGuiAvailableTask.cancelRequestsAndCallback(); + mPollWebGuiAvailableTask = null; + } - if (mApi != null) + if (mEventProcessor != null) { + mEventProcessor.stop(); + mEventProcessor = null; + } + + if (mApi != null) { mApi.shutdown(); + mApi = null; + } - if (mNotificationHandler != null) + if (mNotificationHandler != null) { mNotificationHandler.cancelPersistentNotification(this); + } 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; - } 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() { @@ -350,60 +452,37 @@ public class SyncthingService extends Service { * The listener is called immediately with the current state, and again whenever the state * 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 // when it's just starting up. - listener.onApiChange(mCurrentState); - mOnApiChangeListeners.add(listener); + listener.onServiceStateChange(mCurrentState); + mOnServiceStateChangeListeners.add(listener); } /** * Unregisters a previously registered listener. * - * @see #registerOnApiChangeListener + * @see #registerOnServiceStateChangeListener */ - public void unregisterOnApiChangeListener(OnApiChangeListener listener) { - mOnApiChangeListeners.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(); - }); + public void unregisterOnServiceStateChangeListener(OnServiceStateChangeListener listener) { + mOnServiceStateChangeListeners.remove(listener); } /** * 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(() -> { - mCurrentState = newState; mNotificationHandler.updatePersistentNotification(this); - for (Iterator i = mOnApiChangeListeners.iterator(); + for (Iterator i = mOnServiceStateChangeListeners.iterator(); i.hasNext(); ) { - OnApiChangeListener listener = i.next(); + OnServiceStateChangeListener listener = i.next(); if (listener != null) { - listener.onApiChange(mCurrentState); + listener.onServiceStateChange(mCurrentState); } else { i.remove(); }