mirror of
https://github.com/syncthing/syncthing-android.git
synced 2024-12-23 03:11:30 +00:00
Refactor SyncthingService (lifecycle), DeviceStateHolder, RestApi, multiple fixes (#1119)
This commit is contained in:
parent
165c136bea
commit
e9eef4332b
22 changed files with 625 additions and 381 deletions
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<Device> 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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<String, String> getHeaders() throws AuthFailureError {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String> ssids = mPreferences.getStringSet(Constants.PREF_SYNC_ONLY_WIFI_SSIDS, new HashSet<>());
|
||||
if (ssids.isEmpty()) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<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)) {
|
||||
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) {
|
||||
|
|
|
@ -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<String> 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<String> 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<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<OnWebGuiAvailableListener> 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<OnApiChangeListener> mOnApiChangeListeners = new HashSet<>();
|
||||
private final HashSet<OnServiceStateChangeListener> 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<Void, Void, Void> {
|
||||
|
||||
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<OnApiChangeListener> i = mOnApiChangeListeners.iterator();
|
||||
for (Iterator<OnServiceStateChangeListener> 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();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue