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 418b28df70030c7f8ac637063b7aac3783dfdde5..46b351bfa09913c8ba9fc950e6aa3401faa39cd2 100644 --- a/app/src/main/java/foundation/e/drive/services/ObserverService.java +++ b/app/src/main/java/foundation/e/drive/services/ObserverService.java @@ -1,6 +1,6 @@ /* * Copyright © CLEUS SAS 2018-2019. - * Copyright © ECORP SAS 2022. + * Copyright © MURENA 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,6 @@ package foundation.e.drive.services; -import static com.owncloud.android.lib.resources.files.FileUtils.PATH_SEPARATOR; import static foundation.e.drive.utils.AppConstants.INITIALIZATION_HAS_BEEN_DONE; import android.accounts.Account; @@ -19,7 +18,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageInfo; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; @@ -33,7 +31,6 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.resources.files.model.RemoteFile; import java.io.File; -import java.io.FileOutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -223,7 +220,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene } } - /** * Clear cached file unused: * remove each cached file which isn't in OperationManagerService.lockedSyncedFileState(); @@ -360,46 +356,11 @@ public class ObserverService extends Service implements OnRemoteOperationListene } } - /* Methods related to device Scanning */ - - /** - * Generate a .txt file containing list of all installed packages with their version name - * I.e : " com.android.my_example_package,7.1.2 " - */ - private void generateAppListFile() { - Timber.d("generateAppListFile()"); - final List packagesInfo = getPackageManager().getInstalledPackages(0); - - final StringBuilder fileContents = new StringBuilder(); - for(int i =-1, size = packagesInfo.size(); ++i < size;) { - PackageInfo currentPackage = packagesInfo.get(i); - fileContents.append( currentPackage.packageName).append(",").append(currentPackage.versionName).append("\n"); - } - try { - final FileOutputStream tmp = openFileOutput(AppConstants.APPLICATIONS_LIST_FILE_NAME_TMP, Context.MODE_PRIVATE); - tmp.write(fileContents.toString().getBytes()); - tmp.close(); - - final String filesdir = getFilesDir().getCanonicalPath()+PATH_SEPARATOR; - final File tmp_file = new File(filesdir+AppConstants.APPLICATIONS_LIST_FILE_NAME_TMP); - final File real_file = new File(filesdir+AppConstants.APPLICATIONS_LIST_FILE_NAME); - - if (tmp_file.length() != real_file.length()) { - tmp_file.renameTo(real_file); - } else { - tmp_file.delete(); - } - } catch (Exception exception) { - Timber.w(exception); - } - } - /** * Prepare the list of files and SyncedFileState for synchronisation */ private void scanLocalFiles(){ Timber.i("scanLocalFiles()"); - if (CommonUtils.isSettingsSyncEnabled(mAccount)) generateAppListFile(); final LocalFileLister fileLister = new LocalFileLister(mSyncedFolders); @@ -420,7 +381,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene syncRequests.putAll(scanner.scanContent(fileList, syncedFileStates)); } } - /* end of methods related to device Scanning */ @Nullable @Override diff --git a/app/src/main/java/foundation/e/drive/utils/CommonUtils.java b/app/src/main/java/foundation/e/drive/utils/CommonUtils.java index 3f488756393ea6181682324ea645cc9dd498ae84..9a6ba862ad24e38c3afad6d106a2e28ef893caf6 100644 --- a/app/src/main/java/foundation/e/drive/utils/CommonUtils.java +++ b/app/src/main/java/foundation/e/drive/utils/CommonUtils.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 @@ -42,7 +42,6 @@ import java.util.List; import foundation.e.drive.R; import foundation.e.drive.models.SyncedFolder; import foundation.e.drive.work.AccountUserInfoWorker; -import foundation.e.drive.work.FullScanWorker; import foundation.e.drive.work.WorkRequestFactory; import timber.log.Timber; @@ -274,18 +273,6 @@ public abstract class CommonUtils { return String.format(Locale.ENGLISH, "%.1f %cB", value / 1024.0, ci.current()); } - /** - * Enqueue a unique periodic worker to look for file to be synchronized (remote files + local files - * - * @param workManager the instance of workManager - */ - public static void registerPeriodicFullScanWorker(WorkManager workManager) { - workManager.enqueueUniquePeriodicWork(FullScanWorker.UNIQUE_WORK_NAME, - ExistingPeriodicWorkPolicy.KEEP, - WorkRequestFactory.getPeriodicWorkRequest(WorkRequestFactory.WorkType.FULL_SCAN)); - } - - /** * This method create a chain of WorkRequests to perform Initialization tasks: * Firstly, it creates WorkRequest to create remote root folders on ecloud 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 4898f79bfc3c6954a7330b0e05f6bc2cd5acc855..6663c14352f32f57c8ddfea5a659dc4348cce9f8 100644 --- a/app/src/main/java/foundation/e/drive/work/FirstStartWorker.java +++ b/app/src/main/java/foundation/e/drive/work/FirstStartWorker.java @@ -1,5 +1,5 @@ /* - * Copyright © ECORP SAS 2022. + * Copyright © MURENA 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,18 +9,19 @@ package foundation.e.drive.work; import static foundation.e.drive.utils.AppConstants.INITIALFOLDERS_NUMBER; +import static foundation.e.drive.work.WorkRequestFactory.WorkType.PERIODIC_SCAN; import android.content.Context; import android.content.Intent; import androidx.annotation.NonNull; +import androidx.work.ExistingPeriodicWorkPolicy; import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; import foundation.e.drive.EdriveApplication; import foundation.e.drive.utils.AppConstants; -import foundation.e.drive.utils.CommonUtils; import timber.log.Timber; /** @@ -38,7 +39,7 @@ public class FirstStartWorker extends Worker { @NonNull @Override public Result doWork() { - Timber.v("doWork()"); + Timber.v("FirstStartWorker.doWork()"); final Context appContext = getApplicationContext(); appContext.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE) @@ -47,14 +48,21 @@ public class FirstStartWorker extends Worker { .putInt(INITIALFOLDERS_NUMBER, 9) .apply(); - CommonUtils.registerPeriodicFullScanWorker(WorkManager.getInstance(appContext)); + registerPeriodicWork(appContext); getApplicationContext().startService(new Intent(getApplicationContext(), foundation.e.drive.services.SynchronizationService.class)); getApplicationContext().startService(new Intent(getApplicationContext(), foundation.e.drive.services.ObserverService.class)); - //all folder have been created ((EdriveApplication) getApplicationContext()).startRecursiveFileObserver(); return Result.success(); } -} + + private void registerPeriodicWork(@NonNull final Context context) { + final WorkManager workManager = WorkManager.getInstance(context); + + workManager.enqueueUniquePeriodicWork(PeriodicWorker.UNIQUE_WORK_NAME, + ExistingPeriodicWorkPolicy.KEEP, + WorkRequestFactory.getPeriodicWorkRequest(PERIODIC_SCAN)); + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/work/ListAppsWorker.java b/app/src/main/java/foundation/e/drive/work/ListAppsWorker.java new file mode 100644 index 0000000000000000000000000000000000000000..be79084ddb8c9bd329b7d3f6e233db6c6db83689 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/work/ListAppsWorker.java @@ -0,0 +1,117 @@ +/* + * 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 static com.owncloud.android.lib.resources.files.FileUtils.PATH_SEPARATOR; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.database.Cursor; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; + +import foundation.e.drive.utils.AppConstants; +import timber.log.Timber; + +/** + * Class responsible for building a list of installed app and save that in a file that must be synchronized + * @author vincent Bourgmayer + */ +public class ListAppsWorker extends Worker { + private final static String PWA_PLAYER = "content://foundation.e.pwaplayer.provider/pwa"; + private final static String SEPARATOR =","; + private final static String PWA_SECTION_SEPARATOR = "\n---- PWAs ----\n"; + + public ListAppsWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { + Timber.d("generateAppListFile()"); + final Context context = getApplicationContext(); + final StringBuilder fileContents = listRegularApps(context); + listPWAs(context, fileContents); + if (fileContents.length() == 0) return Result.success(); + + final boolean success = writeToFile(fileContents); + return success ? Result.success() : Result.failure(); + } + + private StringBuilder listRegularApps(@NonNull final Context context) { + final List packagesInfo = context.getPackageManager().getInstalledPackages(0); + final StringBuilder result = new StringBuilder(); + for (PackageInfo currentPkg : packagesInfo) { + result.append(currentPkg.packageName) + .append(SEPARATOR) + .append(currentPkg.versionName) + .append("\n"); + } + return result; + } + + private void listPWAs(@NonNull final Context context, @NonNull final StringBuilder stringBuilder) { + final Cursor cursor = context.getContentResolver().query( + Uri.parse(PWA_PLAYER), null, null, null, null); + + if (cursor.getCount() <= 0) return; + + stringBuilder.append(PWA_SECTION_SEPARATOR); + cursor.moveToFirst(); + + do { + try { + final String pwaTitle = cursor.getString(cursor.getColumnIndexOrThrow("title")); + final String pwaUrl = cursor.getString(cursor.getColumnIndexOrThrow("url")); + final Long pwaDbId = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); + stringBuilder.append(pwaDbId) + .append(SEPARATOR) + .append(pwaTitle) + .append(SEPARATOR) + .append(pwaUrl) + .append("\n"); + + } catch (IllegalArgumentException exception) { + Timber.e(exception, "Catched exception: invalid column names for cursor"); + } + } while (cursor.moveToNext()); + + cursor.close(); + } + + private boolean writeToFile(@NonNull final StringBuilder fileContents) { + try (final FileOutputStream tmp = getApplicationContext().openFileOutput(AppConstants.APPLICATIONS_LIST_FILE_NAME_TMP, Context.MODE_PRIVATE); + ) { + tmp.write(fileContents.toString().getBytes()); + + final String filesDir = getApplicationContext().getFilesDir().getCanonicalPath() + PATH_SEPARATOR; + final File tmp_file = new File(filesDir+AppConstants.APPLICATIONS_LIST_FILE_NAME_TMP); + final File real_file = new File(filesDir+AppConstants.APPLICATIONS_LIST_FILE_NAME); + + if (tmp_file.length() != real_file.length()) { + tmp_file.renameTo(real_file); + } else { + tmp_file.delete(); + } + } catch (IOException exception) { + Timber.e(exception, "catched exception: can't write app list in file"); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/work/PeriodicWorker.java b/app/src/main/java/foundation/e/drive/work/PeriodicWorker.java new file mode 100644 index 0000000000000000000000000000000000000000..fb48565443d3dc1b5d6b7b58eaf92fef7c16d242 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/work/PeriodicWorker.java @@ -0,0 +1,51 @@ +/* + * 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 static foundation.e.drive.work.WorkRequestFactory.WorkType.ONE_TIME_APP_LIST; +import static foundation.e.drive.work.WorkRequestFactory.WorkType.ONE_TIME_FULL_SCAN; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.work.ExistingWorkPolicy; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import java.util.List; + +/** + * Worker that trigger the chain of worker that really need to be periodic + * This worker is required because we cannot chain periodic work through WorkerAPI + * at the moment + * @author vincent Bourgmayer + */ +public class PeriodicWorker extends Worker { + public final static String UNIQUE_WORK_NAME = "periodicWork"; + public PeriodicWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { + final WorkManager workManager = WorkManager.getInstance(getApplicationContext()); + + final List workRequestsLists = List.of( + WorkRequestFactory.getOneTimeWorkRequest(ONE_TIME_APP_LIST, null), + WorkRequestFactory.getOneTimeWorkRequest(ONE_TIME_FULL_SCAN, null)); + + workManager.beginUniqueWork(FullScanWorker.UNIQUE_WORK_NAME, ExistingWorkPolicy.KEEP, workRequestsLists) + .enqueue(); + + return Result.success(); + } +} \ 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 e3daf5166b02341e8b35c5735b2656e45c5aeb38..8627ceb6197562d62135560c65aa39caaa1f1eed 100644 --- a/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.java +++ b/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.java @@ -1,3 +1,11 @@ +/* + * Copyright © MURENA 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 + * http://www.gnu.org/licenses/gpl.html + */ + package foundation.e.drive.work; import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_ENABLE; @@ -11,6 +19,7 @@ import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_REMOTE_P import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_SCAN_LOCAL; import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_SCAN_REMOTE; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.work.BackoffPolicy; import androidx.work.Constraints; @@ -27,8 +36,10 @@ import foundation.e.drive.utils.AppConstants; public class WorkRequestFactory { public enum WorkType { - FULL_SCAN, PERIODIC_USER_INFO, + PERIODIC_SCAN, + ONE_TIME_FULL_SCAN, + ONE_TIME_APP_LIST, ONE_TIME_USER_INFO, CREATE_REMOTE_DIR, FIRST_START @@ -36,14 +47,14 @@ public class WorkRequestFactory { /** * Build an instance of PeriodicWorkRequest depending of the work type specified - * @param type WorkType. Should be FULL_SCAN or PERIODIC_USER_INFO. + * @param type WorkType. Should be FULL_SCAN or PERIODIC_USER_INFO or PERIODIC_APP_LIST * If not, it will throw an InvalidParameterException * @return Periodic WorkRequest */ - public static PeriodicWorkRequest getPeriodicWorkRequest(WorkType type) { + public static PeriodicWorkRequest getPeriodicWorkRequest(@NonNull WorkType type) { switch (type) { - case FULL_SCAN: - return createPeriodicFullScanWorkRequest(); + case PERIODIC_SCAN: + return createPeriodicScanWorkRequest(); case PERIODIC_USER_INFO: return createPeriodicGetUserInfoWorkRequest(); default: @@ -51,41 +62,18 @@ public class WorkRequestFactory { } } - /** - * Build an instance of OneTimeWorkRequest depending of the work type specified. - * @param type Should be ONE_TIME_USER_INFO, or FIRST_START, or CREATE_REMOTE_DIR - * or it will throw InvalidParameterException - * @param syncedFolder this parameter is required for CREATE_REMOTE_DIR work type. If null it will throw an NPE. - * @return OneTimeWorkRequest's instance. - */ - public static OneTimeWorkRequest getOneTimeWorkRequest(WorkType type, @Nullable SyncedFolder syncedFolder) { - switch (type) { - case ONE_TIME_USER_INFO: - return createOneTimeGetUserInfoWorkRequest(); - case FIRST_START: - return createOneTimeFirstStartWorkRequest(); - case CREATE_REMOTE_DIR: - if (syncedFolder == null) throw new NullPointerException("Synced folder is null"); - return createOneTimeCreateRemoteFolderWorkRequest(syncedFolder); - default: - throw new InvalidParameterException("Unsupported Work Type: " + type); - } - } - - /** * Create a PeridocWorkRequest instance for * a Full scan with constraints on network (should * be unmetered) and battery (shouldn't be low) * @return instance of PeriodicWorkRequest */ - private static PeriodicWorkRequest createPeriodicFullScanWorkRequest() { + private static PeriodicWorkRequest createPeriodicScanWorkRequest() { final Constraints constraints = createUnmeteredNetworkAndHighBatteryConstraints(); final PeriodicWorkRequest workRequest = - new PeriodicWorkRequest.Builder(FullScanWorker.class, - 31, TimeUnit.MINUTES, - 5, TimeUnit.MINUTES) + new PeriodicWorkRequest.Builder(PeriodicWorker.class, + 26, TimeUnit.MINUTES, 5, TimeUnit.MINUTES) .setConstraints(constraints) .addTag(AppConstants.WORK_GENERIC_TAG) .build(); @@ -109,6 +97,58 @@ public class WorkRequestFactory { .build(); return workRequest; } + /** + * Build an instance of OneTimeWorkRequest depending of the work type specified. + * @param type Should be ONE_TIME_USER_INFO, or FIRST_START, or CREATE_REMOTE_DIR + * or it will throw InvalidParameterException + * @param syncedFolder this parameter is required for CREATE_REMOTE_DIR work type. If null it will throw an NPE. + * @return OneTimeWorkRequest's instance. + */ + public static OneTimeWorkRequest getOneTimeWorkRequest(@NonNull WorkType type, @Nullable SyncedFolder syncedFolder) { + switch (type) { + case ONE_TIME_APP_LIST: + return createOneTimeAppListGenerationWorkRequest(); + case ONE_TIME_FULL_SCAN: + return createOneTimeFullScanWorkRequest(); + case ONE_TIME_USER_INFO: + return createOneTimeGetUserInfoWorkRequest(); + case FIRST_START: + return createOneTimeFirstStartWorkRequest(); + case CREATE_REMOTE_DIR: + if (syncedFolder == null) throw new NullPointerException("Synced folder is null"); + return createOneTimeCreateRemoteFolderWorkRequest(syncedFolder); + default: + throw new InvalidParameterException("Unsupported Work Type: " + type); + } + } + + private static OneTimeWorkRequest createOneTimeAppListGenerationWorkRequest() { + final OneTimeWorkRequest workRequest = + new OneTimeWorkRequest.Builder(ListAppsWorker.class) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2, TimeUnit.MINUTES) + .addTag(AppConstants.WORK_GENERIC_TAG) + .build(); + return workRequest; + } + + /** + * Create a OneTimeWorkRequest instance for + * a Full scan with constraints on network (should + * be unmetered) and battery (shouldn't be low) + * @return instance of OneTimeWorkRequest + */ + private static OneTimeWorkRequest createOneTimeFullScanWorkRequest() { + final Constraints constraints = createUnmeteredNetworkAndHighBatteryConstraints(); + + final OneTimeWorkRequest workRequest = + new OneTimeWorkRequest.Builder(FullScanWorker.class) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2, TimeUnit.MINUTES) + .setConstraints(constraints) + .addTag(AppConstants.WORK_GENERIC_TAG) + .build(); + return workRequest; + } + /** * Instanciate a OneTimeWorkRequest to retrieve user info @@ -132,7 +172,7 @@ public class WorkRequestFactory { * @param syncedFolder SyncedFolder instance with data about folder to create * @return Instance OneTimeWorkRequest */ - private static OneTimeWorkRequest createOneTimeCreateRemoteFolderWorkRequest(SyncedFolder syncedFolder) { + private static OneTimeWorkRequest createOneTimeCreateRemoteFolderWorkRequest(@NonNull SyncedFolder syncedFolder) { final Constraints constraints = createUnmeteredNetworkAndHighBatteryConstraints(); final OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder( @@ -173,24 +213,23 @@ public class WorkRequestFactory { return constraint; } - /** * Parse SyncedFolder instance in Data, used as data for WorkRequest * @param folder SyncedFolder instance * @return Data instance */ - private static Data createDataFromSyncedFolder(SyncedFolder folder) { + private static Data createDataFromSyncedFolder(@NonNull SyncedFolder folder) { return new Data.Builder() - .putInt(DATA_KEY_ID, folder.getId()) - .putString(DATA_KEY_LIBELLE, folder.getLibelle()) - .putString(DATA_KEY_LOCAL_PATH, folder.getLocalFolder()) - .putString(DATA_KEY_REMOTE_PATH, folder.getRemoteFolder()) - .putString(DATA_KEY_LAST_ETAG, folder.getLastEtag()) - .putLong(DATA_KEY_LAST_MODIFIED, folder.getLastModified()) - .putBoolean(DATA_KEY_SCAN_LOCAL, folder.isScanLocal()) - .putBoolean(DATA_KEY_SCAN_REMOTE, folder.isScanRemote()) - .putBoolean(DATA_KEY_ENABLE, folder.isEnabled()) - .putBoolean(DATA_KEY_MEDIATYPE, folder.isMediaType()) - .build(); + .putInt(DATA_KEY_ID, folder.getId()) + .putString(DATA_KEY_LIBELLE, folder.getLibelle()) + .putString(DATA_KEY_LOCAL_PATH, folder.getLocalFolder()) + .putString(DATA_KEY_REMOTE_PATH, folder.getRemoteFolder()) + .putString(DATA_KEY_LAST_ETAG, folder.getLastEtag()) + .putLong(DATA_KEY_LAST_MODIFIED, folder.getLastModified()) + .putBoolean(DATA_KEY_SCAN_LOCAL, folder.isScanLocal()) + .putBoolean(DATA_KEY_SCAN_REMOTE, folder.isScanRemote()) + .putBoolean(DATA_KEY_ENABLE, folder.isEnabled()) + .putBoolean(DATA_KEY_MEDIATYPE, folder.isMediaType()) + .build(); } }