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

Commit 67c7a85b authored by Vincent Bourgmayer's avatar Vincent Bourgmayer
Browse files

Remove OperationManagerService

- Remove OperationManagerService.java
- Remove OperationManagerServiceTest.java
- Update ServiceExceptionHandler to remove code related to OperationManagerService
parent 7d596aeb
Loading
Loading
Loading
Loading
Loading
+0 −334
Original line number Diff line number Diff line
/*
 * Copyright © Vincent Bourgmayer (/e/ foundation).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 */

package foundation.e.drive.services;

import android.accounts.Account;
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.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Parcelable;
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.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;
import foundation.e.drive.utils.ServiceExceptionHandler;

/**
 * @author Vincent Bourgmayer
 * Service to do upload, remove and download operation.
 */
public class OperationManagerService extends Service implements OnRemoteOperationListener{
    private final static String TAG = OperationManagerService.class.getSimpleName();
    private final Binder binder = new Binder(){
        OperationManagerService getService(){
            return OperationManagerService.this;
        }
    };



    private int workerAmount = 0; //Number of thread available to execute RemoteOperation
    private boolean[] mThreadWorkingState; //State of the threads; true mean the thread is working
    private Thread[] mThreadPool; //The threads to use

    private ConcurrentLinkedDeque<ComparableOperation> mOperationsQueue; // Queue of Operation
    private Hashtable<RemoteOperation, Integer> mStartedOperations; //Operations which are running

    private OperationManagerHandler mHandler;
    private OwnCloudClient mClient; //ClientObject
    private Account mAccount; //Will be used soon

    @Override
    public void onDestroy() {
        Log.i(TAG, "onDestroy()");
        super.onDestroy();
    }


    /**
     * Start to run all threads
     */
    private void startAllThreads(){
        Log.i(TAG, "startAllThreads");
        for(int i =-1; ++i < workerAmount;){
            this.startWork(i);
        }
    }

