diff --git a/app/build.gradle b/app/build.gradle index 545e0bc58793185ddcabb0657a5d4243f64bf56d..d705ff3ca76652e3b76be8018efb079830603117 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,11 +1,12 @@ plugins { id 'com.android.application' + id 'org.jetbrains.kotlin.android' } def versionMajor = 1 def versionMinor = 3 -def versionPatch = 4 +def versionPatch = 7 def getTestProp(String propName) { def result = "" @@ -35,7 +36,7 @@ def getSentryDsn = { -> } android { - compileSdk 31 + compileSdk 33 defaultConfig { applicationId "foundation.e.drive" minSdk 26 @@ -99,12 +100,13 @@ 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' implementation 'com.jakewharton.timber:timber:5.0.1' implementation 'foundation.e:elib:0.0.1-alpha11' - implementation 'foundation.e.lib:telemetry:0.0.4-alpha' + implementation 'foundation.e.lib:telemetry:0.0.7-alpha' androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1c47ced4b8f66e63166711e90003ef99f349d1ca..22dabee08b53b0e563e8265460684282812416a1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + @@ -83,14 +84,6 @@ - - - - - @@ -113,5 +106,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/EdriveApplication.java b/app/src/main/java/foundation/e/drive/EdriveApplication.java index 0ba4332015df5519bc7cefe176af79481a9bf521..ac1765a248de6ddb58faff617f90143f623e09b7 100644 --- a/app/src/main/java/foundation/e/drive/EdriveApplication.java +++ b/app/src/main/java/foundation/e/drive/EdriveApplication.java @@ -44,7 +44,7 @@ public class EdriveApplication extends Application { if (BuildConfig.DEBUG) { Timber.plant(new DebugTree()); } else { - Telemetry.INSTANCE.init(BuildConfig.SENTRY_DSN, this); + Telemetry.init(BuildConfig.SENTRY_DSN, this, true); Timber.plant(new ReleaseTree()); } Timber.tag("EdriveApplication"); @@ -98,6 +98,6 @@ public class EdriveApplication extends Application { @Override public void onLowMemory() { super.onLowMemory(); - Timber.w("System is low on memory. Application might get killed by the system."); + Timber.i("System is low on memory. Application might get killed by the system."); } } diff --git a/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java b/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java index 57c342c86c4eb6c08a1538774b2632be49a11b78..77c55b3629804685ae3ba467bbbcb7b0f2991104 100644 --- a/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java +++ b/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java @@ -184,7 +184,7 @@ public class FileEventListener { final String parentPath = CommonUtils.getLocalPath(file.getParentFile()); SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, appContext); if (parentFolder == null || !parentFolder.isEnabled()) { - Timber.w("Won't send sync request: no parent are known for new file: %s", file.getName()); + Timber.d("Won't send sync request: no parent are known for new file: %s", file.getName()); return; } int scannableValue = 0; diff --git a/app/src/main/java/foundation/e/drive/RecycleBin.kt b/app/src/main/java/foundation/e/drive/RecycleBin.kt new file mode 100644 index 0000000000000000000000000000000000000000..447106bbf12db6f6f755bf326bafa90a055adb16 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/RecycleBin.kt @@ -0,0 +1,86 @@ +/* + * 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 diff --git a/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java b/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java index 654fde1d8bd4d60fd5d4f090dee4a4ba89420f9c..a5b271d508e6f6c497437b456cf85c15ccce546a 100644 --- a/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java +++ b/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java @@ -122,8 +122,11 @@ public class DownloadFileOperation extends RemoteOperation { return FILE_NOT_FOUND; } - if (tmpFile.length() != remoteFile.getLength()) { - Timber.d("Local and remote version of %s doesn't match", remoteFile.getRemotePath()); + final long tmpFileSize = tmpFile.length(); + final long remoteFileSize = remoteFile.getLength(); + + if (tmpFileSize != remoteFileSize) { + Timber.d("Local file size (%s) and remote file size (%s) of %s doesn't match", tmpFileSize, remoteFileSize, remoteFile.getRemotePath()); tmpFile.delete(); return INVALID_OVERWRITE; } @@ -134,6 +137,7 @@ public class DownloadFileOperation extends RemoteOperation { return FORBIDDEN; } + targetFile.setLastModified(remoteFile.getModifiedTimestamp()); syncedFileState.setLocalLastModified(targetFile.lastModified()) .setLastETAG(remoteFile.getEtag()); diff --git a/app/src/main/java/foundation/e/drive/receivers/AccountRemoveCallbackReceiver.java b/app/src/main/java/foundation/e/drive/receivers/AccountRemoveCallbackReceiver.java new file mode 100644 index 0000000000000000000000000000000000000000..6a95cef6e38de915298bfccbb4c41d288ec34d39 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/receivers/AccountRemoveCallbackReceiver.java @@ -0,0 +1,137 @@ +/* + * Copyright © ECORP 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 + */ + +package foundation.e.drive.receivers; + +import static foundation.e.drive.utils.AppConstants.INITIALFOLDERS_NUMBER; +import static foundation.e.drive.utils.AppConstants.INITIALIZATION_HAS_BEEN_DONE; + +import android.accounts.AccountManager; +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; + +import java.io.File; + +import foundation.e.drive.EdriveApplication; +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.database.FailedSyncPrefsManager; +import foundation.e.drive.services.InitializerService; +import foundation.e.drive.services.ObserverService; +import foundation.e.drive.services.SynchronizationService; +import foundation.e.drive.utils.AppConstants; +import timber.log.Timber; + +public class AccountRemoveCallbackReceiver extends BroadcastReceiver { + + @SuppressLint("UnsafeProtectedBroadcastReceiver") + @Override + public void onReceive(Context context, Intent intent) { + if (context == null || intent == null) { + return; + } + + final Context applicationContext = context.getApplicationContext(); + + final SharedPreferences preferences = applicationContext.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); + + if (!shouldProceedWithRemoval(intent, preferences)) { + return; + } + + stopRecursiveFileObserver(applicationContext); + stopAllServices(applicationContext); + deleteDatabase(applicationContext); + cleanSharedPreferences(applicationContext, preferences); + removeCachedFiles(applicationContext); + } + + private void deleteDatabase(@NonNull Context applicationContext) { + final boolean result = applicationContext.deleteDatabase(DbHelper.DATABASE_NAME); + Timber.d("Remove Database: %s", result); + } + + private void stopRecursiveFileObserver(@NonNull Context applicationContext) { + if (applicationContext instanceof EdriveApplication) { + ((EdriveApplication) applicationContext).stopRecursiveFileObserver(); + } + } + + private boolean shouldProceedWithRemoval(@NonNull Intent intent, @NonNull SharedPreferences preferences) { + if (isInvalidAction(intent)) { + return false; + } + + String currentAccount = preferences.getString(AccountManager.KEY_ACCOUNT_NAME, ""); + String currentAccountType = preferences.getString(AccountManager.KEY_ACCOUNT_TYPE, ""); + + if (currentAccount.isEmpty() || currentAccountType.isEmpty()) { + return false; + } + + final String accountType = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_TYPE); + final String accountName = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_NAME); + + return (currentAccount.equals(accountName) && currentAccountType.equals(accountType)); + } + + private boolean isInvalidAction(@NonNull Intent intent) { + return !"android.accounts.action.ACCOUNT_REMOVED".equals(intent.getAction()); + } + + private void stopAllServices(@NonNull Context applicationContext) { + Intent observerServiceIntent = new Intent(applicationContext, ObserverService.class); + boolean observerServiceStopResult = applicationContext.stopService(observerServiceIntent); + Timber.d("stop ObserverService: %s", observerServiceStopResult); + + Intent initializerServiceIntent = new Intent(applicationContext, InitializerService.class); + boolean initServiceStopResult = applicationContext.stopService(initializerServiceIntent); + Timber.d("stop InitializerService: %s", initServiceStopResult); + + Intent synchronizationServiceIntent = new Intent(applicationContext, SynchronizationService.class); + boolean syncServiceStopResult = applicationContext.stopService(synchronizationServiceIntent); + Timber.d("stop SynchronizationService: %s", syncServiceStopResult); + } + + private void cleanSharedPreferences(@NonNull Context applicationContext, @NonNull SharedPreferences prefs) { + if (!applicationContext.deleteSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME)) { + //If removal failed, clear all data inside + prefs.edit().remove(AccountManager.KEY_ACCOUNT_NAME) + .remove(AccountManager.KEY_ACCOUNT_TYPE) + .remove(INITIALIZATION_HAS_BEEN_DONE) + .remove(INITIALFOLDERS_NUMBER) + .remove(AppConstants.KEY_LAST_SYNC_TIME) + .apply(); + } + + applicationContext.deleteSharedPreferences(FailedSyncPrefsManager.PREF_NAME); + } + + private void removeCachedFiles(@NonNull Context applicationContext) { + final File[] cachedFiles = applicationContext.getExternalCacheDir().listFiles(); + if (cachedFiles == null) { + Timber.w("listFiles() returned null. preventing a NPE"); + return; + } + + for (File file : cachedFiles) { + try { + boolean removed = file.delete(); + if (!removed) { + Timber.w("Failed to remove the cached file: %s", file.getName()); + } + } catch (SecurityException e) { + Timber.e(e, "failed to delete cached file on account removal call"); + } + } + } +} diff --git a/app/src/main/java/foundation/e/drive/services/InitializerService.java b/app/src/main/java/foundation/e/drive/services/InitializerService.java index 6b310f4952aa426ac535b5984b4ebb146e1f860b..c6438174e77c8ab39a944da563b732032db9a8ce 100644 --- a/app/src/main/java/foundation/e/drive/services/InitializerService.java +++ b/app/src/main/java/foundation/e/drive/services/InitializerService.java @@ -1,6 +1,6 @@ /* * 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 @@ -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; @@ -44,8 +36,6 @@ import androidx.work.WorkManager; * @author Abhishek Aggarwal */ public class InitializerService extends Service { - private List syncedFolders; - private OwnCloudClient cloudClient; private Account account; @Override @@ -60,130 +50,71 @@ 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.w("Initializer has already been done"); - } else { - String accountName = prefs.getString(AccountManager.KEY_ACCOUNT_NAME, ""); - String accountType = prefs.getString(AccountManager.KEY_ACCOUNT_TYPE, ""); - - if (accountName.isEmpty() && accountType.isEmpty() && intent.getExtras() != null) { - - accountName = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_NAME, ""); - accountType = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_TYPE, ""); - - prefs.edit().putString(AccountManager.KEY_ACCOUNT_NAME, accountName) - .putString(AccountManager.KEY_ACCOUNT_TYPE, accountType) - .apply(); - } - - if (accountName.isEmpty()) { - Timber.w("Account's name not found"); - stopSelf(); - } else { - this.account = CommonUtils.getAccount(accountName, accountType, AccountManager.get(this)); - //Get OwnCloudlient - if (this.account != null) { - this.cloudClient = DavClientProvider.getInstance().getClientInstance(account, getApplicationContext()); - start(); - } else { - Timber.w("Got account is invalid"); - stopSelf(); - } - } + String accountName = prefs.getString(AccountManager.KEY_ACCOUNT_NAME, ""); + String accountType = prefs.getString(AccountManager.KEY_ACCOUNT_TYPE, ""); + + if (accountName.isEmpty() && accountType.isEmpty() && intent.getExtras() != null) { + + accountName = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_NAME, ""); + accountType = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_TYPE, ""); + + prefs.edit().putString(AccountManager.KEY_ACCOUNT_NAME, accountName) + .putString(AccountManager.KEY_ACCOUNT_TYPE, accountType) + .apply(); + } + + if (checkStartConditions(prefs, accountName, accountType)) { + start(); } 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 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 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()"); + + CommonUtils.registerPeriodicUserInfoChecking(WorkManager.getInstance(this)); - private String getExternalFolder(String directory) { - return CommonUtils.getLocalPath(Environment.getExternalStoragePublicDirectory(directory))+ PATH_SEPARATOR; - } + final List syncedFolders = RootSyncedFolderProvider.INSTANCE.getSyncedFolderRoots(getApplicationContext()); - @Override - public void onDestroy() { - super.onDestroy(); - this.account = null; - this.cloudClient = null; - if (this.syncedFolders != null) this.syncedFolders.clear(); - this.syncedFolders = null; + 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 diff --git a/app/src/main/java/foundation/e/drive/services/ObserverService.java b/app/src/main/java/foundation/e/drive/services/ObserverService.java index 46b351bfa09913c8ba9fc950e6aa3401faa39cd2..a9b761050bc3d625475002494ac2af23d0ad0cd2 100644 --- a/app/src/main/java/foundation/e/drive/services/ObserverService.java +++ b/app/src/main/java/foundation/e/drive/services/ObserverService.java @@ -103,7 +103,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene @Override public int onStartCommand(Intent intent, int flags, int startId) { - Timber.i("onStartCommand(%s)", startId); + Timber.v("onStartCommand(%s)", startId); CommonUtils.setServiceUnCaughtExceptionHandler(this); @@ -134,27 +134,27 @@ public class ObserverService extends Service implements OnRemoteOperationListene * @return false if at least one condition is false */ private boolean checkStartCondition(final SharedPreferences prefs, final boolean forcedSync) { - Timber.d("checkStartCondition()"); + Timber.v("checkStartCondition()"); if (mAccount == null) { - Timber.w("No account registered"); + Timber.d("No account registered"); return false; } if (!CommonUtils.isMediaSyncEnabled(mAccount) && !CommonUtils.isSettingsSyncEnabled(mAccount)) { - Timber.w("Synchronization has been disabled in account's settings"); + Timber.d("Synchronization has been disabled in account's settings"); return false; } if (!prefs.getBoolean(INITIALIZATION_HAS_BEEN_DONE, false)) { - Timber.w("Initialization hasn't been done"); + Timber.d("Initialization hasn't been done"); Intent initializerIntent = new Intent(this, InitializerService.class); startService(initializerIntent); return false; } if (isWorking) { - Timber.w("ObserverService is already working"); + Timber.d("ObserverService is already working"); return false; } @@ -164,14 +164,14 @@ public class ObserverService extends Service implements OnRemoteOperationListene final long lastSyncTime = prefs.getLong(AppConstants.KEY_LAST_SYNC_TIME, 0L); final long currentTime = System.currentTimeMillis(); if (!forcedSync && (currentTime - lastSyncTime ) < INTERSYNC_MINIMUM_DELAY ) { - Timber.w("Delay between now and last call is too short"); + Timber.d("Delay between now and last call is too short"); return false; } final boolean meteredNetworkAllowed = CommonUtils.isMeteredNetworkAllowed(mAccount); //check for the case where intent has been launched by initializerService if (!CommonUtils.haveNetworkConnection(this, meteredNetworkAllowed)) { - Timber.w("There is no allowed internet connexion."); + Timber.d("There is no allowed internet connexion."); return false; } @@ -201,13 +201,13 @@ public class ObserverService extends Service implements OnRemoteOperationListene Timber.i("deleteOldestCrashLogs()"); final File externalFilesDir = getExternalFilesDir(ServiceExceptionHandler.CRASH_LOG_FOLDER); if (externalFilesDir == null) { - Timber.e("getExternalFilesDir() returned null. Preventing a NPE"); + Timber.d("getExternalFilesDir() returned null. Preventing a NPE"); return; } final File[] fileToRemove = externalFilesDir.listFiles(new CrashlogsFileFilter()); if (fileToRemove == null) { - Timber.e("getExternalFilesDir() returned null. Preventing a NPE"); + Timber.d("getExternalFilesDir() returned null. Preventing a NPE"); return; } @@ -222,7 +222,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene /** * Clear cached file unused: - * remove each cached file which isn't in OperationManagerService.lockedSyncedFileState(); */ private void clearCachedFile(){ Timber.i("clearCachedFile()"); @@ -249,7 +248,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene this.mSyncedFolders = loadSyncedFolders(); if (mSyncedFolders.isEmpty()) { - Timber.w("List of synced folders is empty"); + Timber.d("List of synced folders is empty"); this.stopSelf(); return; } @@ -257,7 +256,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene if (remote) { final OwnCloudClient client = DavClientProvider.getInstance().getClientInstance(mAccount, getApplicationContext()); if (client == null) { - Timber.w("OwnCloudClient is null"); + Timber.d("OwnCloudClient is null"); return; } @@ -305,7 +304,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene if (!(operation instanceof ListFileRemoteOperation)) return; if (!result.isSuccess()) { - Timber.w("ListRemoteFileOperation failed. Http code: %s", result.getHttpCode()); + Timber.d("ListRemoteFileOperation failed. Http code: %s", result.getHttpCode()); } final List remoteFiles = ((RemoteOperationResult>)result).getResultData(); @@ -333,7 +332,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene Timber.d("syncRequests contains %s", syncRequests.size()); passSyncRequestsToSynchronizationService(); } else { - Timber.w("There is no file to sync."); + Timber.i("There is no file to sync."); getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE) .edit() .putLong(AppConstants.KEY_LAST_SYNC_TIME, System.currentTimeMillis()) diff --git a/app/src/main/java/foundation/e/drive/services/ResetService.java b/app/src/main/java/foundation/e/drive/services/ResetService.java deleted file mode 100644 index a5cfb928dc4ba2d160dcb7fc64d9dc11a55093e8..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/services/ResetService.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright © CLEUS SAS 2018-2019. - * 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.services; - -import android.accounts.AccountManager; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.IBinder; - -import java.io.File; - -import foundation.e.drive.EdriveApplication; -import foundation.e.drive.database.DbHelper; -import foundation.e.drive.database.FailedSyncPrefsManager; -import foundation.e.drive.utils.AppConstants; -import timber.log.Timber; - -import static foundation.e.drive.utils.AppConstants.INITIALFOLDERS_NUMBER; -import static foundation.e.drive.utils.AppConstants.INITIALIZATION_HAS_BEEN_DONE; - -import androidx.annotation.Nullable; -import androidx.work.WorkManager; - -/** - * @author Vincent Bourgmayer - * Service which stop others, remove DB, clear sharedPrefs and unregister ScreenOffReceiver - */ -public class ResetService extends Service { - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - Timber.tag(ResetService.class.getSimpleName()).i("onStartCommand()"); - if ( intent.getExtras() != null ) { - final String intent_accountName = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_NAME, ""); - final String intent_accountType = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_TYPE, ""); - - if (!intent_accountName.isEmpty() && !intent_accountType.isEmpty()) { - final SharedPreferences prefs = this.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); - - if (intent_accountName.equals(prefs.getString(AccountManager.KEY_ACCOUNT_NAME, "")) - && intent_accountType.equals(prefs.getString(AccountManager.KEY_ACCOUNT_TYPE, ""))) { - WorkManager.getInstance(this).cancelAllWorkByTag(AppConstants.WORK_GENERIC_TAG); - ( (EdriveApplication) this.getApplication() ).stopRecursiveFileObserver(); - - stopAllServices(); - - final boolean result = deleteDatabase(DbHelper.DATABASE_NAME); - Timber.d("Remove Database: %s", result); - - cleanSharedPreferences(prefs); - removeCachedFiles(); - } - } - } - stopSelf(); - return super.onStartCommand(intent, flags, startId); - } - - - private void stopAllServices() { - Intent stopperIntent = new Intent(getApplicationContext(), ObserverService.class); - boolean result = getApplicationContext().stopService( stopperIntent ); - Timber.d("stop ObserverService: %s", result); - - stopperIntent = new Intent(getApplicationContext(), InitializerService.class); - result = getApplicationContext().stopService( stopperIntent ); - Timber.d("stop InitializerService: %s", result); - - stopperIntent = new Intent(getApplicationContext(), SynchronizationService.class); - result = getApplicationContext().stopService( stopperIntent ); - - Timber.d("stop SynchronizationService: %s", result); - } - - private void cleanSharedPreferences(SharedPreferences prefs) { - if (!this.deleteSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME)) { - - //If removal failed, clear all data inside - prefs.edit().remove(AccountManager.KEY_ACCOUNT_NAME) - .remove(AccountManager.KEY_ACCOUNT_TYPE) - .putBoolean(INITIALIZATION_HAS_BEEN_DONE, false) - .remove(INITIALFOLDERS_NUMBER) - .remove(AppConstants.KEY_LAST_SYNC_TIME) - .apply(); - } - - deleteSharedPreferences(FailedSyncPrefsManager.PREF_NAME); - } - - private void removeCachedFiles() { - final File[] cachedFiles = this.getApplicationContext().getExternalCacheDir().listFiles(); - if (cachedFiles == null) { - Timber.e("listFiles() returned null. preventing a NPE"); - return; - } - - for (File f : cachedFiles) { - f.delete(); - } - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return null; - } -} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/services/SynchronizationService.java b/app/src/main/java/foundation/e/drive/services/SynchronizationService.java index 0eaa40ffa49487a86f45b50551470589d8647b8d..78c87b07e86f748415d8f713305332b3c89f9e8e 100644 --- a/app/src/main/java/foundation/e/drive/services/SynchronizationService.java +++ b/app/src/main/java/foundation/e/drive/services/SynchronizationService.java @@ -82,7 +82,7 @@ public class SynchronizationService extends Service implements OnRemoteOperation account = CommonUtils.getAccount(accountName, accountType, AccountManager.get(this)); if (account == null) { - Timber.w("No account available"); + Timber.d("No account available"); stopSelf(); return START_NOT_STICKY; } @@ -113,12 +113,6 @@ public class SynchronizationService extends Service implements OnRemoteOperation return binder; } - @Override - public void onLowMemory() { - Timber.w("System is low on memory. Service might get killed."); - } - - /** * Try to pause synchronization. * Can only be paused if no sync is already running and no request queue is started @@ -242,6 +236,7 @@ public class SynchronizationService extends Service implements OnRemoteOperation final SyncWrapper syncWrapper = new SyncWrapper(request, account, getApplicationContext()); if (request.getOperationType().equals(SyncRequest.Type.LOCAL_DELETE)) { + Timber.v(" starts " + request.getSyncedFileState().getName() + " local deletion on thread " + threadIndex); final LocalFileDeleter fileDeleter = new LocalFileDeleter(request.getSyncedFileState()); @@ -297,38 +292,38 @@ public class SynchronizationService extends Service implements OnRemoteOperation break; case SYNC_CONFLICT: //Case specific to UploadFileOperation - Timber.e("%s : Sync_conflict : File is already up to date", operationClassName); + Timber.d("%s : Sync_conflict : File is already up to date", operationClassName); break; case INVALID_OVERWRITE: - Timber.e("%s => invalid_overwrite :\n remote file and local file doesn't have the same size", operationClassName); + Timber.d("%s => invalid_overwrite :\n remote file and local file doesn't have the same size", operationClassName); break; case UNKNOWN_ERROR: if (callerOperation instanceof UploadFileOperation) { final int rowAffected = DbHelper.forceFoldertoBeRescan(((UploadFileOperation) callerOperation).getSyncedState().getId(), getApplicationContext()); - Timber.e("Upload failed for unknown reason.\n Force folder to be rescan next time (row affected) : %s", rowAffected); + Timber.d("Upload failed for unknown reason.\n Force folder to be rescan next time (row affected) : %s", rowAffected); } else if (callerOperation instanceof DownloadFileOperation) { - Timber.e("Download: Unknown_error : failed"); + Timber.d("Download: Unknown_error : failed"); } break; case FORBIDDEN: if (callerOperation instanceof UploadFileOperation) { final int rowAffected = DbHelper.forceFoldertoBeRescan(((UploadFileOperation) callerOperation).getSyncedState().getId(), getApplicationContext()); - Timber.e("Upload: Forbidden : Can't get syncedFileState, no remote path defined. Force folder to be rescan next time (row affected) : %s", rowAffected); + Timber.d("Upload: Forbidden : Can't get syncedFileState, no remote path defined. Force folder to be rescan next time (row affected) : %s", rowAffected); } else if (callerOperation instanceof DownloadFileOperation) { - Timber.e("Download : Forbidden: Can't get syncedFileState, no local path defined"); + Timber.d("Download : Forbidden: Can't get syncedFileState, no local path defined"); } break; case QUOTA_EXCEEDED: //Case specific to UploadFileOperation - Timber.w("Quota_EXCEEDED"); + Timber.d("Quota_EXCEEDED"); break; case FILE_NOT_FOUND: //Case specific to DownloadFileOperation - Timber.e("%s : File_not_found: File not found after download", operationClassName); + Timber.d("%s : File_not_found: File not found after download", operationClassName); break; case ETAG_UNCHANGED: //Case specific to DownloadFileOperation - Timber.e("%s : Sync_conflict: File is already up to date", operationClassName); + Timber.d("%s : Sync_conflict: File is already up to date", operationClassName); break; } } diff --git a/app/src/main/java/foundation/e/drive/utils/AppConstants.java b/app/src/main/java/foundation/e/drive/utils/AppConstants.java index bad256e6ab2ec8b0e8698d509008d26dd305c1d0..0d907fc37a1f13b464f65b80a487e732363054b7 100644 --- a/app/src/main/java/foundation/e/drive/utils/AppConstants.java +++ b/app/src/main/java/foundation/e/drive/utils/AppConstants.java @@ -1,6 +1,6 @@ /* * 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 @@ -9,7 +9,10 @@ package foundation.e.drive.utils; +import static com.owncloud.android.lib.resources.files.FileUtils.PATH_SEPARATOR; + import android.os.Build; +import android.os.Environment; import java.text.SimpleDateFormat; import java.util.Locale; @@ -47,6 +50,8 @@ public abstract class AppConstants { public static final String WORK_INITIALIZATION_TAG = "eDrive-init"; public static final String USER_AGENT = "eos(" + getBuildTime() + ")-eDrive(" + BuildConfig.VERSION_NAME + ")"; + public static final String RECYCLE_BIN_PATH = Environment.getExternalStorageDirectory() + PATH_SEPARATOR + "Sync trash"; + /** * Get a readable OS's build date String * diff --git a/app/src/main/java/foundation/e/drive/utils/ReleaseTree.java b/app/src/main/java/foundation/e/drive/utils/ReleaseTree.java index dbc19e127fe63ebe8344fdc19ead984c5eb645b8..48e3385d04da198290355299a103e6fa30017222 100644 --- a/app/src/main/java/foundation/e/drive/utils/ReleaseTree.java +++ b/app/src/main/java/foundation/e/drive/utils/ReleaseTree.java @@ -12,7 +12,6 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import foundation.e.lib.telemetry.Telemetry; import timber.log.Timber; /** @@ -31,10 +30,6 @@ public class ReleaseTree extends Timber.DebugTree{ return; } - if (priority >= Log.WARN) { - Telemetry.INSTANCE.reportMessage(tag + " " + message); - } - super.log(priority, tag, message, throwable); } } diff --git a/app/src/main/java/foundation/e/drive/utils/RootSyncedFolderProvider.kt b/app/src/main/java/foundation/e/drive/utils/RootSyncedFolderProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..2c3d46b90f3c5039e862da228194585dd91ab558 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/utils/RootSyncedFolderProvider.kt @@ -0,0 +1,118 @@ +/* + * 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 + */ +package foundation.e.drive.utils + +import android.content.Context +import android.os.Build +import android.os.Build.BRAND +import android.os.Build.MODEL +import android.os.Environment +import android.os.Environment.DIRECTORY_PODCASTS +import android.os.Environment.DIRECTORY_MUSIC +import android.os.Environment.DIRECTORY_DCIM +import android.os.Environment.DIRECTORY_DOCUMENTS +import android.os.Environment.DIRECTORY_PICTURES +import android.os.Environment.DIRECTORY_RINGTONES +import android.os.Environment.DIRECTORY_MOVIES +import com.owncloud.android.lib.resources.files.FileUtils.PATH_SEPARATOR +import foundation.e.drive.models.SyncedFolder + +object RootSyncedFolderProvider { + private const val CATEGORY_IMAGES = "Images" + private const val CATEGORY_MOVIES = "Movies" + private const val CATEGORY_MUSIC = "Music" + private const val CATEGORY_RINGTONES = "Ringtones" + private const val CATEGORY_DOCUMENTS = "Documents" + private const val CATEGORY_PODCASTS = "Podcasts" + private const val CATEGORY_ROM_SETTINGS = "Rom settings" + + private const val LOCAL_ROM_SETTINGS_PATH = "/data/system/users/0/" + private val REMOTE_ROM_SETTINGS_PATH = PATH_SEPARATOR + "Devices" + PATH_SEPARATOR + BRAND + "_" + MODEL + "_" + Build.getSerial() + "/rom_settings/" + private val REMOTE_APP_LIST_PATH = REMOTE_ROM_SETTINGS_PATH + "app_list/" + + fun getSyncedFolderRoots(context : Context): ArrayList { + val syncedFolders = ArrayList() + val categories = getSyncableCategories() + + categories.forEach { + when (it) { + CATEGORY_IMAGES -> { + syncedFolders.add(createPhotosSyncedFolder()) + syncedFolders.add(createPicturesSyncedFolder()) + } + CATEGORY_MOVIES -> syncedFolders.add(createMovieSyncedFolder()) + CATEGORY_DOCUMENTS -> syncedFolders.add(createDocumentsSyncedFolder()) + CATEGORY_MUSIC -> syncedFolders.add(createMusicsSyncedFolder()) + CATEGORY_PODCASTS -> syncedFolders.add(createPodcastsSyncedFolder()) + CATEGORY_RINGTONES -> syncedFolders.add(createRingtonesSyncedFolder()) + CATEGORY_ROM_SETTINGS -> { + syncedFolders.add(createRomSettingsSyncedFolder()) + syncedFolders.add(createAppListSyncedFolder(context)) + } + } + } + return syncedFolders + } + + private fun getSyncableCategories(): List { + return listOf(CATEGORY_IMAGES, + CATEGORY_MOVIES, + CATEGORY_MUSIC, + CATEGORY_RINGTONES, + CATEGORY_DOCUMENTS, + CATEGORY_PODCASTS, + CATEGORY_ROM_SETTINGS) + } + + private fun createPhotosSyncedFolder(): SyncedFolder { + return createMediaSyncedFolder(CATEGORY_IMAGES, DIRECTORY_DCIM, "/Photos/") + } + + private fun createPicturesSyncedFolder(): SyncedFolder { + return createMediaSyncedFolder(CATEGORY_IMAGES, DIRECTORY_PICTURES, "/Pictures/") + } + + private fun createMovieSyncedFolder(): SyncedFolder { + return createMediaSyncedFolder(CATEGORY_MOVIES, DIRECTORY_MOVIES, "/Movies/") + } + + private fun createDocumentsSyncedFolder(): SyncedFolder { + return createMediaSyncedFolder(CATEGORY_DOCUMENTS, DIRECTORY_DOCUMENTS, "/Documents/") + } + + private fun createMusicsSyncedFolder(): SyncedFolder { + return createMediaSyncedFolder(CATEGORY_MUSIC, DIRECTORY_MUSIC, "/Music/") + } + + private fun createRingtonesSyncedFolder(): SyncedFolder { + return createMediaSyncedFolder(CATEGORY_RINGTONES, DIRECTORY_RINGTONES, "/Ringtones/") + } + + private fun createPodcastsSyncedFolder(): SyncedFolder { + return createMediaSyncedFolder(CATEGORY_PODCASTS, DIRECTORY_PODCASTS, "/Podcasts/") + } + + private fun createRomSettingsSyncedFolder(): SyncedFolder { + return createSettingsSyncedFolder(CATEGORY_ROM_SETTINGS, LOCAL_ROM_SETTINGS_PATH, REMOTE_ROM_SETTINGS_PATH) + } + + private fun createAppListSyncedFolder(context: Context): SyncedFolder { + val localPath = context.filesDir.absolutePath + PATH_SEPARATOR + return createSettingsSyncedFolder(CATEGORY_ROM_SETTINGS, localPath, REMOTE_APP_LIST_PATH) + } + + private fun createMediaSyncedFolder(category: String, publicDirectoryType: String, remotePath: String): SyncedFolder { + val dirPath = CommonUtils.getLocalPath(Environment.getExternalStoragePublicDirectory(publicDirectoryType)) + val localPath = dirPath + PATH_SEPARATOR + return SyncedFolder(category, localPath, remotePath, true) + } + + private fun createSettingsSyncedFolder(category: String, localPath: String, remotePath: String): SyncedFolder { + return SyncedFolder(category, localPath, remotePath, true, false, true, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/work/CreateRemoteFolderWorker.java b/app/src/main/java/foundation/e/drive/work/CreateRemoteFolderWorker.java index 280051993622c5107fd709dda9ee0aa6df216366..e112c0f393c270ebb8e1fcff685fbce925039eac 100644 --- a/app/src/main/java/foundation/e/drive/work/CreateRemoteFolderWorker.java +++ b/app/src/main/java/foundation/e/drive/work/CreateRemoteFolderWorker.java @@ -60,7 +60,7 @@ public class CreateRemoteFolderWorker extends Worker { final Context context = getApplicationContext(); final Account account = getAccount(); if (account == null) { - Timber.e("doWork(): Can't get valid account"); + Timber.d("doWork(): Can't get valid account"); return Result.failure(); } diff --git a/app/src/main/java/foundation/e/drive/work/FirstStartWorker.java b/app/src/main/java/foundation/e/drive/work/FirstStartWorker.java index 7cdbf4f15c6eb94fde151eec34e1b7d64a148d60..5890e103e6c450b5ff71fe13f3a01363f2dce8cc 100644 --- a/app/src/main/java/foundation/e/drive/work/FirstStartWorker.java +++ b/app/src/main/java/foundation/e/drive/work/FirstStartWorker.java @@ -10,6 +10,7 @@ package foundation.e.drive.work; import static foundation.e.drive.utils.AppConstants.INITIALFOLDERS_NUMBER; import static foundation.e.drive.work.WorkRequestFactory.WorkType.ONE_TIME_APP_LIST; +import static foundation.e.drive.work.WorkRequestFactory.WorkType.PERIODIC_CLEANING; import static foundation.e.drive.work.WorkRequestFactory.WorkType.PERIODIC_SCAN; import android.content.Context; @@ -74,5 +75,9 @@ public class FirstStartWorker extends Worker { workManager.enqueueUniquePeriodicWork(PeriodicWorker.UNIQUE_WORK_NAME, ExistingPeriodicWorkPolicy.KEEP, WorkRequestFactory.getPeriodicWorkRequest(PERIODIC_SCAN)); + + workManager.enqueueUniquePeriodicWork(RecycleBinCleaningWorker.UNIQUE_NAME, + ExistingPeriodicWorkPolicy.KEEP, + WorkRequestFactory.getPeriodicWorkRequest(PERIODIC_CLEANING)); } } \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/work/LocalFileDeleter.java b/app/src/main/java/foundation/e/drive/work/LocalFileDeleter.java index f617855185af86fe939b22f5975b8080f5fbb2c1..bfbc1c350dcd21d92c34400d97e5665a7ea45956 100644 --- a/app/src/main/java/foundation/e/drive/work/LocalFileDeleter.java +++ b/app/src/main/java/foundation/e/drive/work/LocalFileDeleter.java @@ -13,6 +13,7 @@ import android.provider.MediaStore; import java.io.File; +import foundation.e.drive.RecycleBin; import foundation.e.drive.database.DbHelper; import foundation.e.drive.models.SyncedFileState; import foundation.e.drive.utils.CommonUtils; @@ -37,34 +38,21 @@ public class LocalFileDeleter { public Runnable getRunnable(final Handler handler, final int threadId, final Context context, final LocalFileDeletionListener listener) { return () -> { - final boolean succeed = deleteFile(fileState, context); - notifyCompletion(threadId, listener, succeed, handler); - }; - } + final File file = new File(fileState.getLocalPath()); + final boolean succeed = RecycleBin.INSTANCE.trashFile(file); + + if (succeed) { + context.getContentResolver().delete(MediaStore.Files.getContentUri("external"), + MediaStore.Files.FileColumns.DATA + "=?", + new String[]{CommonUtils.getLocalPath(file)}); - private boolean deleteFile(SyncedFileState fileState, Context context) { - final File file = new File(fileState.getLocalPath()); - if (file.exists()) { - try { - if (!file.delete()) { - Timber.w("local file ( %s ) removal failed", file.getName()); - return false; + if (DbHelper.manageSyncedFileStateDB(fileState, "DELETE", context) <= 0) { + Timber.d("Failed to remove %s from DB", file.getName()); } - } catch (SecurityException exception) { - Timber.e(exception); - return false; } - context.getContentResolver().delete(MediaStore.Files.getContentUri("external"), - MediaStore.Files.FileColumns.DATA + "=?", - new String[]{CommonUtils.getLocalPath(file)}); - } - - - if (DbHelper.manageSyncedFileStateDB(fileState, "DELETE", context) <= 0) { - Timber.e("Failed to remove %s from DB", file.getName()); - } - return true; + notifyCompletion(threadId, listener, succeed, handler); + }; } private void notifyCompletion(final int threadId, final LocalFileDeletionListener listener, final boolean success, final Handler handler) { diff --git a/app/src/main/java/foundation/e/drive/work/RecycleBinCleaningWorker.kt b/app/src/main/java/foundation/e/drive/work/RecycleBinCleaningWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..45328ed3589a82b870153b36ebee9c798d234632 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/work/RecycleBinCleaningWorker.kt @@ -0,0 +1,34 @@ +/* + * 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 + */ +package foundation.e.drive.work + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import foundation.e.drive.RecycleBin + +/** + * This class is the worker that call cleaning of eDrive's technical files + * and cleaning + * @author Vincent Bourgmayer + */ + +class RecycleBinCleaningWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { + companion object { + const val UNIQUE_NAME = "binCleaner" + } + + override fun doWork(): Result { + val result = RecycleBin.clearOldestFiles() + + if (result) + return Result.success() + else + return Result.failure() + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.java b/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.java index 52084c10f3d664bd87838ee2c64c474f0d4a8532..3b6a0892151eb1c74ff88426269270f2271c4fd1 100644 --- a/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.java +++ b/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.java @@ -26,7 +26,6 @@ import androidx.work.Constraints; import androidx.work.Data; import androidx.work.NetworkType; import androidx.work.OneTimeWorkRequest; -import androidx.work.OutOfQuotaPolicy; import androidx.work.PeriodicWorkRequest; import java.security.InvalidParameterException; @@ -39,6 +38,7 @@ public class WorkRequestFactory { public enum WorkType { PERIODIC_USER_INFO, PERIODIC_SCAN, + PERIODIC_CLEANING, ONE_TIME_FULL_SCAN, ONE_TIME_APP_LIST, ONE_TIME_USER_INFO, @@ -58,11 +58,23 @@ public class WorkRequestFactory { return createPeriodicScanWorkRequest(); case PERIODIC_USER_INFO: return createPeriodicGetUserInfoWorkRequest(); + case PERIODIC_CLEANING: + return createPeriodicCleaningWorkRequest(); default: throw new InvalidParameterException("Unsupported Work Type: " + type); } } + /** + * Create a periodic work request that clean the trash bin + * @return + */ + private static PeriodicWorkRequest createPeriodicCleaningWorkRequest() { + return new PeriodicWorkRequest.Builder(RecycleBinCleaningWorker.class, 1, TimeUnit.DAYS, 2, TimeUnit.HOURS) + .addTag(AppConstants.WORK_GENERIC_TAG) + .build(); + } + /** * Create a PeridocWorkRequest instance for * a Full scan with constraints on network (should @@ -125,7 +137,6 @@ public class WorkRequestFactory { /** * Create a workRequest to generate file which contains list of installed apps - * @param expedited if request is expedited * @return the workRequest */ private static OneTimeWorkRequest createOneTimeAppListGenerationWorkRequest() { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4739c88b128da4f83541778631603161136a05d3..5cfb2194959d0cac8a16cb16af5386d38794ef89 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -35,4 +35,5 @@ Anwendungseinstellungen Erlaube Dateien-Synchronisation bei limitierten Netzwerken Mein Konto + Mein Cloud-Konto \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 0c03cbedb42f027f4c4662d34fe786939b13ab0b..cecd5660aa6ecdff88e2476ad599300d04050328 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,17 +1,17 @@ /e/ Disco - Configuración adicional de la cuenta + Ajustes adicionales de la cuenta Ajustes Copiado %1$s al portapapeles - Has llenado el almacenamiento en la nube asignado hasta el 80%. + Has llenado el 80% de tu almacenamiento en la nube. Has llenado el almacenamiento en la nube asignado hasta el 90%. - Se utiliza el 99% del almacenamiento en la nube asignado. Por favor, tome medidas. - Imposible subir un archivo que es más grande que su almacenamiento gratuito asignado en la nube. Por favor, tome medidas. + Has utilizado el 99% de tu almacenamiento en la nube. Por favor, toma medidas. + No puedes subir un archivo más grande que su almacenamiento gratuito en la nube. Por favor, toma medidas. Tu almacenamiento en la nube está casi lleno. Tu almacenamiento en la nube está lleno. Canal de notificaciones de eDrive - Si no se detecta conexión a Internet, el widget se actualizará automáticamente cuando se active la conexión + No se ha detectado conexión a Internet, widget se actualizará automáticamente cuando haya una conexión activa a Internet Alias Alias Ocultar alias @@ -22,18 +22,18 @@ Sincroniza tus fotos y \nvídeos de forma segura con la nube Cuenta Murena - Sincronizado por última vez: %1$s + Última sincronización: %1$s Ver alias Actualizar Mi Plan:  - Inglés -\n%1$s De %2$s ocasión + %1$s de %2$s usados %1$s Plan Premium %1$s Plan gratuito Mi cuenta en la nube Añadir widget - Configuración de la aplicación + Ajustes de aplicación Fotos y vídeos - Permitir la sincronización de archivos en la red medida + Permitir la sincronización de archivos en red medida Mi cuenta + Mi cuenta en la nube \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 1b31434b545a9a573147ef57aaa0dcd95cd5b1ff..7e997f4a08e152091d66a83b0184d839d90907c2 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -35,4 +35,5 @@ Immagini e video Consenti sincronizzazione file su rete mobile Il mio Account + Il mio Account Cloud \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 9ee54c70cf1950b22949b46f13a4c2fa4d9deced..de4092a78eace5802b5c62ffb5bd1a29d0ef0876 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -35,4 +35,5 @@ Applicatie instellingen Sta synchronisatie toe op netwerken met data limiet Mijn Account + Mijn cloud account \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 258bdb2c5b2d8477cfec84f8fe7de0bc8177183b..6482d03b927d7f1371bef8ce53699c0ceb962777 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -35,4 +35,5 @@ Настройки приложения Картинки и видео Моя учётная запись + Моя облачная учетная запись \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3858e8721ea3bb6fc4d91cb1cab840b43a985749..2d560f5fea0b4f87ee1048719049ceb63400a562 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'com.android.application' version '7.1.3' apply false id 'com.android.library' version '7.1.3' apply false + id 'org.jetbrains.kotlin.android' version '1.8.20-RC' apply false } task clean(type: Delete) {