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

Commit 5954bee6 authored by Fahim Salam Chowdhury's avatar Fahim Salam Chowdhury 👽
Browse files

Merge branch '115-epic' into 115-merge_persistence_flag_changes_into_main

parents b252d2e1 e248aa37
Loading
Loading
Loading
Loading
Loading
+55 −4
Original line number Diff line number Diff line
@@ -49,7 +49,7 @@ eDrive can also receive some broadcast intent for different purpose:
**Force the synchronization**

```bash
adb shell am broadcast -a foundation.e.drive.action.FORCE_SYNC --receiver-include-background
adb shell am broadcast -a foundation.e.drive.action.FORCE_SCAN --receiver-include-background
```

**Generate a database dump accessible by the user**
@@ -58,6 +58,12 @@ adb shell am broadcast -a foundation.e.drive.action.FORCE_SYNC --receiver-includ
adb shell am broadcast -a foundation.e.drive.action.DUMP_DATABASE --receiver-include-background
```

or Download database directly:

```bash
adb pull /data/data/foundation.e.drive/databases
```

**Disable log limit on release build**

```bash
@@ -70,4 +76,49 @@ adb shell am broadcast -a foundation.e.drive.action.DUMP_DATABASE --receiver-inc
adb shell am broadcast -a foundation.e.drive.action.FULL_LOG_ON_PROD --receiver-include-background --ez full_log_enable false
```

[doc-persistence]: https://developer.android.com/guide/topics/manifest/application-element#persistent
 No newline at end of file
**Test SyncWorker: upload 10 empty files**

Does not rely on FileObserver or FullScanWorker

```bash
adb shell am broadcast -a foundation.e.drive.action.TEST_SYNC --receiver-include-background
```

**Display logat**

```bash
adb logcat --pid=$(adb shell pidof -s foundation.e.drive)
```

You can also use the script `dev-tools.sh` to run those command. Use : `./dev-tools.sh -h` to display options


**Disable File Observer**


```bash
adb shell am broadcast -a foundation.e.drive.action.ENABLE_FILE_OBSERVER --receiver-include-background --ez file_observer_enable false
```

**Enable File Observer**


```bash
adb shell am broadcast -a foundation.e.drive.action.ENABLE_FILE_OBSERVER --receiver-include-background --ez file_observer_enable true
```

### local NC for testing

Use following documentation to set up a local NC instance

https://gitlab.e.foundation/internal/wiki/-/wikis/qa-team/how-to-run-local-nextcloud

`docker run -d -p 8080:80 -e NEXTCLOUD_TRUSTED_DOMAINS="<your computer IP>:8080" -e SQLITE_DATABASE=nc -e NEXTCLOUD_ADMIN_USER=admin -e NEXTCLOUD_ADMIN_PASSWORD=admin nextcloud:<NC version>
`

Then you can use Wireshark by example to collect network packet.
Example of filter for wireshark:
`(ip.src == <computer IP> && ip.dst == <Phone IP>) || (ip.dst == <computer IP> && ip.src == <Phone IP> ) && http`


*note: replace <computer IP>, <NC version> and <Phone IP> by values*
+8 −10
Original line number Diff line number Diff line
@@ -3,10 +3,9 @@ plugins {
    id 'org.jetbrains.kotlin.android'
}


def versionMajor = 1
def versionMinor = 4
def versionPatch = 4
def versionMinor = 5
def versionPatch = 1

def getTestProp(String propName) {
    def result = ""
@@ -101,16 +100,15 @@ dependencies {
    implementation 'foundation.e:Nextcloud-Android-Library:1.0.8-u2.17-release'
    implementation "commons-httpclient:commons-httpclient:3.1@jar"
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    api 'androidx.annotation:annotation:1.6.0'
    implementation 'androidx.core:core:1.10.1'
    api 'androidx.annotation:annotation:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation "androidx.constraintlayout:constraintlayout:2.1.4"
    implementation 'com.google.android.material:material:1.9.0'
    implementation 'com.google.android.material:material:1.10.0'
    implementation 'com.github.bumptech.glide:glide:4.15.1'
    implementation 'com.github.bumptech.glide:annotations:4.15.1'
    implementation 'androidx.core:core-ktx:1.10.1'
    implementation 'androidx.core:core-ktx:1.12.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1'
    implementation "androidx.work:work-runtime:2.8.1"
    implementation "androidx.work:work-runtime:2.9.0"
    implementation 'androidx.test:core:1.5.0'
    implementation 'com.jakewharton.timber:timber:5.0.1'
    implementation 'foundation.e:elib:0.0.1-alpha11'
@@ -118,7 +116,7 @@ dependencies {

    androidTestImplementation 'androidx.test:runner:1.5.2'
    androidTestImplementation 'androidx.test:rules:1.5.0'
    androidTestImplementation 'androidx.annotation:annotation:1.6.0'
    androidTestImplementation 'androidx.annotation:annotation:1.7.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation 'junit:junit:4.13.2'

@@ -128,5 +126,5 @@ dependencies {
    testImplementation 'junit:junit:4.13.2'
    testImplementation 'org.robolectric:robolectric:4.7.3'
    testImplementation 'org.mockito:mockito-core:5.0.0'
    testImplementation 'androidx.work:work-testing:2.8.1'
    testImplementation 'androidx.work:work-testing:2.9.0'
}
+9 −3
Original line number Diff line number Diff line
@@ -12,10 +12,15 @@
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" /> <!-- for Android 30+ -->
    <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.RECEIVE_BOOT_COMPLETED" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
    <uses-permission android:name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND"
        tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
    <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
    <uses-permission android:name="foundation.e.pwaplayer.provider.READ_WRITE" />
    <uses-permission
@@ -37,7 +42,6 @@
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:persistent="true"
        android:roundIcon="@mipmap/ic_launcher"
        android:requestLegacyExternalStorage="true"> <!-- required to access media folder since app target sdk is 29+ but deprecated. Remove when Q support will be dropped-->
        <activity
@@ -98,9 +102,11 @@
            android:exported="true"
            tools:ignore="ExportedReceiver">
            <intent-filter>
                <action android:name="foundation.e.drive.action.FORCE_SYNC" />
                <action android:name="foundation.e.drive.action.FORCE_SCAN" />
                <action android:name="foundation.e.drive.action.DUMP_DATABASE"/>
                <action android:name="foundation.e.drive.action.FULL_LOG_ON_PROD"/>
                <action android:name="foundation.e.drive.action.ENABLE_FILE_OBSERVER"/>
                <action android:name="foundation.e.drive.action.TEST_SYNC"/>
            </intent-filter>
        </receiver>

+19 −30
Original line number Diff line number Diff line
@@ -8,40 +8,40 @@

package foundation.e.drive;

import static timber.log.Timber.DebugTree;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;

import foundation.e.drive.FileObservers.FileEventListener;
import foundation.e.drive.FileObservers.RecursiveFileObserver;
import androidx.annotation.NonNull;

import foundation.e.drive.database.FailedSyncPrefsManager;
import foundation.e.drive.fileObservers.FileObserverManager;
import foundation.e.drive.utils.AppConstants;
import foundation.e.drive.utils.CommonUtils;
import foundation.e.drive.utils.ReleaseTree;
import foundation.e.lib.telemetry.Telemetry;
import timber.log.Timber;
import static timber.log.Timber.DebugTree;

import androidx.annotation.NonNull;

/**
 * Class representing the eDrive application.
 * It is instantiated before any other class.
 *
 * @author Jonathan klee
 * @author Vincent Bourgmayer
 */
public class EdriveApplication extends Application {
    private RecursiveFileObserver mFileObserver = null;
    private FileEventListener fileEventListener;
    private FileObserverManager fileObserverManager = null;

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

        fileObserverManager = new FileObserverManager(getApplicationContext());

        CommonUtils.createNotificationChannel(getApplicationContext());

@@ -49,7 +49,9 @@ public class EdriveApplication extends Application {

        if (!isAccountStoredInPreferences(prefs)) {
            final Account account = CommonUtils.getAccount(getString(R.string.eelo_account_type), AccountManager.get(this));
            if (account == null) { return; }
            if (account == null) {
                return;
            }

            prefs.edit().putString(AccountManager.KEY_ACCOUNT_NAME, account.name)
                    .putString(AccountManager.KEY_ACCOUNT_TYPE, account.type)
@@ -59,19 +61,12 @@ public class EdriveApplication extends Application {
        FailedSyncPrefsManager.getInstance(getApplicationContext()).clearPreferences();
    }

    /**
     * Start Recursive FileObserver if not already watching
     */
    synchronized public void startRecursiveFileObserver() {
        if (!mFileObserver.isWatching()) {
            mFileObserver.startWatching();
            Timber.d("Started RecursiveFileObserver on root folder");
        }
        fileObserverManager.initializeObserving();
    }

    synchronized public void stopRecursiveFileObserver() {
        mFileObserver.stopWatching();
        Timber.d("Stopped RecursiveFileObserver on root folder");
        fileObserverManager.stopObserving();
    }

    @Override
@@ -80,21 +75,15 @@ public class EdriveApplication extends Application {
        Timber.i("System is low on memory. Application might get killed by the system.");
    }

    private void instantiateFileEventListener() {
        fileEventListener = new FileEventListener(getApplicationContext());

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

    private void setupLogging() {
        if (BuildConfig.DEBUG) {
            Timber.plant(new DebugTree());
        } else {
            return;
        }

        Telemetry.init(BuildConfig.SENTRY_DSN, this, true);
        Timber.plant(new ReleaseTree());
    }
    }

    private boolean isAccountStoredInPreferences(@NonNull SharedPreferences prefs) {
        return prefs.getString(AccountManager.KEY_ACCOUNT_NAME, null) != null;
+0 −243
Original line number Diff line number Diff line
/*
 * Copyright © ECORP SAS 2022.
 * 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.DISABLE_SYNCING;
import static foundation.e.drive.models.SyncRequest.Type.UPLOAD;
import static foundation.e.drive.models.SyncedFileStateKt.DO_NOT_SCAN;
import static foundation.e.drive.models.SyncedFileStateKt.SCAN_ON_CLOUD;
import static foundation.e.drive.models.SyncedFileStateKt.SCAN_ON_DEVICE;
import static foundation.e.drive.utils.FileUtils.getLocalPath;

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

import androidx.annotation.NonNull;

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

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.synchronization.SyncRequestCollector;
import foundation.e.drive.synchronization.SyncProxy;
import timber.log.Timber;

/**
 * @author Narinder Rana
 * @author vincent Bourgmayer
 */
public class FileEventListener  {

    private final Context appContext;

    public FileEventListener(@NonNull Context applicationContext) {
        Timber.tag(FileEventListener.class.getSimpleName());
        this.appContext = applicationContext;
    }

    public void onEvent(int event, @NonNull File file) {
        if (file.isHidden()) return;

        if (file.isDirectory()) {
            handleDirectoryEvent(event, file);
        } else {
            handleFileEvent(event, file);
        }
    }

    /**
     * Handle some file event for a file which is not a directory
     * @param event the event mask. CLOSE_WRITE, DELETE & MOVE_SELF are handled
     * @param file the file concerned by the event
     */
    private void handleFileEvent(int event, @NonNull File file) {
        switch(event) {
            case FileObserver.CLOSE_WRITE: //todo it is called two times per file except if screenshot by example or take a picture
                handleFileCloseWrite(file);
                break;
            case FileObserver.DELETE:
                handleFileDelete(file);
                break;
            case FileObserver.MOVE_SELF: //todo to be able to catch that, we probably need a buffer to catch a succession (MOVE_FROM, MOVE_TO, then MOVE_SELF).
                Timber.d("%s has been moved. Not handled yet", file.getAbsolutePath());
                break;
            default:
                break;
        }
    }

    /**
     * Handle FileEvent for a directory
     * @param event FileEvent mask. CREATE, CLOSE_WRITE, DELETE, MOVE_SELF
     * @param dir directory concerned by file event
     */
    private void handleDirectoryEvent(int event, @NonNull File dir) {
        switch(event) {
            case FileObserver.CREATE:
                handleDirectoryCreate(dir);
                break;
            case FileObserver.CLOSE_WRITE:
                handleDirectoryCloseWrite(dir);
                break;
            case FileObserver.DELETE: //todo #1 Fix: never used. when done on a dir, it triggers handleFileEvent. Why ?!
                handleDirectoryDelete(dir);
                break;
            case FileObserver.MOVE_SELF:
                Timber.d("%s has been moved. Not handled yet", dir.getAbsolutePath());
                break;
            default:
                break;
        }
    }

    /**
     * Send syncRequest to SynchronizationService
     * @param request SyncRequest that should be executed asap
     */
    private void sendSyncRequestToSynchronizationService(@NonNull SyncRequest request) {
        final SyncRequestCollector syncManager = SyncProxy.INSTANCE;
        final boolean requestAdded = syncManager.queueSyncRequest(request, appContext.getApplicationContext());
        if (requestAdded) {
            Timber.d("Sending a SyncRequest for %s", request.getSyncedFileState().getName());
            syncManager.startSynchronization(appContext);
        }
    }

    /**
     * When a new directory is detected, it must be inserted in database
     * if it's parent directory is already in the database
     * @param directory Directory that has been created
     */
    private void handleDirectoryCreate(@NonNull File directory) {
        Timber.d("handleDirectoryCreate( %s )",directory.getAbsolutePath());
        final File parentFile = directory.getParentFile();
        if (parentFile == null) return;

        final String parentPath = getLocalPath(parentFile);
        final SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, appContext);
        if (parentFolder != null) {
            final SyncedFolder folder = new SyncedFolder(parentFolder, directory.getName() + PATH_SEPARATOR, directory.lastModified(), "");
            DbHelper.insertSyncedFolder(folder, appContext);
        }
    }

    /**
     * Handle CLOSE_WRITE event for a directory
     * todo: check in which condition a directory can generate a close_write
     * @param directory Directory that has been modified
     */
    private void handleDirectoryCloseWrite(@NonNull File directory) {
        final String fileLocalPath = getLocalPath(directory);
        Timber.d("handleDirectoryCloseWrite( %s )",fileLocalPath );
        final SyncedFolder folder = DbHelper.getSyncedFolderByLocalPath(fileLocalPath, appContext);
        if (folder == null) {
            handleDirectoryCreate(directory); //todo check if really relevant
        } else {  //It's a directory update
            folder.setLastModified(directory.lastModified());
            DbHelper.updateSyncedFolder(folder, appContext);
        }
    }

    /**
     * Handle a file deletion event for a directory
     * @param directory Directory that has been removed
     */
    private void handleDirectoryDelete(@NonNull File directory) {
        final String fileLocalPath = getLocalPath(directory);
        Timber.d("handleDirectoryDelete( %s )", fileLocalPath);
        SyncedFolder folder = DbHelper.getSyncedFolderByLocalPath(fileLocalPath, appContext);
        if (folder == null) {
            //look for parent
            final File parentFile = directory.getParentFile();
            if (parentFile == null) return;

            final String parentPath = getLocalPath(parentFile);
            final SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, appContext);
            if (parentFolder == null ) { //if parent is not in the DB
                return;
            }
            folder = new SyncedFolder(parentFolder, directory.getName() + PATH_SEPARATOR, directory.lastModified(), "");
            folder.setEnabled(false);
            DbHelper.insertSyncedFolder(folder, appContext);
        } else if (folder.isEnabled()) {
            folder.setEnabled(false);
            DbHelper.updateSyncedFolder(folder, appContext);
        }
    }

    /**
     * handle a file close_write event for a file which is not a directory
     * @param file File that has been modified
     */
    private void handleFileCloseWrite(@NonNull File file) {
        final String fileLocalPath = getLocalPath(file);
        Timber.d("handleFileCloseWrite( %s )", fileLocalPath);
        SyncRequest request = null;
        SyncedFileState fileState = DbHelper.loadSyncedFile( appContext, fileLocalPath, true);

        if (fileState == null) { //New file discovered
            final File parentFile = file.getParentFile();
            if (parentFile == null) return;

            final String parentPath = getLocalPath(file.getParentFile());
            SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, appContext);
            if (parentFolder == null || !parentFolder.isEnabled()) {
                Timber.d("Won't send sync request: no parent are known for new file: %s", file.getName());
                return;
            }
            int scanScope = DO_NOT_SCAN;
            if (parentFolder.isEnabled()) {
                if (parentFolder.isScanRemote()) scanScope = SCAN_ON_CLOUD;
                if (parentFolder.isScanLocal()) scanScope += SCAN_ON_DEVICE;
            }

            final String remotePath = parentFolder.getRemoteFolder()+file.getName();
            fileState = new SyncedFileState(-1, file.getName(), getLocalPath(file), remotePath, "", 0L, parentFolder.getId(), parentFolder.isMediaType(), scanScope);
            int storedId = DbHelper.manageSyncedFileStateDB(fileState, "INSERT", appContext);
            if (storedId > 0) {
                fileState.setId(storedId);
                request = new SyncRequest(fileState, UPLOAD);
            } else {
                Timber.d("New File %s observed but impossible to insert it in DB", file.getName());
            }
        } else { //File update
            final boolean isWaitingForDownload = fileState.isLastEtagStored() && fileState.getLastModified() == 0L;
            if (fileState.getScanScope() > SCAN_ON_CLOUD && !isWaitingForDownload) {
                request = new SyncRequest(fileState, UPLOAD);
            }
        }
        if (request != null) {
            sendSyncRequestToSynchronizationService(request);
        }
    }

    /**
     * Handle a file deletion event for a file which is not a directory
     * @param file File that has been removed
     */
    private void handleFileDelete(@NonNull File file) {
        final String fileLocalPath = getLocalPath(file);
        Timber.d("handleFileDelete( %s )",fileLocalPath);
        final SyncedFileState fileState = DbHelper.loadSyncedFile( appContext, fileLocalPath, true);
        if (fileState == null) {
            return; //Todo #1: should we call handleDirectoryDelete before to return ?
        }

        //If already in DB
        if (fileState.getScanScope() > DO_NOT_SCAN) {
            //todo: if file is already sync disabled, we should probably remove file from DB
            final SyncRequest disableSyncingRequest = new SyncRequest(fileState, DISABLE_SYNCING);
            this.sendSyncRequestToSynchronizationService(disableSyncingRequest);
        }
    }
}
Loading