    /**
     * retrieve an operation from queue and execute it if a thread is available.
     * @param threadIndex index of thread which execute job.
     */
    private synchronized void startWork( int threadIndex ){
        Log.i(TAG, "startWork("+threadIndex+")" );

        //Check if operation queue is empty
        if(mOperationsQueue.isEmpty()){
            boolean stillWorking = false;
            //check that all other thread has finished.
            for(boolean a : this.mThreadWorkingState){
                if(a){
                    stillWorking = a;
                    break;
                }
            }

            if(!stillWorking) {
                Log.i(TAG, "Operation queue is empty. all jobs Done. The End");
                //Register timestamp to allow to calculate min delay between two sync
                getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE)
                        .edit()
                        .putLong(AppConstants.KEY_LAST_SYNC_TIME, System.currentTimeMillis())
                        .putBoolean(AppConstants.KEY_OMS_IS_WORKING, false)
                        .apply();

                stopSelf();
                return;
            }
        }
        //start the new Job
        if( !mThreadWorkingState[threadIndex] && CommonUtils.haveNetworkConnexion( getApplicationContext() ) ) { //check if the thread corresponding to threadIndex isn't already working

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

                if( CommonUtils.isThisSyncAllowed(mAccount, operation.isMediaType() ) ) {
                    mStartedOperations.put(operation.toRemoteOperation(), threadIndex);
                    this.mThreadPool[threadIndex] = operation.toRemoteOperation().execute(mClient, this, this.mHandler);
                    this.mThreadWorkingState[threadIndex] = true;
                }
            }
        } //else : thread is already running or no network connexion
    }

    /**
     * Called when a remoteOperation finish
     * @param operation the operation which finished
     * @param result the result of the operation
     */
    @Override
    public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) {
        Log.i(TAG, "onRemoteOperationFinish()");

        // Start an another operation with the thread which has run this one
        Integer threadIndex = this.mStartedOperations.remove(operation);
        if(threadIndex != null) {
            this.mThreadWorkingState[threadIndex] = false;
            this.startWork(threadIndex);
        }

        if(operation instanceof RemoveFileOperation){
            if( result.isSuccess() ) {
                DbHelper.manageSyncedFileStateDB( ( ( RemoveFileOperation ) operation ).getSyncedFileState(),
                        "DELETE", this);
            }
        }else {
            String operationClassName = operation.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 (operation 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 (operation instanceof DownloadFileOperation) {
                        Log.e(TAG, " Download: Unknown_error : failed");
                    }
                    break;
                case FORBIDDEN:
                    if (operation 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 (operation 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, mClient.getBaseUri()),
                                    0))
                            .setContentText("Your drive lacks of space. Tap to check " + mClient.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 OperationManagerHandler extends Handler {
        private final String TAG = OperationManagerHandler.class.getSimpleName();

        private final WeakReference<OperationManagerService> mOperationServiceWeakRef;

        OperationManagerHandler(OperationManagerService mOperationService){
            this.mOperationServiceWeakRef = new WeakReference<>(mOperationService);
        }

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

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

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

        try{
            CommonUtils.setServiceUnCaughtExceptionHandler(this);

            if (null == intent || null == intent.getAction ()) {
                String source = null == intent ? "intent" : "action";
                Log.e (TAG, source + " was null, flags=" + flags + " bits=" + Integer.toBinaryString (flags));
                //return START_STICKY;
            }

            Bundle extras = intent.getExtras();
            Log.d(TAG, "OperationManagerService recieved "+(extras == null ? "null extras": extras.size()+" operations to perform") );

            if(extras != null) {
                //Load operation from intent
                this.mOperationsQueue = new ConcurrentLinkedDeque<>();
                for (String key : extras.keySet()) {

                    Parcelable parcelableObject = extras.getParcelable(key);
                    if (key.equals("account")) {
                        this.mAccount = (Account) parcelableObject;
                    } else if (parcelableObject.getClass().getName().equals(DownloadFileOperation.class.getName())) {
                        DownloadFileOperation download = (DownloadFileOperation) parcelableObject;
                        download.setContext(getApplicationContext());
                        mOperationsQueue.add(download);
                    } else if (parcelableObject.getClass().getName().equals(UploadFileOperation.class.getName())) {
                        UploadFileOperation upload = (UploadFileOperation) parcelableObject;
                        upload.setContext(getApplicationContext());
                        mOperationsQueue.add(upload);
                    } else if (parcelableObject.getClass().getName().equals(RemoveFileOperation.class.getName())) {
                        mOperationsQueue.add((RemoveFileOperation) parcelableObject);
                    }
                }

                if(mAccount == null || mOperationsQueue.isEmpty()){
                    Log.w(TAG, "No account or Operation queue is empty");
                    return super.onStartCommand(intent, flags, startId);
                }

                //Initialize class's field
                this.workerAmount = 4; //This variable could be replace later by an option in settings

                this.mThreadPool = new Thread[workerAmount];
                this.mThreadWorkingState = new boolean[workerAmount];
                this.mHandler = new OperationManagerHandler(this);
                this.mStartedOperations = new Hashtable<RemoteOperation, Integer>();

                mClient = DavClientProvider.getInstance().getClientInstance(mAccount, getApplicationContext());
                if (mClient != null) {
                    getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE)
                            .edit()
                            .putBoolean(AppConstants.KEY_OMS_IS_WORKING, true)
                            .apply();

                    startAllThreads();
                } else {
                    Log.w(TAG, "No Client, Can't Work!");
                    stopSelf();
                }
            }else{
                Log.w(TAG, "Intent's extras is null.");
            }
        }catch (Exception ex){
            Log.e("Exception", ex.getMessage());
            ex.printStackTrace();
        }

        return super.onStartCommand(intent, flags, startId);
    }


    @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();
    }
}
+0 −10
Original line number Diff line number Diff line
@@ -7,7 +7,6 @@
 */
package foundation.e.drive.utils;
import android.app.Service;
import android.content.Context;
import android.os.Environment;
import android.util.Log;

@@ -18,8 +17,6 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.Thread.UncaughtExceptionHandler;

import foundation.e.drive.services.OperationManagerService;

