/* * Copyright (c) 2015 OpenSilk Productions LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package syncthing.android.service; import android.content.Intent; import android.content.pm.PackageManager; import android.database.ContentObserver; import android.media.MediaScannerConnection; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.wifi.WifiManager; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.provider.MediaStore; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.opensilk.common.core.mortar.DaggerService; import org.opensilk.common.core.mortar.MortarService; import org.opensilk.common.core.util.VersionUtils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import mortar.MortarScope; import rx.Subscription; import rx.functions.Action1; import rx.schedulers.Schedulers; import syncthing.android.BuildConfig; import syncthing.api.Session; import syncthing.api.SessionController; import syncthing.api.SessionManager; import syncthing.api.SynchingApiWrapper; import syncthing.api.SyncthingApi; import syncthing.api.SyncthingApiConfig; import syncthing.api.model.FolderConfig; import syncthing.api.model.Ok; import syncthing.api.model.event.ItemFinished; import timber.log.Timber; /** * Created by drew on 3/8/15. */ public class SyncthingInstance extends MortarService { static final String PACKAGE = BuildConfig.APPLICATION_ID; //activity is informing us it was opened or closed public static final String FOREGROUND_STATE_CHANGED = PACKAGE + ".action.fgstatechanged"; public static final String EXTRA_NOW_IN_FOREGROUND = PACKAGE + ".extra.nowinforeground"; //binary exited with restart status static final String BINARY_NEED_RESTART = PACKAGE + ".action.binaryneedrestart"; //binary exited with clean shutdown status static final String BINARY_WAS_SHUTDOWN = PACKAGE + ".action.binarywasshutdown"; //binary exited with unhandled exit code static final String BINARY_DIED = PACKAGE + ".action.binarydied"; //Reload settings public static final String REEVALUATE = PACKAGE + ".action.reevaluate"; //shutdown service public static final String SHUTDOWN = PACKAGE + ".action.shutdown"; //recieved by alarmmanager static final String SCHEDULED_SHUTDOWN = PACKAGE + ".action.scheduledshutdown"; static final String SCHEDULED_WAKEUP = PACKAGE + "action.wakeup"; @Inject ServiceSettings mSettings; @Inject NotificationHelper mNotificationHelper; @Inject AlarmManagerHelper mAlarmManagerHelper; @Inject SessionManager mSessionManager; @Inject WifiManager mWifiManager; @Inject ConnectivityManager mConnectivityManager; SyncthingThread mSyncthingThread; SyncthingInotifyThread mSyncthingInotifyThread; WifiManager.WifiLock mWifiLock; ContentObserver initializedObserver; Session mSession; final SessionHelper mSessionHelper = new SessionHelper(); boolean mAnyActivityInForeground; boolean wasShutdown; static class SessionHelper { Subscription eventSubscripion; void release() { if (eventSubscripion != null) { eventSubscripion.unsubscribe(); } } } @Override protected void onBuildScope(MortarScope.Builder builder) { ServiceComponent component = DaggerService.getDaggerComponent(getApplicationContext()); builder.withService(DaggerService.DAGGER_SERVICE, SyncthingInstanceComponent.FACTORY.call(component, this)); } @Override public void onCreate() { super.onCreate(); Timber.d("onCreate"); ensureBinaries(); DaggerService.<SyncthingInstanceComponent>getDaggerComponent(this).inject(this); mSettings.setCached(true); } @Override public void onDestroy() { super.onDestroy(); Timber.d("onDestroy"); ensureSyncthingKilled(); mAlarmManagerHelper.cancelDelayedShutdown(); mSettings.release(); if (initializedObserver != null) { getContentResolver().unregisterContentObserver(initializedObserver); } releaseSession(); releaseWifiLock(); } @Override public IBinder onBind(Intent intent) { throw new UnsupportedOperationException("service not bindable"); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Timber.d("onStartCommand %s", intent); if (intent != null) { String action = intent.getAction(); if (intent.hasExtra(EXTRA_NOW_IN_FOREGROUND)) { mAnyActivityInForeground = intent.getBooleanExtra(EXTRA_NOW_IN_FOREGROUND, false); if (!mAnyActivityInForeground && wasShutdown) { doOrderlyShutdown(); return START_NOT_STICKY; } } else { wasShutdown = false; } if (SHUTDOWN.equals(action) || SCHEDULED_SHUTDOWN.equals(action)) { if (SCHEDULED_SHUTDOWN.equals(action)) { mAlarmManagerHelper.onReceivedDelayedShutdown(); } doOrderlyShutdown(); return START_NOT_STICKY; } switch (action) { case BINARY_DIED: tryKillingRougeInstance(); doOrderlyShutdown(); mNotificationHelper.showError(); break; case BINARY_WAS_SHUTDOWN: doOrderlyShutdown(); break; case BINARY_NEED_RESTART: safeStartSyncthing(); updateForegroundState(); break; case REEVALUATE: case SCHEDULED_WAKEUP: default: reevaluate(); break; } } else { //System restarted us reevaluate(); } return START_STICKY; } void reevaluate() { if (mAnyActivityInForeground) { //when in foreground we only care about disabled status and //connection override, we will assume that since the user //opened the app they wish for the server to start if (mSettings.isDisabled() || !mSettings.hasSuitableConnection()) { ensureSyncthingKilled(); } else { maybeStartSyncthing(); mAlarmManagerHelper.cancelDelayedShutdown(); } } else { //in background if (mSettings.isAllowedToRun()) { maybeStartSyncthing(); //as you were if (mSettings.isOnSchedule()) { mAlarmManagerHelper.scheduleDelayedShutdown(); mAlarmManagerHelper.scheduleWakeup(); } else /*always run*/ { mAlarmManagerHelper.cancelDelayedShutdown(); } if (isConnectedToWifi()) { acquireWifiLock(); } else { releaseWifiLock(); } } else { ensureSyncthingKilled(); //dont shutdown right away in case circumstances change mAlarmManagerHelper.scheduleDelayedShutdown(); if (mSettings.isOnSchedule()) { mAlarmManagerHelper.scheduleWakeup(); } releaseWifiLock(); } } updateForegroundState(); } void doOrderlyShutdown() { wasShutdown = true; ensureSyncthingKilled(); mNotificationHelper.killNotification(); //always stick around while activity is running if (!mAnyActivityInForeground) { stopSelf(); } } /* * Notification helpers */ void updateForegroundState() { if (isSyncthingRunning()) { //show if server running mNotificationHelper.buildNotification(); } else { //server not running dont show mNotificationHelper.killNotification(); } } /* * Syncthing Helpers */ boolean isSyncthingRunning() { return mSyncthingThread != null && mSyncthingThread.isAlive(); } void safeStartSyncthing() { ensureSyncthingKilled(); startSyncthing(); } void startSyncthing() { mSyncthingThread = new SyncthingThread(this); mSyncthingThread.start(); if (mSettings.isInitialised()) { startInotify(); } else if (initializedObserver == null) { //register listener and wait for notify initializedObserver = new InitializedListener(this); getContentResolver().registerContentObserver( mSettings.getInitializedUri(), false, initializedObserver); } //else already listening } void maybeStartSyncthing() { if (!isSyncthingRunning()) { safeStartSyncthing(); } } void ensureSyncthingKilled() { releaseSession(); ensureInotifyKilled(); if (mSyncthingThread != null) { mSyncthingThread.kill(); mSyncthingThread = null; } } boolean isInotifyRunning() { return mSyncthingInotifyThread != null && mSyncthingInotifyThread.isAlive(); } void safeStartInotify() { ensureInotifyKilled(); startInotify(); } void startInotify() { mSyncthingInotifyThread = new SyncthingInotifyThread(this); mSyncthingInotifyThread.start(); startMonitor(); } void maybeStartInotify() { if (isSyncthingRunning() && !isInotifyRunning()) { safeStartInotify(); } } void ensureInotifyKilled() { if (mSyncthingInotifyThread != null) { mSyncthingInotifyThread.kill(); mSyncthingInotifyThread = null; } } void releaseSession() { if (mSession != null) { mSessionManager.release(mSession); } mSessionHelper.release(); } void acquireSession() { SyncthingApiConfig.Builder bob = SyncthingApiConfig.builder(); ConfigXml config = ConfigXml.get(this); //noinspection ConstantConditions bob.setUrl(config.getUrl()); bob.setApiKey(config.getApiKey()); bob.setCaCert(SyncthingUtils.getSyncthingCACert(this)); releaseSession(); mSession = mSessionManager.acquire(bob.build()); } void startMonitor() { acquireSession(); final SessionController controller = mSession.controller(); controller.init(); mSessionHelper.eventSubscripion = controller.subscribeChanges(new Action1<SessionController.ChangeEvent>() { @Override public void call(SessionController.ChangeEvent changeEvent) { switch (changeEvent.change) { case ONLINE: { break; } case ITEM_FINISHED: { ItemFinished.Data data = (ItemFinished.Data) changeEvent.data; switch (data.action) { case UPDATE: { FolderConfig folder = controller.getFolder(data.folder); if (folder != null) { File file = new File(folder.path, data.item); Timber.d("Item finished update for %s", file.getAbsolutePath()); if (file.exists()) { MediaScannerConnection.scanFile(SyncthingInstance.this, new String[]{file.getAbsolutePath()}, null, null); } } break; } case DELETE: { FolderConfig folder = controller.getFolder(data.folder); if (folder != null) { File file = new File(folder.path, data.item); Timber.d("Item finished delete for %s", file.getAbsolutePath()); if (!file.exists()) { final String where = MediaStore.Files.FileColumns.DATA + "=?"; int count = getContentResolver().delete(MediaStore.Files.getContentUri("external"), where, new String[] {file.getAbsolutePath()}); Timber.i("Removed %d items from mediastore for path %s", count, file.getAbsolutePath()); } } break; } } break; } } } }, SessionController.Change.ONLINE, SessionController.Change.ITEM_FINISHED); } void tryKillingRougeInstance() { acquireSession(); try { Ok ok = SynchingApiWrapper.wrap(mSession.api(), Schedulers.newThread()) .shutdown().timeout(2, TimeUnit.SECONDS).toBlocking().first(); } catch (RuntimeException ignored) { } } public ServiceSettings getSettings() { return mSettings; } boolean isConnectedToWifi() { NetworkInfo networkInfo = mConnectivityManager.getActiveNetworkInfo(); return networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIMAX; } void acquireWifiLock() { releaseWifiLock(); mWifiLock = mWifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "SyncthingInstance"); mWifiLock.acquire(); } void releaseWifiLock() { if (mWifiLock != null && mWifiLock.isHeld()) { mWifiLock.release(); mWifiLock = null; } } /* * Initial setup */ void ensureBinaries() { final String[] abis; if (VersionUtils.hasLollipop()) { abis = Build.SUPPORTED_ABIS; } else { abis = new String[]{Build.CPU_ABI, Build.CPU_ABI2}; } String ext = null; for (String abi: abis) { if (StringUtils.equals(abi, "x86_64")) { Timber.i("Found abi %s", abi); ext = "amd64"; break; } else if (StringUtils.equals(abi, "x86")) { Timber.i("Found abi %s", abi); ext = "386"; break; } else if (StringUtils.equals(abi, "armeabi-v7a")) { Timber.i("Found abi %s", abi); ext = "arm"; break; } } if (ext == null) throw new RuntimeException("Unable to find supported arch in " + Arrays.toString(abis)); ensureBinary("syncthing" + "." + ext, SyncthingUtils.getSyncthingBinaryPath(this)); ensureBinary("syncthing-inotify" + "." + ext, SyncthingUtils.getSyncthingInotifyBinaryPath(this)); } // From camlistore void ensureBinary(String asset, String destPath) { long myTime = getAPKModTime(); File f = new File(destPath); Timber.d("My Time: %d", myTime); Timber.d("Bin Time: %d", f.lastModified()); if (f.exists() && f.lastModified() > myTime) { Timber.i("%s modtime up-to-date.", f.getName()); return; } Timber.i("%s missing or modtime stale. Re-copying from APK.", f.getName()); String writingFilePath = destPath + ".writing"; InputStream is = null; FileOutputStream fos = null; try { is = getAssets().open(asset); fos = new FileOutputStream(new File(writingFilePath)); IOUtils.copy(is, fos); fos.flush(); Timber.d("wrote out %s", writingFilePath); Runtime.getRuntime().exec("chmod 0700 " + writingFilePath).waitFor(); Timber.d("did chmod 0700 on %s", writingFilePath); Runtime.getRuntime().exec("mv " + writingFilePath + " " + destPath).waitFor(); Timber.d("moved %s to %s", writingFilePath, destPath); f = new File(destPath); if (f.setLastModified(System.currentTimeMillis())) { Timber.d("set modtime of %s", destPath); } } catch (IOException|InterruptedException e) { FileUtils.deleteQuietly(new File(destPath)); FileUtils.deleteQuietly(new File(writingFilePath)); throw new RuntimeException(e); } finally { IOUtils.closeQuietly(is); IOUtils.closeQuietly(fos); } } long getAPKModTime() { try { return getPackageManager().getPackageInfo(getPackageName(), 0).lastUpdateTime; } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException(e); } } static class InitializedListener extends ContentObserver { WeakReference<SyncthingInstance> mService; public InitializedListener(SyncthingInstance service) { super(new Handler(Looper.getMainLooper())); mService = new WeakReference<SyncthingInstance>(service); } @Override public void onChange(boolean selfChange) { SyncthingInstance s = mService.get(); if (s != null && s.mSettings.isInitialised()) { s.maybeStartInotify(); } } } }