syncthing.android.service.SyncthingInstance.java Source code

Java tutorial

Introduction

Here is the source code for syncthing.android.service.SyncthingInstance.java

Source

/*
 * 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();
            }
        }
    }

}