/**
 * @author Vincent Bourgmayer
 */
@@ -48,13 +45,6 @@ public class ServiceExceptionHandler implements UncaughtExceptionHandler{
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Log.d(TAG, "Service class: "+service.getClass().getSimpleName());
        //IF OMS is crashing, set settings that it runs to false;
        if(service.getClass().getSimpleName().equals(OperationManagerService.class.getSimpleName())){
            service.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE)
                    .edit()
                    .putBoolean(AppConstants.KEY_OMS_IS_WORKING, false)
                    .apply();
        }

        if(isExternalStorageAvailable() && !isExternalStorageReadOnly()){
            //Get TimeStamp
+0 −108
Original line number Diff line number Diff line
package foundation.e.drive.services;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.job.JobScheduler;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;

import com.owncloud.android.lib.resources.files.model.RemoteFile;

import junit.framework.Assert;

import org.junit.Test;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadows.ShadowLog;

import java.util.List;

import foundation.e.drive.TestUtils;
import foundation.e.drive.database.DbHelper;
import foundation.e.drive.models.SyncedFileState;
import foundation.e.drive.operations.DownloadFileOperation;
import foundation.e.drive.utils.AppConstants;


public class OperationManagerServiceTest extends AbstractServiceIT<OperationManagerService>{

    public OperationManagerServiceTest(){
        mServiceController =  Robolectric.buildService(OperationManagerService.class);
        mService = mServiceController.get();
        context = RuntimeEnvironment.application;
        accountManager = AccountManager.get(context);
        jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        contentResolver = context.getContentResolver();
        sharedPreferences = context.getSharedPreferences( AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE);
        connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        dbHelper = new DbHelper(context);
    }


    /**
     * Check that the service stop quickly if no intent is provided
     */
    @Test
    public void noIntent_shouldStop(){
        mServiceController.create();
        mService.onStartCommand(null,0, 0);

        List<ShadowLog.LogItem> logs = ShadowLog.getLogs();
        ShadowLog.LogItem lastLog = logs.get(logs.size()-2);

        Assert.assertEquals("Expected last log was: 'intent was null, flags=0 bits=0' but got:"+lastLog.msg, "intent was null, flags=0 bits=0", lastLog.msg);
    }


    /**
     * Start the service with an intent that doesn't contains list of extra data
     * (meaning no account and no synchronisation to perform)
     */
    @Test
    public void intentWithoutExtras_shouldStop(){
        mServiceController.create();
        mService.onStartCommand(new Intent(), 0, 0);

        List<ShadowLog.LogItem> logs = ShadowLog.getLogs();
        ShadowLog.LogItem lastLog = logs.get(logs.size()-1);

        Assert.assertEquals("Expected last log was: 'Intent's extras is null.' but got:"+lastLog.msg, "Intent's extras is null.", lastLog.msg);
    }


    /**
     * Start the OperationmanagerService with File to upload but no account provided
     * in the intent.
     * Failure is expected
     */
    @Test
    public void noAccount_shouldFail(){
        prepareValidAccount();
        final Account account = TestUtils.getValidAccount();
        final Intent intent = new Intent("dummyAction");

        intent.putExtra("0",
                new DownloadFileOperation(
                        new RemoteFile("/eDrive-test/coco.txt"),
                        new SyncedFileState(3, "coco.txt", "/tmp/eDrive-test/", "/eDrive-test/coco.txt","", -1, 1, true )
                )
        );


        intent.putExtra("account", account);

        final boolean accountRemoved = accountManager.removeAccountExplicitly(account);
        Assert.assertTrue("Account removal should return true but returned false", accountRemoved);

        mServiceController.create();
        mService.onStartCommand(intent, 0,0);

        List<ShadowLog.LogItem> logs = ShadowLog.getLogs();
        ShadowLog.LogItem lastLog = logs.get(logs.size()-1);
        Assert.assertEquals("Expected last log was: 'No Client, Can't Work!' but got:"+lastLog.msg, "No Client, Can't Work!", lastLog.msg);

    }


}