Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a49b3ffc authored by Vincent Bourgmayer's avatar Vincent Bourgmayer
Browse files

Add SynchronizationService

This service will replace OperationManagerService in next modification.
It is a bound and started Service which will run undefinitively
- Add new services.SynchronizationService
- Update EdriveApplication.java to start SynchronizationService when device reboot (if all condition are met)
- Update services.ResetService.java to stop SynchronizationService when account is removed
- Update AndroidManifest.xml to replace OperationManagerService by SynchronizationService
- Update InitializerService to start SynchronizationService when its jobs is over
parent e3bbb271
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -64,7 +64,7 @@ http://www.gnu.org/licenses/gpl.html
        <service
            android:name=".services.ObserverService"
            android:enabled="true" />
        <service android:name=".services.OperationManagerService" />
        <service android:name=".services.SynchronizationService" />

        <receiver
            android:name=".receivers.PackageEventReceiver"
+12 −7
Original line number Diff line number Diff line
@@ -12,12 +12,14 @@ import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Environment;
import android.util.Log;

import foundation.e.drive.FileObservers.FileEventListener;
import foundation.e.drive.FileObservers.RecursiveFileObserver;
import foundation.e.drive.services.SynchronizationService;
import foundation.e.drive.utils.AppConstants;
import foundation.e.drive.utils.CommonUtils;

@@ -46,17 +48,20 @@ public class EdriveApplication extends Application {
        if (prefs.getString(AccountManager.KEY_ACCOUNT_NAME, null) != null) {
            Log.d(TAG, "Account already registered");
            startRecursiveFileObserver();

            Intent SynchronizationServiceIntent = new Intent(getApplicationContext(), SynchronizationService.class);
            startService(SynchronizationServiceIntent);

        } else {
            Account mAccount = CommonUtils.getAccount(getString(R.string.eelo_account_type), AccountManager.get(this));
            if (mAccount != null) {
            if (mAccount == null) {return ;}

            String accountName = mAccount.name;
            String accountType = mAccount.type;

            prefs.edit().putString(AccountManager.KEY_ACCOUNT_NAME, accountName)
                    .putString(AccountManager.KEY_ACCOUNT_TYPE, accountType)
                    .apply();

            }
        }
    }

+2 −2
Original line number Diff line number Diff line
@@ -283,8 +283,8 @@ public class InitializerService extends Service implements OnRemoteOperationList
        //all folder have been created
        ((EdriveApplication) this.getApplication() ).startRecursiveFileObserver();

        //Intent FileObserverService = new Intent(getApplicationContext(), foundation.e.drive.services.FileObserverService.class);
        //startService(FileObserverService);
        Intent SynchronizationServiceIntent = new Intent(getApplicationContext(), foundation.e.drive.services.SynchronizationService.class);
        startService(SynchronizationServiceIntent);

        //Immediatly start ObserverService to not have to wait 30 minutes.
        Intent observersServiceIntent = new Intent(getApplicationContext(), foundation.e.drive.services.ObserverService.class);
