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

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

Merge branch '1-epic-refactoring-p1' into v1-oreo

parents 4308856d 53522a86
Loading
Loading
Loading
Loading
Loading
+15 −18
Original line number Diff line number Diff line
@@ -23,7 +23,7 @@ def getTestProp(String propName) {


android {
    compileSdkVersion 28
    compileSdkVersion 31
    defaultConfig {
        applicationId "foundation.e.drive"
        minSdkVersion 26
@@ -61,27 +61,24 @@ android {


dependencies {
    api project(':NextcloudLib')
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:26.1.0'

    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test:rules:1.0.2'
    androidTestImplementation 'androidx.annotation:annotation:1.3.0'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
    api 'androidx.annotation:annotation:1.3.0'
    api project(':NextcloudLib')
    implementation 'androidx.core:core:1.6.0'

    //start to add lib for test - 1/4/21
    //@TODO: add junit runner as lib for testImplementation
    def work_version = "2.7.1"
    // (Java only)
    implementation "androidx.work:work-runtime:$work_version"

    testImplementation 'com.android.support.test:runner:1.0.2'
    testImplementation 'com.android.support.test:rules:1.0.2'
    androidTestImplementation 'androidx.test:runner:1.4.0'
    androidTestImplementation 'androidx.test:rules:1.4.0'
    androidTestImplementation 'androidx.annotation:annotation:1.3.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation  'junit:junit:4.12'

    testImplementation 'androidx.test:runner:1.4.0'
    testImplementation 'androidx.test:rules:1.4.0'
    testImplementation 'junit:junit:4.12'
    //testImplementation 'org.robolectric:robolectric:4.4' //need AndroidX
    testImplementation "org.robolectric:robolectric:3.8"
    testImplementation 'org.robolectric:robolectric:4.4'
    testImplementation('org.mockito:mockito-inline:3.4.0')

    //testImplementation Libs.AndroidX.Test.archCoreTesting //TODO: replace by not android X version
    //implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
    androidTestImplementation  'junit:junit:4.12'
}
+8 −15
Original line number Diff line number Diff line
@@ -8,10 +8,11 @@
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- needed for PersistedJob -->
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
    <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />

    <permission
        android:name="android.permission.FORCE_STOP_PACKAGES"
@@ -25,6 +26,7 @@
        android:icon="@mipmap/ic_eelo"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_eelo_round">
        <!-- widget -->
        <activity
            android:exported="true"
            android:name=".activity.AccountsActivity"
@@ -45,7 +47,7 @@
                android:resource="@xml/e_drive_widget_info" />
        </receiver>

        <!-- Providers -->
        <!-- eDrive -->
        <provider
            android:name=".providers.MediasSyncProvider"
            android:authorities="foundation.e.drive.providers.MediasSyncProvider"
@@ -57,7 +59,9 @@
            android:authorities="foundation.e.drive.providers.SettingsSyncProvider"
            android:enabled="true"
            android:exported="true"
            android:label="Application settings" /> <!-- Services -->
            android:label="Application settings" />

        <!-- Services -->
        <service
            android:name=".services.InitializerService"
            android:enabled="true"
@@ -74,22 +78,11 @@
                <action android:name="drive.services.ResetService" />
            </intent-filter>
        </service>
        <service
            android:name=".jobs.ScannerJob"
            android:permission="android.permission.BIND_JOB_SERVICE" />
        <service
            android:name=".services.ObserverService"
            android:enabled="true" />
        <service android:name=".services.OperationManagerService" />
        <service android:name=".services.SynchronizationService" />

        <receiver
            android:name=".receivers.BatteryStateReceiver"
            android:enabled="true">
            <intent-filter>
                <action android:name="android.intent.action.BATTERY_LOW" />
                <action android:name="android.intent.action.BATTERY_OKAY" />
            </intent-filter>
        </receiver>
        <receiver
            android:name=".receivers.PackageEventReceiver"
            android:enabled="true">
+43 −19
Original line number Diff line number Diff line
@@ -11,13 +11,20 @@ package foundation.e.drive;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
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;
import foundation.e.drive.utils.JobUtils;

/**
 * Class representing the eDrive application.
@@ -26,42 +33,59 @@ import foundation.e.drive.utils.JobUtils;
public class EdriveApplication extends Application {

    private static final String TAG = "EdriveApplication";
    private RecursiveFileObserver mFileObserver = null;
    private FileEventListener fileEventListener;

    @Override
    public void onCreate() {
        super.onCreate();

        fileEventListener = new FileEventListener(getApplicationContext());
        Log.i(TAG, "Starting");
        resetOperationManagerSetting();

        final String pathForObserver = Environment.getExternalStorageDirectory().getAbsolutePath();
        mFileObserver = new RecursiveFileObserver(getApplicationContext(), pathForObserver, fileEventListener);

        SharedPreferences prefs = getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE);
        CommonUtils.createNotificationChannel(getApplicationContext());

        if (prefs.getString(AccountManager.KEY_ACCOUNT_NAME, null) != null) {
            scheduleScannerJob();
            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();

                scheduleScannerJob();
            }
        }
    }

    private void scheduleScannerJob() {
        if (!JobUtils.isScannerJobRegistered(this)) {
            JobUtils.scheduleScannerJob(this);
    /**
     * Start Recursive FileObserver if not already watching
     */
    public void startRecursiveFileObserver(){
        if (!mFileObserver.isWatching()) {
            fileEventListener.bindToSynchronizationService();
            mFileObserver.startWatching();
            Log.d(TAG, "Starting RecursiveFileObserver on root folder");
        }
        else {
            Log.w(TAG, "RecursiveFileObserver (for media's root folder) was already running");
        }
    }

    private void resetOperationManagerSetting() {
        getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE).edit()
                .putBoolean(AppConstants.KEY_OMS_IS_WORKING, false)
                .apply();
    public void stopRecursiveFileObserver(){
        mFileObserver.stopWatching();
        fileEventListener.unbindFromSynchronizationService();
        Log.d(TAG, "RecursiveFileObserver on root folder stops watching ");
    }

    @Override
+178 −0
Original line number Diff line number Diff line
/*
 * Copyright © Narinder Rana (/e/ foundation).
 * 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.FileObservers;

import static foundation.e.drive.models.SyncRequest.Type.UPLOAD;

import android.content.Context;
import android.content.Intent;
import android.os.FileObserver;
import android.util.Log;

import com.owncloud.android.lib.resources.files.FileUtils;

import java.io.File;

import foundation.e.drive.database.DbHelper;
import foundation.e.drive.models.SyncRequest;
import foundation.e.drive.models.SyncedFileState;
import foundation.e.drive.models.SyncedFolder;
import foundation.e.drive.services.SynchronizationService;
import foundation.e.drive.utils.CommonUtils;
import foundation.e.drive.utils.SynchronizationServiceConnection;

/**
 * @author Narinder Rana
 * @author vincent Bourgmayer
 */
public class FileEventListener  {
    private static final String TAG = FileEventListener.class.getSimpleName();

    private final Context appContext;
    private final SynchronizationServiceConnection serviceConnection = new SynchronizationServiceConnection();

    public FileEventListener(Context applicationContext) {
        this.appContext = applicationContext;
    }

    public void onEvent(int event, File file) {
        if (event == FileObserver.DELETE) {
            if (file.isDirectory()) {
                handleDirectoryDelete(file);
            } else {
                handleFileDelete(file);
            }
        } else if (event == FileObserver.CLOSE_WRITE) {

            if (file.isDirectory()) {
                handleDirectoryCloseWrite(file);
            } else {
                handleFileCloseWrite(file);
            }
        } else if (event == FileObserver.MOVE_SELF){
            Log.d(TAG, file.getAbsolutePath() + " has been moved. Not handled yet");
        }
    }

    private void sendSyncRequestToSynchronizationService(SyncRequest request) {
        Log.d(TAG, "Sending a SyncRequest for " + request.getSyncedFileState().getName());
        if (serviceConnection.isBoundToSynchronizationService()) {
            serviceConnection.getSynchronizationService().queueOperation(request);
            serviceConnection.getSynchronizationService().startSynchronization();
        }else{
            Log.w(TAG, "Impossible to send SyncRequest. FileEventListener is not bound to SynchronizationService");
        }
    }

    private void handleDirectoryCloseWrite(File directory) {
        final String fileLocalPath = CommonUtils.getLocalPath(directory);
        Log.d(TAG, "handleDirectoryCloseWrite(" + fileLocalPath + ")");
        SyncedFolder folder = DbHelper.getSyncedFolderByLocalPath(fileLocalPath, appContext);
        if (folder == null) { //it's a directory creation
            final String parentPath = CommonUtils.getLocalPath(directory.getParentFile());
            SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, appContext);
            if (parentFolder != null) { //if parent is in the DB
                folder = new SyncedFolder(parentFolder, directory.getName() + FileUtils.PATH_SEPARATOR, directory.lastModified(), "");
                DbHelper.insertSyncedFolder(folder, appContext);
            }
        } else {  //It's a directory update
            folder.setLastModified(directory.lastModified());
            DbHelper.updateSyncedFolder(folder, appContext);
        }
    }

    private void handleDirectoryDelete(File directory) {
        final String fileLocalPath = CommonUtils.getLocalPath(directory);
        Log.d(TAG, "handleDirectoryDelete("+fileLocalPath+")");
        SyncedFolder folder = DbHelper.getSyncedFolderByLocalPath(fileLocalPath, appContext);
        if (folder == null) {
            //look for parent
            final String parentPath = CommonUtils.getLocalPath(directory.getParentFile());
            SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, appContext);
            if (parentFolder != null ) { //if parent is in the DB
                folder = new SyncedFolder(parentFolder, directory.getName()+ FileUtils.PATH_SEPARATOR, directory.lastModified(), "");
                folder.setEnabled(false);
                DbHelper.insertSyncedFolder(folder, appContext);
            }
        } else {  //If already in DB
            if (folder.isEnabled()) {
                folder.setEnabled(false);
                DbHelper.updateSyncedFolder(folder, appContext);
            }
        }
    }

    private void handleFileCloseWrite(File file) {
        final String fileLocalPath = CommonUtils.getLocalPath(file);
        Log.d(TAG, "handleFileCloseWrite("+fileLocalPath+")");
        SyncRequest request = null;
        SyncedFileState fileState = DbHelper.loadSyncedFile( appContext, fileLocalPath, true);

        if (fileState == null) { //New file discovered
            final String parentPath = CommonUtils.getLocalPath(file.getParentFile());
            SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, appContext);
            if (parentFolder == null || !parentFolder.isEnabled()) {
                Log.w(TAG, "Won't send sync request: no parent are known for new file: "+file.getName());
                return;
            }
            int scannableValue = 0;
            if (parentFolder.isEnabled()) {
                if (parentFolder.isScanRemote()) scannableValue++;
                if (parentFolder.isScanLocal()) scannableValue += 2;
            }

            final String remotePath = parentFolder.getRemoteFolder()+file.getName();
            fileState = new SyncedFileState(-1, file.getName(), CommonUtils.getLocalPath(file), remotePath, "", 0L, parentFolder.getId(), parentFolder.isMediaType(), scannableValue);
            int storedId = DbHelper.manageSyncedFileStateDB(fileState, "INSERT", appContext);
            if (storedId > 0) {
                fileState.setId(storedId);
                request = new SyncRequest(fileState, UPLOAD);
            } else {
                Log.w(TAG, "New File " + file.getName() + " observed but impossible to insert it in DB");
            }
        } else { //File update
            if (fileState.getScannable() > 1) {
                request = new SyncRequest(fileState, UPLOAD);
            }
        }
        if (request != null) {
            sendSyncRequestToSynchronizationService(request);
        }
    }

    private void handleFileDelete(File file) {
        final String fileLocalPath = CommonUtils.getLocalPath(file);
        Log.d(TAG, "handleFileDelete("+fileLocalPath+")");
        SyncedFileState fileState = DbHelper.loadSyncedFile( appContext, fileLocalPath, true);
        if (fileState == null) {
            Log.d(TAG, "Ignore event because file is not already in database");
            return;
        }

        //If already in DB
        if (fileState.getScannable() > 0) {
            fileState.setScannable(0);
            DbHelper.manageSyncedFileStateDB(fileState, "UPDATE", appContext);
        }
    }

    public void unbindFromSynchronizationService(){
        if(serviceConnection.isBoundToSynchronizationService())
            appContext.unbindService(serviceConnection);
        else
            Log.w(TAG, "Not bound to SynchronizationService: can't unbind.");
    }

    public void bindToSynchronizationService(){
        Log.d(TAG, "bindToSynchronizationService()");
        final Intent SynchronizationServiceIntent = new Intent(appContext, SynchronizationService.class);
        appContext.bindService(SynchronizationServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
    }
}
+174 −0
Original line number Diff line number Diff line
/*
 * Copyright © Narinder Rana (/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.FileObservers;

import android.content.Context;
import android.os.FileObserver;

import java.io.File;
import java.io.FileFilter;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;

import foundation.e.drive.database.DbHelper;
import foundation.e.drive.models.SyncedFolder;

/**
 * @author Narinder Rana
 * @author Vincent Bourgmayer
 */
public class RecursiveFileObserver extends FileObserver {
    private final HashMap<String, FileObserver> observers = new HashMap<>();
    private static final  FileFilter WATCHABLE_DIRECTORIES_FILTER = new FileFilter() {
        @Override
        public boolean accept(File file) {
            return file.isDirectory() && !file.getName().startsWith(".");
        }
    };

    private boolean watching = false;
    private final Context applicationContext;
    private String path;
    private int mask;
    private FileEventListener listener;

    public RecursiveFileObserver(Context applicationContext, String path, FileEventListener listener) {
        this(applicationContext, path, ALL_EVENTS, listener);
    }

    public RecursiveFileObserver(Context applicationContext, String path, int mask, FileEventListener listener) {
        super(path, mask);
        this.path = path;
        this.mask = mask | FileObserver.CREATE | FileObserver.DELETE_SELF;
        this.listener = listener;
        this.applicationContext = applicationContext;
    }


    @Override
    public void onEvent(int event, String path) {
        File file;
        if (path == null) {
            file = new File(this.path);
        } else {
            file = new File(this.path, path);
        }

        notify(event, file);
    }

    private void notify(int event, File file) {
        if (listener != null) {
            listener.onEvent(event & FileObserver.ALL_EVENTS, file);
        }
    }

    @Override
    public void startWatching() {
        Stack<String> stack = new Stack<>();

        List<SyncedFolder> mSyncedFolders = DbHelper.getAllSyncedFolders(applicationContext);
        if (!mSyncedFolders.isEmpty()){
            for (SyncedFolder syncedFolder:mSyncedFolders){
                stack.push(syncedFolder.getLocalFolder());
                stack.push(syncedFolder.getRemoteFolder());
            }
            watching = true;
        }

        // Recursively watch all child directories
        while (!stack.empty()) {
            String parent = stack.pop();
            startWatching(parent);

            File path = new File(parent);
            File[] files = path.listFiles(WATCHABLE_DIRECTORIES_FILTER);
            if (files != null) {
                for (File file : files) {
                    stack.push(file.getAbsolutePath());
                }
            }
        }
    }

    /**
     * Start watching a single file
     * @param path
     */
    private void startWatching(String path) {
        synchronized (observers) {
            FileObserver observer = observers.remove(path);
            if (observer != null) {
                observer.stopWatching();
            }
            observer = new SingleFileObserver(path, mask);
            observer.startWatching();
            observers.put(path, observer);
        }
    }

    @Override
    public void stopWatching() {
        for (FileObserver observer : observers.values()) {
            observer.stopWatching();
        }
        observers.clear();
        watching = false;
    }

    /**
     * Stop watching a single file
     * @param path
     */
    private void stopWatching(String path) {
        synchronized (observers) {
            FileObserver observer = observers.remove(path);
            if (observer != null) {
                observer.stopWatching();
            }
        }
    }

    public boolean isWatching(){
        return watching;
    }

    private class SingleFileObserver extends FileObserver {
        private String filePath;

        public SingleFileObserver(String path, int mask) {
            super(path, mask);
            filePath = path;
        }

        @Override
        public void onEvent(int event, String path) {
            File file;
            if (path == null) {
                file = new File(filePath);
            } else {
                file = new File(filePath, path);
            }

            switch (event & FileObserver.ALL_EVENTS) {
                case DELETE_SELF:
                    RecursiveFileObserver.this.stopWatching(filePath);
                    break;
                case CREATE:
                    if (WATCHABLE_DIRECTORIES_FILTER.accept(file)) {
                        RecursiveFileObserver.this.startWatching(file.getAbsolutePath());
                    }
                    break;
            }

            RecursiveFileObserver.this.notify(event, file);
        }
    }
}
Loading