From 808c9098ea7476fd7e6311aa838e81d90c9e0e99 Mon Sep 17 00:00:00 2001 From: Chris Narkiewicz Date: Sat, 14 Sep 2019 16:45:15 +0100 Subject: [PATCH] Migrate NContentObserverJob to WorkManager Signed-off-by: Chris Narkiewicz --- build.gradle | 3 + src/main/AndroidManifest.xml | 13 +- .../com/nextcloud/client/di/AppComponent.java | 4 +- .../com/nextcloud/client/di/AppModule.java | 7 +- .../client/jobs/BackgroundJobFactory.kt | 81 ++++++++++++ .../client/jobs/BackgroundJobManager.kt | 37 ++++++ .../client/jobs/BackgroundJobManagerImpl.kt | 54 ++++++++ .../client/jobs/ContentObserverWork.kt | 85 +++++++++++++ .../com/nextcloud/client/jobs/JobsModule.kt | 50 ++++++++ .../java/com/owncloud/android/MainApp.java | 24 +++- .../files/BootupBroadcastReceiver.java | 5 +- .../owncloud/android/jobs/FilesSyncJob.java | 2 +- .../android/jobs/NContentObserverJob.java | 102 --------------- .../android/utils/FilesSyncHelper.java | 33 +---- .../client/jobs/BackgroundJobFactoryTest.kt | 87 +++++++++++++ .../client/jobs/ContentObserverWorkTest.kt | 118 ++++++++++++++++++ 16 files changed, 560 insertions(+), 145 deletions(-) create mode 100644 src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt create mode 100644 src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt create mode 100644 src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt create mode 100644 src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt create mode 100644 src/main/java/com/nextcloud/client/jobs/JobsModule.kt delete mode 100644 src/main/java/com/owncloud/android/jobs/NContentObserverJob.java create mode 100644 src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt create mode 100644 src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt diff --git a/build.gradle b/build.gradle index 5e1403fd25..1d98ab94ce 100644 --- a/build.gradle +++ b/build.gradle @@ -272,6 +272,8 @@ dependencies { implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.exifinterface:exifinterface:1.0.0' implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0" + implementation "androidx.work:work-runtime:2.2.0" + implementation "androidx.work:work-runtime-ktx:2.2.0" implementation 'com.github.albfernandez:juniversalchardet:2.0.3' // need this version for Android <7 implementation 'com.google.code.findbugs:annotations:2.0.1' implementation 'commons-io:commons-io:2.6' @@ -315,6 +317,7 @@ dependencies { ktlint "com.pinterest:ktlint:0.34.2" implementation 'org.conscrypt:conscrypt-android:2.2.1' + // dependencies for local unit tests testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:3.1.0' diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index f3ce8e29b0..c623ef87c1 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -164,11 +164,6 @@ android:label="@string/app_name" android:theme="@style/Theme.ownCloud.Fullscreen" /> - - - @@ -272,6 +267,14 @@ android:exported="true"> + + + + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.nextcloud.client.jobs + +import android.content.ContentResolver +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.work.ListenableWorker +import androidx.work.WorkerFactory +import androidx.work.WorkerParameters +import com.nextcloud.client.device.DeviceInfo +import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.datamodel.SyncedFolderProvider +import javax.inject.Inject +import javax.inject.Provider + +/** + * This factory is responsible for creating all background jobs and for injecting + * all jobs dependencies. + */ +class BackgroundJobFactory @Inject constructor( + private val preferences: AppPreferences, + private val contentResolver: ContentResolver, + private val powerManagerService: PowerManagementService, + private val backgroundJobManager: Provider, + private val deviceInfo: DeviceInfo +) : WorkerFactory() { + + override fun createWorker( + context: Context, + workerClassName: String, + workerParameters: WorkerParameters + ): ListenableWorker? { + + val workerClass = try { + Class.forName(workerClassName).kotlin + } catch (ex: ClassNotFoundException) { + null + } + + return when (workerClass) { + ContentObserverWork::class -> createContentObserverJob(context, workerParameters) + else -> null // falls back to default factory + } + } + + private fun createContentObserverJob(context: Context, workerParameters: WorkerParameters): ListenableWorker? { + val folderResolver = SyncedFolderProvider(contentResolver, preferences) + @RequiresApi(Build.VERSION_CODES.N) + if (deviceInfo.apiLevel >= Build.VERSION_CODES.N) { + return ContentObserverWork( + context, + workerParameters, + folderResolver, + powerManagerService, + backgroundJobManager.get() + ) + } else { + return null + } + } +} diff --git a/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt new file mode 100644 index 0000000000..c22d5ef700 --- /dev/null +++ b/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -0,0 +1,37 @@ +/* + * Nextcloud Android client application + * + * @author Chris Narkiewicz + * Copyright (C) 2019 Chris Narkiewicz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.nextcloud.client.jobs + +import android.os.Build +import androidx.annotation.RequiresApi + +/** + * This interface allows to control, schedule and monitor all application + * long-running background tasks, such as periodic checks or synchronization. + */ +interface BackgroundJobManager { + + /** + * Start content observer job that monitors changes in media folders + * and launches synchronization when needed. + */ + @RequiresApi(Build.VERSION_CODES.N) + fun scheduleContentObserverJob() +} diff --git a/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt new file mode 100644 index 0000000000..d68acf9848 --- /dev/null +++ b/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -0,0 +1,54 @@ +/* + * Nextcloud Android client application + * + * @author Chris Narkiewicz + * Copyright (C) 2019 Chris Narkiewicz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.nextcloud.client.jobs + +import android.os.Build +import android.provider.MediaStore +import androidx.annotation.RequiresApi +import androidx.work.Constraints +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import java.util.concurrent.TimeUnit + +internal class BackgroundJobManagerImpl(private val workManager: WorkManager) : BackgroundJobManager { + + companion object { + const val TAG_CONTENT_SYNC = "content_sync" + const val MAX_CONTENT_TRIGGER_DELAY_MS = 1500L + } + + @RequiresApi(Build.VERSION_CODES.N) + override fun scheduleContentObserverJob() { + val constrains = Constraints.Builder() + .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) + .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true) + .addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true) + .addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true) + .setTriggerContentMaxDelay(MAX_CONTENT_TRIGGER_DELAY_MS, TimeUnit.MILLISECONDS) + .build() + + val request = OneTimeWorkRequest.Builder(ContentObserverWork::class.java) + .setConstraints(constrains) + .addTag(TAG_CONTENT_SYNC) + .build() + + workManager.enqueue(request) + } +} diff --git a/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt b/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt new file mode 100644 index 0000000000..3c3e5f7118 --- /dev/null +++ b/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt @@ -0,0 +1,85 @@ +/* + * Nextcloud Android client application + * + * @author Chris Narkiewicz + * Copyright (C) 2019 Chris Narkiewicz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.nextcloud.client.jobs + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.evernote.android.job.JobRequest +import com.evernote.android.job.util.support.PersistableBundleCompat +import com.nextcloud.client.device.PowerManagementService +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.jobs.FilesSyncJob +import com.owncloud.android.jobs.MediaFoldersDetectionJob + +/** + * This work is triggered when OS detects change in media folders. + * + * It fires media detection job and sync job and finishes immediately. + * + * This job must not be started on API < 24. + */ +@RequiresApi(Build.VERSION_CODES.N) +class ContentObserverWork( + appContext: Context, + private val params: WorkerParameters, + private val syncerFolderProvider: SyncedFolderProvider, + private val powerManagementService: PowerManagementService, + private val backgroundJobManager: BackgroundJobManager +) : Worker(appContext, params) { + + override fun doWork(): Result { + if (params.triggeredContentUris.size > 0) { + checkAndStartFileSyncJob() + startMediaFolderDetectionJob() + } + recheduleSelf() + return Result.success() + } + + private fun recheduleSelf() { + backgroundJobManager.scheduleContentObserverJob() + } + + private fun checkAndStartFileSyncJob() { + val syncFolders = syncerFolderProvider.countEnabledSyncedFolders() > 0 + if (!powerManagementService.isPowerSavingEnabled && syncFolders) { + val persistableBundleCompat = PersistableBundleCompat() + persistableBundleCompat.putBoolean(FilesSyncJob.SKIP_CUSTOM, true) + + JobRequest.Builder(FilesSyncJob.TAG) + .startNow() + .setExtras(persistableBundleCompat) + .setUpdateCurrent(false) + .build() + .schedule() + } + } + + private fun startMediaFolderDetectionJob() { + JobRequest.Builder(MediaFoldersDetectionJob.TAG) + .startNow() + .setUpdateCurrent(false) + .build() + .schedule() + } +} diff --git a/src/main/java/com/nextcloud/client/jobs/JobsModule.kt b/src/main/java/com/nextcloud/client/jobs/JobsModule.kt new file mode 100644 index 0000000000..027874e48a --- /dev/null +++ b/src/main/java/com/nextcloud/client/jobs/JobsModule.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud Android client application + * + * @author Chris Narkiewicz + * Copyright (C) 2019 Chris Narkiewicz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.nextcloud.client.jobs + +import android.content.Context +import android.content.ContextWrapper +import androidx.work.Configuration +import androidx.work.WorkManager +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +class JobsModule { + + @Provides + @Singleton + fun backgroundJobManager(context: Context, factory: BackgroundJobFactory): BackgroundJobManager { + val configuration = Configuration.Builder() + .setWorkerFactory(factory) + .build() + + val contextWrapper = object : ContextWrapper(context) { + override fun getApplicationContext(): Context { + return this + } + } + + WorkManager.initialize(contextWrapper, configuration) + val wm = WorkManager.getInstance(context) + return BackgroundJobManagerImpl(wm) + } +} diff --git a/src/main/java/com/owncloud/android/MainApp.java b/src/main/java/com/owncloud/android/MainApp.java index 2e10e459d3..0a6b3603bc 100644 --- a/src/main/java/com/owncloud/android/MainApp.java +++ b/src/main/java/com/owncloud/android/MainApp.java @@ -49,6 +49,7 @@ import com.nextcloud.client.device.PowerManagementService; import com.nextcloud.client.di.ActivityInjector; import com.nextcloud.client.di.DaggerAppComponent; import com.nextcloud.client.errorhandling.ExceptionHandler; +import com.nextcloud.client.jobs.BackgroundJobManager; import com.nextcloud.client.logger.LegacyLoggerAdapter; import com.nextcloud.client.logger.Logger; import com.nextcloud.client.network.ConnectivityService; @@ -156,6 +157,9 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector { @Inject AppInfo appInfo; + @Inject + BackgroundJobManager backgroundJobManager; + private PassCodeManager passCodeManager; @SuppressWarnings("unused") @@ -184,6 +188,14 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector { return powerManagementService; } + /** + * Temporary getter enabling intermediate refactoring. + * TODO: remove when FileSyncHelper is refactored/removed + */ + public BackgroundJobManager getBackgroundJobManager() { + return backgroundJobManager; + } + private String getAppProcessName() { String processName = ""; if(Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { @@ -286,8 +298,11 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector { Log_OC.d("Debug", "Failed to disable uri exposure"); } } - - initSyncOperations(uploadsStorageManager, accountManager, connectivityService, powerManagementService); + initSyncOperations(uploadsStorageManager, + accountManager, + connectivityService, + powerManagementService, + backgroundJobManager); initContactsBackup(accountManager); notificationChannels(); @@ -444,7 +459,8 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector { final UploadsStorageManager uploadsStorageManager, final UserAccountManager accountManager, final ConnectivityService connectivityService, - final PowerManagementService powerManagementService + final PowerManagementService powerManagementService, + final BackgroundJobManager jobManager ) { updateToAutoUpload(); cleanOldEntries(); @@ -462,7 +478,7 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector { initiateExistingAutoUploadEntries(); - FilesSyncHelper.scheduleFilesSyncIfNeeded(mContext); + FilesSyncHelper.scheduleFilesSyncIfNeeded(mContext, jobManager); FilesSyncHelper.restartJobsIfNeeded( uploadsStorageManager, accountManager, diff --git a/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java b/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java index 15a4680309..77f1535f07 100644 --- a/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java +++ b/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java @@ -29,6 +29,7 @@ import android.content.Intent; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.device.PowerManagementService; +import com.nextcloud.client.jobs.BackgroundJobManager; import com.nextcloud.client.network.ConnectivityService; import com.owncloud.android.MainApp; import com.owncloud.android.datamodel.UploadsStorageManager; @@ -51,6 +52,7 @@ public class BootupBroadcastReceiver extends BroadcastReceiver { @Inject UploadsStorageManager uploadsStorageManager; @Inject ConnectivityService connectivityService; @Inject PowerManagementService powerManagementService; + @Inject BackgroundJobManager backgroundJobManager; /** * Receives broadcast intent reporting that the system was just boot up. @@ -66,7 +68,8 @@ public class BootupBroadcastReceiver extends BroadcastReceiver { MainApp.initSyncOperations(uploadsStorageManager, accountManager, connectivityService, - powerManagementService); + powerManagementService, + backgroundJobManager); MainApp.initContactsBackup(accountManager); } else { Log_OC.d(TAG, "Getting wrong intent: " + intent.getAction()); diff --git a/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java b/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java index 10a1a624d0..eac4a8edba 100644 --- a/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java +++ b/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java @@ -74,7 +74,7 @@ import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR; */ public class FilesSyncJob extends Job { public static final String TAG = "FilesSyncJob"; - static final String SKIP_CUSTOM = "skipCustom"; + public static final String SKIP_CUSTOM = "skipCustom"; public static final String OVERRIDE_POWER_SAVING = "overridePowerSaving"; private static final String WAKELOCK_TAG_SEPARATION = ":"; diff --git a/src/main/java/com/owncloud/android/jobs/NContentObserverJob.java b/src/main/java/com/owncloud/android/jobs/NContentObserverJob.java deleted file mode 100644 index 7d228ada08..0000000000 --- a/src/main/java/com/owncloud/android/jobs/NContentObserverJob.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Mario Danic - * Copyright (C) 2017 Mario Danic - * Copyright (C) 2017 Nextcloud - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU AFFERO GENERAL PUBLIC LICENSE for more details. - * - * You should have received a copy of the GNU Affero General Public - * License along with this program. If not, see . - */ - -package com.owncloud.android.jobs; - -import android.app.job.JobParameters; -import android.app.job.JobService; -import android.os.Build; - -import com.evernote.android.job.JobRequest; -import com.evernote.android.job.util.support.PersistableBundleCompat; -import com.nextcloud.client.device.PowerManagementService; -import com.nextcloud.client.preferences.AppPreferences; -import com.owncloud.android.MainApp; -import com.owncloud.android.datamodel.SyncedFolderProvider; -import com.owncloud.android.utils.FilesSyncHelper; - -import androidx.annotation.RequiresApi; - -/* - Job that triggers new FilesSyncJob in case new photo or video were detected - and starts a job to find new media folders - */ -@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) -public class NContentObserverJob extends JobService { - - private PowerManagementService powerManagementService; - private AppPreferences preferences; - - @Override - public void onCreate() { - super.onCreate(); - - // Temporary workaround for https://github.com/nextcloud/android/issues/4147 - // TODO: this must be fixed properly - MainApp app = (MainApp) getApplication(); - powerManagementService = app.getPowerManagementService(); - preferences = app.getPreferences(); - } - - @Override - public boolean onStartJob(JobParameters params) { - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (params.getJobId() == FilesSyncHelper.ContentSyncJobId && params.getTriggeredContentAuthorities() - != null && params.getTriggeredContentUris() != null - && params.getTriggeredContentUris().length > 0) { - - checkAndStartFileSyncJob(); - - new JobRequest.Builder(MediaFoldersDetectionJob.TAG) - .startNow() - .setUpdateCurrent(false) - .build() - .schedule(); - - } - - FilesSyncHelper.scheduleJobOnN(); - } - - return true; - } - - private void checkAndStartFileSyncJob() { - if (!powerManagementService.isPowerSavingEnabled() && - new SyncedFolderProvider(getContentResolver(), preferences).countEnabledSyncedFolders() > 0) { - PersistableBundleCompat persistableBundleCompat = new PersistableBundleCompat(); - persistableBundleCompat.putBoolean(FilesSyncJob.SKIP_CUSTOM, true); - - new JobRequest.Builder(FilesSyncJob.TAG) - .startNow() - .setExtras(persistableBundleCompat) - .setUpdateCurrent(false) - .build() - .schedule(); - } - } - - @Override - public boolean onStopJob(JobParameters params) { - return false; - } -} diff --git a/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java b/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java index dc8057c439..7f637a5a9e 100644 --- a/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java +++ b/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java @@ -24,9 +24,6 @@ package com.owncloud.android.utils; import android.accounts.Account; -import android.app.job.JobInfo; -import android.app.job.JobScheduler; -import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; @@ -40,6 +37,7 @@ import com.evernote.android.job.JobManager; import com.evernote.android.job.JobRequest; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.device.PowerManagementService; +import com.nextcloud.client.jobs.BackgroundJobManager; import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.client.preferences.AppPreferences; import com.owncloud.android.MainApp; @@ -52,7 +50,6 @@ import com.owncloud.android.datamodel.UploadsStorageManager; import com.owncloud.android.db.OCUpload; import com.owncloud.android.files.services.FileUploader; import com.owncloud.android.jobs.FilesSyncJob; -import com.owncloud.android.jobs.NContentObserverJob; import com.owncloud.android.jobs.OfflineSyncJob; import org.lukhnos.nnio.file.FileVisitResult; @@ -259,7 +256,7 @@ public final class FilesSyncHelper { }).start(); } - public static void scheduleFilesSyncIfNeeded(Context context) { + public static void scheduleFilesSyncIfNeeded(Context context, BackgroundJobManager jobManager) { // always run this because it also allows us to perform retries of manual uploads new JobRequest.Builder(FilesSyncJob.TAG) .setPeriodic(900000L, 300000L) @@ -268,7 +265,7 @@ public final class FilesSyncHelper { .schedule(); if (context != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - scheduleJobOnN(); + jobManager.scheduleContentObserverJob(); } } @@ -282,29 +279,5 @@ public final class FilesSyncHelper { .schedule(); } } - - @RequiresApi(api = Build.VERSION_CODES.N) - public static void scheduleJobOnN() { - JobScheduler jobScheduler = MainApp.getAppContext().getSystemService(JobScheduler.class); - - if (jobScheduler != null) { - JobInfo.Builder builder = new JobInfo.Builder(ContentSyncJobId, new ComponentName(MainApp.getAppContext(), - NContentObserverJob.class.getName())); - builder.addTriggerContentUri(new JobInfo.TriggerContentUri(android.provider.MediaStore. - Images.Media.INTERNAL_CONTENT_URI, - JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)); - builder.addTriggerContentUri(new JobInfo.TriggerContentUri(MediaStore. - Images.Media.EXTERNAL_CONTENT_URI, - JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)); - builder.addTriggerContentUri(new JobInfo.TriggerContentUri(android.provider.MediaStore. - Video.Media.INTERNAL_CONTENT_URI, - JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)); - builder.addTriggerContentUri(new JobInfo.TriggerContentUri(MediaStore. - Video.Media.EXTERNAL_CONTENT_URI, - JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)); - builder.setTriggerContentMaxDelay(1500); - jobScheduler.schedule(builder.build()); - } - } } diff --git a/src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt b/src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt new file mode 100644 index 0000000000..7a65adc76e --- /dev/null +++ b/src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt @@ -0,0 +1,87 @@ +package com.nextcloud.client.jobs + +import android.content.ContentResolver +import android.content.Context +import android.os.Build +import androidx.work.WorkerParameters +import com.nextcloud.client.device.DeviceInfo +import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.preferences.AppPreferences +import com.nhaarman.mockitokotlin2.whenever +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import javax.inject.Provider + +class BackgroundJobFactoryTest { + + @Mock + private lateinit var context: Context + + @Mock + private lateinit var params: WorkerParameters + + @Mock + private lateinit var contentResolver: ContentResolver + + @Mock + private lateinit var preferences: AppPreferences + + @Mock + private lateinit var powerManagementService: PowerManagementService + + @Mock + private lateinit var backgroundJobManager: BackgroundJobManager + + @Mock + private lateinit var deviceInfo: DeviceInfo + + private lateinit var factory: BackgroundJobFactory + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + factory = BackgroundJobFactory( + preferences, + contentResolver, + powerManagementService, + Provider { backgroundJobManager }, + deviceInfo + ) + } + + @Test + fun `worker is created on api level 24+`() { + // GIVEN + // api level is > 24 + // content URI trigger is supported + whenever(deviceInfo.apiLevel).thenReturn(Build.VERSION_CODES.N) + + // WHEN + // factory is called to create content observer worker + val worker = factory.createWorker(context, ContentObserverWork::class.java.name, params) + + // THEN + // factory creates a worker compatible with API level + assertNotNull(worker) + } + + @Test + fun `worker is not created below api level 24`() { + // GIVEN + // api level is < 24 + // content URI trigger is not supported + whenever(deviceInfo.apiLevel).thenReturn(Build.VERSION_CODES.M) + + // WHEN + // factory is called to create content observer worker + val worker = factory.createWorker(context, ContentObserverWork::class.java.name, params) + + // THEN + // factory does not create a worker incompatible with API level + assertNull(worker) + } +} diff --git a/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt b/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt new file mode 100644 index 0000000000..3144600cdb --- /dev/null +++ b/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt @@ -0,0 +1,118 @@ +package com.nextcloud.client.jobs + +import android.content.Context +import android.net.Uri +import androidx.work.WorkerParameters +import com.nextcloud.client.device.PowerManagementService +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import com.owncloud.android.datamodel.SyncedFolderProvider +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +class ContentObserverWorkTest { + + private lateinit var worker: ContentObserverWork + + @Mock + lateinit var params: WorkerParameters + + @Mock + lateinit var context: Context + + @Mock + lateinit var folderProvider: SyncedFolderProvider + + @Mock + lateinit var powerManagementService: PowerManagementService + + @Mock + lateinit var backgroundJobManager: BackgroundJobManager + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + worker = ContentObserverWork( + appContext = context, + params = params, + syncerFolderProvider = folderProvider, + powerManagementService = powerManagementService, + backgroundJobManager = backgroundJobManager + ) + val uri: Uri = Mockito.mock(Uri::class.java) + whenever(params.triggeredContentUris).thenReturn(listOf(uri)) + } + + @Test + fun `job reschedules self after each run unconditionally`() { + // GIVEN + // nothing to sync + whenever(params.triggeredContentUris).thenReturn(emptyList()) + + // WHEN + // worker is called + worker.doWork() + + // THEN + // worker reschedules itself unconditionally + verify(backgroundJobManager).scheduleContentObserverJob() + } + + @Test + @Ignore("TODO: needs further refactoring") + fun `sync is triggered`() { + // GIVEN + // power saving is disabled + // some folders are configured for syncing + whenever(powerManagementService.isPowerSavingEnabled).thenReturn(false) + whenever(folderProvider.countEnabledSyncedFolders()).thenReturn(1) + + // WHEN + // worker is called + worker.doWork() + + // THEN + // sync job is scheduled + // TO DO: verify(backgroundJobManager).sheduleFilesSync() or something like this + } + + @Test + @Ignore("TODO: needs further refactoring") + fun `sync is not triggered under power saving mode`() { + // GIVEN + // power saving is enabled + // some folders are configured for syncing + whenever(powerManagementService.isPowerSavingEnabled).thenReturn(true) + whenever(folderProvider.countEnabledSyncedFolders()).thenReturn(1) + + // WHEN + // worker is called + worker.doWork() + + // THEN + // sync job is scheduled + // TO DO: verify(backgroundJobManager, never()).sheduleFilesSync() or something like this) + } + + @Test + @Ignore("TODO: needs further refactoring") + fun `sync is not triggered if no folder are synced`() { + // GIVEN + // power saving is disabled + // no folders configured for syncing + whenever(powerManagementService.isPowerSavingEnabled).thenReturn(false) + whenever(folderProvider.countEnabledSyncedFolders()).thenReturn(0) + + // WHEN + // worker is called + worker.doWork() + + // THEN + // sync job is scheduled + // TO DO: verify(backgroundJobManager, never()).sheduleFilesSync() or something like this) + } +}