+3 −4
Original line number Diff line number Diff line
@@ -59,11 +59,10 @@ public class ResetService extends Service {
                    result = getApplicationContext().stopService( stopperIntent );
                    Log.d(TAG, "stop InitializerService: "+result);

                    stopperIntent = new Intent(getApplicationContext(), OperationManagerService.class); //@todo try to replace it by stopperIntent.setClassName(getApplicationContext, OperationManagerService.class)


                    stopperIntent = new Intent(getApplicationContext(), SynchronizationService.class); //@todo try to replace it by stopperIntent.setClassName(getApplicationContext, OperationManagerService.class)
                    result = getApplicationContext().stopService( stopperIntent );
                    Log.d(TAG, "stop OperationManagerService: "+result);

                    Log.d(TAG, "stop SynchronizationService: "+result);

                    WorkManager.getInstance(this).cancelAllWorkByTag(AppConstants.WORK_GENERIC_TAG);

+231 −0
Original line number Diff line number Diff line
package foundation.e.drive.services;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;

import androidx.annotation.Nullable;

import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.operations.OnRemoteOperationListener;
import com.owncloud.android.lib.common.operations.RemoteOperation;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;

import java.lang.ref.WeakReference;
import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedDeque;

import foundation.e.drive.database.DbHelper;
import foundation.e.drive.operations.ComparableOperation;
import foundation.e.drive.operations.DownloadFileOperation;
import foundation.e.drive.operations.RemoveFileOperation;
import foundation.e.drive.operations.UploadFileOperation;
import foundation.e.drive.utils.AppConstants;
import foundation.e.drive.utils.CommonUtils;
import foundation.e.drive.utils.DavClientProvider;

/**
 * @author Vincent Bourgmayer
 */
public class SynchronizationService extends Service implements OnRemoteOperationListener {
    private final static String TAG = SynchronizationService.class.getSimpleName();
    private final Binder binder = new Binder(){
        SynchronizationService getService(){
            return SynchronizationService.this;
        }
    };

    private ConcurrentLinkedDeque<ComparableOperation> operationsQueue;
    private Hashtable<RemoteOperation, Integer> startedOperations; //Operations which are running
    private Account account;
    private final int workerAmount = 4;
    private boolean[] threadWorkingState; //State of the threads; true mean the thread is working
    private Thread[] threadPool;
    private OwnCloudClient client;
    private OperationHandler handler;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand()");

        final SharedPreferences prefs = getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE);

        if (prefs.getString(AccountManager.KEY_ACCOUNT_NAME, null) != null) {
            Log.w(TAG, "No account available. Stop SynchronizationService");
            stopSelf();
            return START_NOT_STICKY;
        }


        account = (Account) intent.getParcelableExtra("account");
        operationsQueue = new ConcurrentLinkedDeque<>();
        threadPool = new Thread[workerAmount];
        threadWorkingState = new boolean[workerAmount];
        client = DavClientProvider.getInstance().getClientInstance(account, getApplicationContext());
        handler = new OperationHandler(this);

        return START_REDELIVER_INTENT;
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    @Override
    public void onLowMemory() {
        Log.w(TAG, "System is low on memory. Service might get killed. Setting KEY_OMS_IS_WORKING to false");
        getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE).edit()
                .putBoolean(AppConstants.KEY_OMS_IS_WORKING, false)
                .apply();
    }

    public boolean queueOperation(ComparableOperation operation){
        return operationsQueue.add(operation);
    }

    public boolean queueOperations(List<ComparableOperation> operations){
        return operationsQueue.addAll(operations);
    }

    public void startSynchronization(){
        Log.d(TAG, "startAllThreads");
        for(int i =-1; ++i < workerAmount;){
            this.startWorker(i);
        }
    }

    private void startWorker(int threadIndex){
        if (operationsQueue.isEmpty()) return;
        if (!threadWorkingState[threadIndex] && CommonUtils.haveNetworkConnexion(getApplicationContext())) { //check if the thread corresponding to threadIndex isn't already working

            ComparableOperation operation = this.operationsQueue.poll(); //return null if deque is empty
            if (operation != null) {
                Log.v(TAG, " an operation has been poll from queue");

                if (CommonUtils.isThisSyncAllowed(account, operation.isMediaType())) {
                    startedOperations.put(operation.toRemoteOperation(), threadIndex);
                    threadPool[threadIndex] = operation.toRemoteOperation().execute(client, this, handler);
                    threadWorkingState[threadIndex] = true;
                }
            }
        }
    }

    @Override
    public void onRemoteOperationFinish(RemoteOperation callerOperation, RemoteOperationResult result) {
        Log.d(TAG, "onRemoteOperationFinish()");
        Integer threadIndex = this.startedOperations.remove(callerOperation);
        if (threadIndex != null) {
            this.threadWorkingState[threadIndex] = false;
            this.startWorker(threadIndex);
        }

        if (callerOperation instanceof RemoveFileOperation){
            if ( result.isSuccess() ) {
                DbHelper.manageSyncedFileStateDB( ( ( RemoveFileOperation ) callerOperation ).getSyncedFileState(),
                        "DELETE", this);
            }
        } else {
            String operationClassName = callerOperation.getClass().getSimpleName();
            switch (result.getCode()) {
                case OK:
                    Log.d(TAG, operationClassName + " Succeed");
                    break;
                case SYNC_CONFLICT:
                    //Case specific to UploadFileOperation
                    Log.e(TAG, operationClassName+" : Sync_conflict : File is already up to date");
                    break;
                case INVALID_OVERWRITE:
                    Log.e(TAG, operationClassName + " => invalid_overwrite :\n remote file and local file doesn't have the same size");
                    break;
                case UNKNOWN_ERROR:
                    if (callerOperation instanceof UploadFileOperation) {
                        if(result.getData() != null) {
                            int rowAffected = DbHelper.forceFoldertoBeRescan(((Long) result.getData().get(0)).intValue(), getApplicationContext());
                            Log.e(TAG, " Upload failed for unknown reason.\n Force folder to be rescan next time (row affected) :" + rowAffected);
                        } else {
                            Log.w(TAG, "result.getData() for UploadFileOperation returned null");
                        }
                    } else if (callerOperation instanceof DownloadFileOperation) {
                        Log.e(TAG, " Download: Unknown_error : failed");
                    }
                    break;
                case FORBIDDEN:
                    if (callerOperation instanceof UploadFileOperation) {
                        if (result.getData() != null) {
                            int rowAffected = DbHelper.forceFoldertoBeRescan(((Long) result.getData().get(0)).intValue(), getApplicationContext());
                            Log.e(TAG, " Upload: Forbidden : Can't get syncedFileState, no remote path defined. Force folder to be rescan next time (row affected) :" + rowAffected);
                        } else {
                            Log.w(TAG, "result.getData() for UploadFileOperation returned null");
                        }
                    } else if (callerOperation instanceof DownloadFileOperation) {
                        Log.e(TAG, "Download : Forbidden: Can't get syncedFileState, no local path defined");
                    }
                    break;
                case QUOTA_EXCEEDED:
                    //Case specific to UploadFileOperation
                    Log.w(TAG, "Quota_EXCEEDED");

                    NotificationManager nM = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);

                    Notification notif = new Notification.Builder(this, AppConstants.notificationChannelID)
                            .setContentIntent(PendingIntent.getActivity(getApplicationContext(),
                                    0,
                                    new Intent(Intent.ACTION_VIEW, client.getBaseUri()),
                                    0))
                            .setContentText("Your drive lacks of space. Tap to check " + client.getBaseUri())
                            .setSmallIcon(android.R.drawable.stat_sys_warning)
                            .build();

                    nM.notify(1,notif );
                    break;
                case FILE_NOT_FOUND:
                    //Case specific to DownloadFileOperation
                    Log.e(TAG, operationClassName+" : File_not_found: File not found after download");
                    break;
                case ETAG_UNCHANGED:
                    //Case specific to DownloadFileOperation
                    Log.e(TAG, operationClassName+" : Sync_conflict: File is already up to date");
                    break;

            } //Close switch
        } //Close else
    }

    /**
     * Handler for the class
     */
    static class OperationHandler extends Handler {
        private final String TAG = SynchronizationService.OperationHandler.class.getSimpleName();

        private final WeakReference<SynchronizationService> serviceWeakRef;

        OperationHandler(SynchronizationService mOperationService){
            serviceWeakRef = new WeakReference<>(mOperationService);
        }

        @Override
        public void handleMessage(Message msg) {
            Log.i(TAG, "handler.handleMessage()");
            Bundle data = msg.getData();

            serviceWeakRef.get()
                    .threadWorkingState[data.getInt("thread index")] = data.getBoolean("mThreadWorkingState");
        }
    }
}