Loading app/build.gradle +3 −1 Original line number Diff line number Diff line plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' } Loading Loading @@ -35,7 +36,7 @@ def getSentryDsn = { -> } android { compileSdk 31 compileSdk 33 defaultConfig { applicationId "foundation.e.drive" minSdk 26 Loading Loading @@ -99,6 +100,7 @@ dependencies { implementation 'com.google.android.material:material:1.6.0' implementation 'com.github.bumptech.glide:glide:4.14.2' implementation 'com.github.bumptech.glide:annotations:4.14.2' implementation 'androidx.core:core-ktx:+' annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2' implementation "androidx.work:work-runtime:2.7.1" implementation 'androidx.test:core:1.4.0' Loading app/src/main/AndroidManifest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/> <!-- for Android 30+ --> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> Loading app/src/main/java/foundation/e/drive/RecycleBin.kt 0 → 100644 +86 −0 Original line number Diff line number Diff line /* * Copyright © MURENA SAS 2023. * 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 */ @file:JvmName("RecycleBin") package foundation.e.drive import foundation.e.drive.utils.AppConstants import timber.log.Timber import java.io.File import java.io.IOException import java.nio.file.Files import kotlin.io.path.Path import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration /** * This class contains method for trashing file & cleaning the trash */ object RecycleBin { private val DELAY_FOR_DELETION = 30.toDuration(DurationUnit.DAYS) private val BIN_PATH = AppConstants.RECYCLE_BIN_PATH //TMP only, Need to find a way to get context /** * Remove files which are in recycle bin * for more than DELAY_FOR_DELETION * @return false as soon as some files that should be removed is not removed */ fun clearOldestFiles(): Boolean { val binDir = File(BIN_PATH) if (!binDir.exists()) return true try { val filesToRemove = binDir.listFiles { file -> computeTimeInBin(file.lastModified()) > DELAY_FOR_DELETION } filesToRemove?.forEach { file -> file?.delete() } } catch (exception: IOException) { //Note that some files might have already been removed Timber.e(exception, "Caught exception when clearing oldest file in bin") return false } return true } /** * Compute time from which file is in Bin * and return it as a Duration in days */ private fun computeTimeInBin(fileLastModified: Long): Duration { return (System.currentTimeMillis() - fileLastModified).toDuration(DurationUnit.DAYS) } /** * put a file into the bin */ fun trashFile(file: File): Boolean { File(BIN_PATH).mkdirs() //Assert that recycle bin exist if (file.exists()) { val targetPath = File(BIN_PATH, file.name).absolutePath try { val moveResult = Files.move(file.toPath(), Path(targetPath)) if (moveResult.toFile().exists()) { return true } } catch (exception: IOException) { Timber.e(exception) } catch (exception: SecurityException) { Timber.e(exception) } catch (exception: NullPointerException) { Timber.e(exception) } } Timber.d("Can't move %s to trashbin", file.absolutePath) return false } } No newline at end of file app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java +0 −4 Original line number Diff line number Diff line Loading @@ -82,16 +82,12 @@ public class RemoteContentScanner extends AbstractContentScanner<RemoteFile> { @Override protected void onMissingFile(SyncedFileState fileState) { //TODO: disabled file deletion feature for now to handle accidental file deletion. //Uncomment the following block when we resolve the issue: https://gitlab.e.foundation/e/backlog/-/issues/6711 /* if (!fileState.hasBeenSynchronizedOnce()) { return; } Timber.d("Add local deletion request for file: %s", fileState.getLocalPath()); this.syncRequests.put(fileState.getId(), new SyncRequest(fileState, LOCAL_DELETE)); */ } @Override Loading app/src/main/java/foundation/e/drive/services/InitializerService.java +55 −123 Original line number Diff line number Diff line /* * Copyright © CLEUS SAS 2018-2019. * Copyright © ECORP SAS 2022. * Copyright © ECORP SAS 2022-2023. * 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 Loading @@ -15,26 +15,18 @@ import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Environment; import android.os.IBinder; import com.owncloud.android.lib.common.OwnCloudClient; import java.util.ArrayList; import java.util.Arrays; import java.io.File; import java.util.List; import foundation.e.drive.models.SyncedFolder; import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.CommonUtils; import foundation.e.drive.utils.DavClientProvider; import foundation.e.drive.utils.RootSyncedFolderProvider; import timber.log.Timber; import static com.owncloud.android.lib.resources.files.FileUtils.PATH_SEPARATOR; import static foundation.e.drive.utils.AppConstants.MEDIA_SYNCABLE_CATEGORIES; import static foundation.e.drive.utils.AppConstants.SETTINGS_SYNCABLE_CATEGORIES; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.work.WorkManager; Loading @@ -44,8 +36,6 @@ import androidx.work.WorkManager; * @author Abhishek Aggarwal */ public class InitializerService extends Service { private List<SyncedFolder> syncedFolders; private OwnCloudClient cloudClient; private Account account; @Override Loading @@ -60,11 +50,8 @@ public class InitializerService extends Service { CommonUtils.setServiceUnCaughtExceptionHandler(this); //Get account SharedPreferences prefs = this.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); final SharedPreferences prefs = this.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); if (prefs.getBoolean(AppConstants.INITIALIZATION_HAS_BEEN_DONE, false)) { Timber.i("Initializer has already been done"); } else { String accountName = prefs.getString(AccountManager.KEY_ACCOUNT_NAME, ""); String accountType = prefs.getString(AccountManager.KEY_ACCOUNT_TYPE, ""); Loading @@ -78,111 +65,56 @@ public class InitializerService extends Service { .apply(); } if (accountName.isEmpty()) { Timber.d("Account's name not found"); stopSelf(); } else { this.account = CommonUtils.getAccount(accountName, accountType, AccountManager.get(this)); if (this.account != null) { this.cloudClient = DavClientProvider.getInstance().getClientInstance(account, getApplicationContext()); if (checkStartConditions(prefs, accountName, accountType)) { start(); } else { Timber.i("Got account is invalid"); stopSelf(); } } } return super.onStartCommand(intent, flags, startId); } public void start() { Timber.d("start()"); if (cloudClient == null) { stopSelf(); return; /** * Check if condition are present to start * - Initialization not already done * - AccountName is not empty * - Account available * @return true if condition are met */ private boolean checkStartConditions(@NonNull final SharedPreferences prefs, @NonNull final String accountName, @NonNull final String accountType) { if (prefs.getBoolean(AppConstants.INITIALIZATION_HAS_BEEN_DONE, false)) { Timber.w("Initialization has already been done"); return false; } CommonUtils.registerPeriodicUserInfoChecking(WorkManager.getInstance(this)); final List<String> syncCategories = new ArrayList<>(); syncCategories.addAll(Arrays.asList(MEDIA_SYNCABLE_CATEGORIES)); syncCategories.addAll(Arrays.asList(SETTINGS_SYNCABLE_CATEGORIES)); if (accountName.isEmpty()) { Timber.w("No account Name available"); return false; } getInitialSyncedFolders(syncCategories); CommonUtils.registerInitializationWorkers(syncedFolders, WorkManager.getInstance(getApplicationContext()) ); account = CommonUtils.getAccount(accountName, accountType, AccountManager.get(this)); if (account == null) { Timber.w("got Invalid %s account for username: %s ", accountType, accountName); return false; } return true; } /** * Return a list of SyncedFolder * @param categories categories indicating which syncedFolder to create * Set up base component for eDrive: * - Register basic worker * - build root folders to sync * - Create Recycle bin for eDrive */ private void getInitialSyncedFolders(List<String> categories) { Timber.d("getInitialSyncedFolders"); this.syncedFolders = new ArrayList<>(); for(int i=-1, size = categories.size(); ++i < size;) { final String DEVICE_SPECIFIC_PATH = PATH_SEPARATOR+"Devices"+PATH_SEPARATOR+ Build.BRAND+"_"+ Build.MODEL+"_" + Build.SERIAL; switch (categories.get(i)) { case "Images": syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_DCIM), "/Photos/", true)); syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_PICTURES), "/Pictures/", true)); break; case "Movies": syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_MOVIES), "/Movies/", true)); break; case "Music": syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_MUSIC), "/Music/", true)); break; case "Ringtones": syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_RINGTONES), "/Ringtones/", true)); break; case "Documents": syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_DOCUMENTS), "/Documents/", true)); break; case "Podcasts": syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_PODCASTS), "/Podcasts/", true)); break; case "Rom settings": String remoteFolderPath = DEVICE_SPECIFIC_PATH+"/rom_settings/"; syncedFolders.add(new SyncedFolder(categories.get(i), "/data/system/users/0/", remoteFolderPath, true, false, true, false)); try{ syncedFolders.add(new SyncedFolder( categories.get(i), getFilesDir().getCanonicalPath()+PATH_SEPARATOR, remoteFolderPath+"app_list/", true, false, CommonUtils.isSettingsSyncEnabled(account), false)); } catch (Exception exception) { Timber.e(exception); } break; } } } private void start() { Timber.d("start()"); private String getExternalFolder(String directory) { return CommonUtils.getLocalPath(Environment.getExternalStoragePublicDirectory(directory))+ PATH_SEPARATOR; } CommonUtils.registerPeriodicUserInfoChecking(WorkManager.getInstance(this)); @Override public void onDestroy() { super.onDestroy(); this.account = null; this.cloudClient = null; if (this.syncedFolders != null) this.syncedFolders.clear(); this.syncedFolders = null; final List<SyncedFolder> syncedFolders = RootSyncedFolderProvider.INSTANCE.getSyncedFolderRoots(getApplicationContext()); final boolean recycleBinCreated = new File(AppConstants.RECYCLE_BIN_PATH).mkdirs(); if (!recycleBinCreated) Timber.w("Cannot create recycle bin. It may be already existing"); CommonUtils.registerInitializationWorkers(syncedFolders, WorkManager.getInstance(getApplicationContext()) ); } @Nullable Loading Loading
app/build.gradle +3 −1 Original line number Diff line number Diff line plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' } Loading Loading @@ -35,7 +36,7 @@ def getSentryDsn = { -> } android { compileSdk 31 compileSdk 33 defaultConfig { applicationId "foundation.e.drive" minSdk 26 Loading Loading @@ -99,6 +100,7 @@ dependencies { implementation 'com.google.android.material:material:1.6.0' implementation 'com.github.bumptech.glide:glide:4.14.2' implementation 'com.github.bumptech.glide:annotations:4.14.2' implementation 'androidx.core:core-ktx:+' annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2' implementation "androidx.work:work-runtime:2.7.1" implementation 'androidx.test:core:1.4.0' Loading
app/src/main/AndroidManifest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/> <!-- for Android 30+ --> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> Loading
app/src/main/java/foundation/e/drive/RecycleBin.kt 0 → 100644 +86 −0 Original line number Diff line number Diff line /* * Copyright © MURENA SAS 2023. * 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 */ @file:JvmName("RecycleBin") package foundation.e.drive import foundation.e.drive.utils.AppConstants import timber.log.Timber import java.io.File import java.io.IOException import java.nio.file.Files import kotlin.io.path.Path import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration /** * This class contains method for trashing file & cleaning the trash */ object RecycleBin { private val DELAY_FOR_DELETION = 30.toDuration(DurationUnit.DAYS) private val BIN_PATH = AppConstants.RECYCLE_BIN_PATH //TMP only, Need to find a way to get context /** * Remove files which are in recycle bin * for more than DELAY_FOR_DELETION * @return false as soon as some files that should be removed is not removed */ fun clearOldestFiles(): Boolean { val binDir = File(BIN_PATH) if (!binDir.exists()) return true try { val filesToRemove = binDir.listFiles { file -> computeTimeInBin(file.lastModified()) > DELAY_FOR_DELETION } filesToRemove?.forEach { file -> file?.delete() } } catch (exception: IOException) { //Note that some files might have already been removed Timber.e(exception, "Caught exception when clearing oldest file in bin") return false } return true } /** * Compute time from which file is in Bin * and return it as a Duration in days */ private fun computeTimeInBin(fileLastModified: Long): Duration { return (System.currentTimeMillis() - fileLastModified).toDuration(DurationUnit.DAYS) } /** * put a file into the bin */ fun trashFile(file: File): Boolean { File(BIN_PATH).mkdirs() //Assert that recycle bin exist if (file.exists()) { val targetPath = File(BIN_PATH, file.name).absolutePath try { val moveResult = Files.move(file.toPath(), Path(targetPath)) if (moveResult.toFile().exists()) { return true } } catch (exception: IOException) { Timber.e(exception) } catch (exception: SecurityException) { Timber.e(exception) } catch (exception: NullPointerException) { Timber.e(exception) } } Timber.d("Can't move %s to trashbin", file.absolutePath) return false } } No newline at end of file
app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java +0 −4 Original line number Diff line number Diff line Loading @@ -82,16 +82,12 @@ public class RemoteContentScanner extends AbstractContentScanner<RemoteFile> { @Override protected void onMissingFile(SyncedFileState fileState) { //TODO: disabled file deletion feature for now to handle accidental file deletion. //Uncomment the following block when we resolve the issue: https://gitlab.e.foundation/e/backlog/-/issues/6711 /* if (!fileState.hasBeenSynchronizedOnce()) { return; } Timber.d("Add local deletion request for file: %s", fileState.getLocalPath()); this.syncRequests.put(fileState.getId(), new SyncRequest(fileState, LOCAL_DELETE)); */ } @Override Loading
app/src/main/java/foundation/e/drive/services/InitializerService.java +55 −123 Original line number Diff line number Diff line /* * Copyright © CLEUS SAS 2018-2019. * Copyright © ECORP SAS 2022. * Copyright © ECORP SAS 2022-2023. * 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 Loading @@ -15,26 +15,18 @@ import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Environment; import android.os.IBinder; import com.owncloud.android.lib.common.OwnCloudClient; import java.util.ArrayList; import java.util.Arrays; import java.io.File; import java.util.List; import foundation.e.drive.models.SyncedFolder; import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.CommonUtils; import foundation.e.drive.utils.DavClientProvider; import foundation.e.drive.utils.RootSyncedFolderProvider; import timber.log.Timber; import static com.owncloud.android.lib.resources.files.FileUtils.PATH_SEPARATOR; import static foundation.e.drive.utils.AppConstants.MEDIA_SYNCABLE_CATEGORIES; import static foundation.e.drive.utils.AppConstants.SETTINGS_SYNCABLE_CATEGORIES; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.work.WorkManager; Loading @@ -44,8 +36,6 @@ import androidx.work.WorkManager; * @author Abhishek Aggarwal */ public class InitializerService extends Service { private List<SyncedFolder> syncedFolders; private OwnCloudClient cloudClient; private Account account; @Override Loading @@ -60,11 +50,8 @@ public class InitializerService extends Service { CommonUtils.setServiceUnCaughtExceptionHandler(this); //Get account SharedPreferences prefs = this.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); final SharedPreferences prefs = this.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); if (prefs.getBoolean(AppConstants.INITIALIZATION_HAS_BEEN_DONE, false)) { Timber.i("Initializer has already been done"); } else { String accountName = prefs.getString(AccountManager.KEY_ACCOUNT_NAME, ""); String accountType = prefs.getString(AccountManager.KEY_ACCOUNT_TYPE, ""); Loading @@ -78,111 +65,56 @@ public class InitializerService extends Service { .apply(); } if (accountName.isEmpty()) { Timber.d("Account's name not found"); stopSelf(); } else { this.account = CommonUtils.getAccount(accountName, accountType, AccountManager.get(this)); if (this.account != null) { this.cloudClient = DavClientProvider.getInstance().getClientInstance(account, getApplicationContext()); if (checkStartConditions(prefs, accountName, accountType)) { start(); } else { Timber.i("Got account is invalid"); stopSelf(); } } } return super.onStartCommand(intent, flags, startId); } public void start() { Timber.d("start()"); if (cloudClient == null) { stopSelf(); return; /** * Check if condition are present to start * - Initialization not already done * - AccountName is not empty * - Account available * @return true if condition are met */ private boolean checkStartConditions(@NonNull final SharedPreferences prefs, @NonNull final String accountName, @NonNull final String accountType) { if (prefs.getBoolean(AppConstants.INITIALIZATION_HAS_BEEN_DONE, false)) { Timber.w("Initialization has already been done"); return false; } CommonUtils.registerPeriodicUserInfoChecking(WorkManager.getInstance(this)); final List<String> syncCategories = new ArrayList<>(); syncCategories.addAll(Arrays.asList(MEDIA_SYNCABLE_CATEGORIES)); syncCategories.addAll(Arrays.asList(SETTINGS_SYNCABLE_CATEGORIES)); if (accountName.isEmpty()) { Timber.w("No account Name available"); return false; } getInitialSyncedFolders(syncCategories); CommonUtils.registerInitializationWorkers(syncedFolders, WorkManager.getInstance(getApplicationContext()) ); account = CommonUtils.getAccount(accountName, accountType, AccountManager.get(this)); if (account == null) { Timber.w("got Invalid %s account for username: %s ", accountType, accountName); return false; } return true; } /** * Return a list of SyncedFolder * @param categories categories indicating which syncedFolder to create * Set up base component for eDrive: * - Register basic worker * - build root folders to sync * - Create Recycle bin for eDrive */ private void getInitialSyncedFolders(List<String> categories) { Timber.d("getInitialSyncedFolders"); this.syncedFolders = new ArrayList<>(); for(int i=-1, size = categories.size(); ++i < size;) { final String DEVICE_SPECIFIC_PATH = PATH_SEPARATOR+"Devices"+PATH_SEPARATOR+ Build.BRAND+"_"+ Build.MODEL+"_" + Build.SERIAL; switch (categories.get(i)) { case "Images": syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_DCIM), "/Photos/", true)); syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_PICTURES), "/Pictures/", true)); break; case "Movies": syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_MOVIES), "/Movies/", true)); break; case "Music": syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_MUSIC), "/Music/", true)); break; case "Ringtones": syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_RINGTONES), "/Ringtones/", true)); break; case "Documents": syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_DOCUMENTS), "/Documents/", true)); break; case "Podcasts": syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_PODCASTS), "/Podcasts/", true)); break; case "Rom settings": String remoteFolderPath = DEVICE_SPECIFIC_PATH+"/rom_settings/"; syncedFolders.add(new SyncedFolder(categories.get(i), "/data/system/users/0/", remoteFolderPath, true, false, true, false)); try{ syncedFolders.add(new SyncedFolder( categories.get(i), getFilesDir().getCanonicalPath()+PATH_SEPARATOR, remoteFolderPath+"app_list/", true, false, CommonUtils.isSettingsSyncEnabled(account), false)); } catch (Exception exception) { Timber.e(exception); } break; } } } private void start() { Timber.d("start()"); private String getExternalFolder(String directory) { return CommonUtils.getLocalPath(Environment.getExternalStoragePublicDirectory(directory))+ PATH_SEPARATOR; } CommonUtils.registerPeriodicUserInfoChecking(WorkManager.getInstance(this)); @Override public void onDestroy() { super.onDestroy(); this.account = null; this.cloudClient = null; if (this.syncedFolders != null) this.syncedFolders.clear(); this.syncedFolders = null; final List<SyncedFolder> syncedFolders = RootSyncedFolderProvider.INSTANCE.getSyncedFolderRoots(getApplicationContext()); final boolean recycleBinCreated = new File(AppConstants.RECYCLE_BIN_PATH).mkdirs(); if (!recycleBinCreated) Timber.w("Cannot create recycle bin. It may be already existing"); CommonUtils.registerInitializationWorkers(syncedFolders, WorkManager.getInstance(getApplicationContext()) ); } @Nullable Loading