diff --git a/app/src/main/java/org/openedx/app/AppViewModel.kt b/app/src/main/java/org/openedx/app/AppViewModel.kt index 1febbd15a..c18e48026 100644 --- a/app/src/main/java/org/openedx/app/AppViewModel.kt +++ b/app/src/main/java/org/openedx/app/AppViewModel.kt @@ -13,6 +13,7 @@ import org.openedx.core.BaseViewModel import org.openedx.core.SingleEventLiveData import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.utils.FileUtil class AppViewModel( private val config: Config, @@ -21,6 +22,7 @@ class AppViewModel( private val preferencesManager: CorePreferences, private val dispatcher: CoroutineDispatcher, private val analytics: AppAnalytics, + private val fileUtil: FileUtil, ) : BaseViewModel() { private val _logoutUser = SingleEventLiveData<Unit>() @@ -32,10 +34,14 @@ class AppViewModel( private var logoutHandledAt: Long = 0 val isBranchEnabled get() = config.getBranchConfig().enabled + private val canResetAppDirectory get() = preferencesManager.canResetAppDirectory override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) setUserId() + if (canResetAppDirectory) { + resetAppDirectory() + } viewModelScope.launch { notifier.notifier.collect { event -> if (event is LogoutEvent && System.currentTimeMillis() - logoutHandledAt > 5000) { @@ -60,6 +66,11 @@ class AppViewModel( ) } + private fun resetAppDirectory() { + fileUtil.deleteOldAppDirectory() + preferencesManager.canResetAppDirectory = false + } + private fun setUserId() { preferencesManager.user?.let { analytics.setUserIdForSession(it.id) diff --git a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt index 603876d54..e0b65af14 100644 --- a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt +++ b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt @@ -152,6 +152,12 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences } get() = getBoolean(APP_WAS_POSITIVE_RATED) + override var canResetAppDirectory: Boolean + set(value) { + saveBoolean(RESET_APP_DIRECTORY, value) + } + get() = getBoolean(RESET_APP_DIRECTORY, true) + override fun setCalendarSyncEventsDialogShown(courseName: String) { saveBoolean(courseName.replaceSpace("_"), true) } @@ -172,5 +178,6 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences private const val VIDEO_SETTINGS_STREAMING_QUALITY = "video_settings_streaming_quality" private const val VIDEO_SETTINGS_DOWNLOAD_QUALITY = "video_settings_download_quality" private const val APP_CONFIG = "app_config" + private const val RESET_APP_DIRECTORY = "reset_app_directory" } } diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index cd3615e26..393b16248 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -67,7 +67,7 @@ import org.openedx.whatsnew.presentation.whatsnew.WhatsNewViewModel val screenModule = module { - viewModel { AppViewModel(get(), get(), get(), get(), get(named("IODispatcher")), get()) } + viewModel { AppViewModel(get(), get(), get(), get(), get(named("IODispatcher")), get(), get()) } viewModel { MainViewModel(get(), get(), get()) } factory { AuthRepository(get(), get(), get()) } diff --git a/app/src/test/java/org/openedx/AppViewModelTest.kt b/app/src/test/java/org/openedx/AppViewModelTest.kt index 40b3e813d..c81c9c2e5 100644 --- a/app/src/test/java/org/openedx/AppViewModelTest.kt +++ b/app/src/test/java/org/openedx/AppViewModelTest.kt @@ -28,6 +28,7 @@ import org.openedx.app.system.notifier.AppNotifier import org.openedx.app.system.notifier.LogoutEvent import org.openedx.core.config.Config import org.openedx.core.data.model.User +import org.openedx.core.utils.FileUtil @ExperimentalCoroutinesApi class AppViewModelTest { @@ -42,6 +43,7 @@ class AppViewModelTest { private val room = mockk<AppDatabase>() private val preferencesManager = mockk<PreferencesManager>() private val analytics = mockk<AppAnalytics>() + private val fileUtil = mockk<FileUtil>() private val user = User(0, "", "", "") @@ -60,8 +62,17 @@ class AppViewModelTest { every { analytics.setUserIdForSession(any()) } returns Unit every { preferencesManager.user } returns user every { notifier.notifier } returns flow { } + every { preferencesManager.canResetAppDirectory } returns false val viewModel = - AppViewModel(config, notifier, room, preferencesManager, dispatcher, analytics) + AppViewModel( + config, + notifier, + room, + preferencesManager, + dispatcher, + analytics, + fileUtil + ) val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) @@ -82,8 +93,17 @@ class AppViewModelTest { every { preferencesManager.user } returns user every { room.clearAllTables() } returns Unit every { analytics.logoutEvent(true) } returns Unit + every { preferencesManager.canResetAppDirectory } returns false val viewModel = - AppViewModel(config, notifier, room, preferencesManager, dispatcher, analytics) + AppViewModel( + config, + notifier, + room, + preferencesManager, + dispatcher, + analytics, + fileUtil + ) val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) @@ -106,8 +126,17 @@ class AppViewModelTest { every { preferencesManager.user } returns user every { room.clearAllTables() } returns Unit every { analytics.logoutEvent(true) } returns Unit + every { preferencesManager.canResetAppDirectory } returns false val viewModel = - AppViewModel(config, notifier, room, preferencesManager, dispatcher, analytics) + AppViewModel( + config, + notifier, + room, + preferencesManager, + dispatcher, + analytics, + fileUtil + ) val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) diff --git a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt index 48999ab4e..f9cacbd04 100644 --- a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt +++ b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt @@ -11,6 +11,7 @@ interface CorePreferences { var user: User? var videoSettings: VideoSettings var appConfig: AppConfig + var canResetAppDirectory: Boolean fun clear() } diff --git a/core/src/main/java/org/openedx/core/utils/FileUtil.kt b/core/src/main/java/org/openedx/core/utils/FileUtil.kt index 2f5c2b2e5..a59317193 100644 --- a/core/src/main/java/org/openedx/core/utils/FileUtil.kt +++ b/core/src/main/java/org/openedx/core/utils/FileUtil.kt @@ -4,6 +4,7 @@ import android.content.Context import com.google.gson.Gson import com.google.gson.GsonBuilder import java.io.File +import java.util.Collections class FileUtil(val context: Context) { @@ -15,7 +16,10 @@ class FileUtil(val context: Context) { return file } - inline fun <reified T> saveObjectToFile(obj: T, fileName: String = "${T::class.java.simpleName}.json") { + inline fun <reified T> saveObjectToFile( + obj: T, + fileName: String = "${T::class.java.simpleName}.json", + ) { val gson: Gson = GsonBuilder().setPrettyPrinting().create() val jsonString = gson.toJson(obj) File(getExternalAppDir().path + fileName).writeText(jsonString) @@ -31,6 +35,43 @@ class FileUtil(val context: Context) { null } } + + /** + * Deletes all the files and directories in the app's external storage directory. + */ + fun deleteOldAppDirectory() { + val externalFilesDir = context.getExternalFilesDir(null) + val externalAppDir = File(externalFilesDir?.parentFile, Directories.VIDEOS.name) + if (externalAppDir.isDirectory) { + deleteRecursive(externalAppDir, Collections.emptyList()) + } + } + + /** + * Deletes a file or directory and all its content recursively. + * + * @param fileOrDirectory The file or directory that needs to be deleted. + * @param exceptions Names of the files or directories that need to be skipped while deletion. + */ + private fun deleteRecursive( + fileOrDirectory: File, + exceptions: List<String>, + ) { + if (exceptions.contains(fileOrDirectory.name)) return + + if (fileOrDirectory.isDirectory) { + val filesList = fileOrDirectory.listFiles() + if (filesList != null) { + for (child in filesList) { + deleteRecursive(child, exceptions) + } + } + } + + // Don't break the recursion upon encountering an error + // noinspection ResultOfMethodCallIgnored + fileOrDirectory.delete() + } } enum class Directories { diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt index 127164cc2..0a7f59c93 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt @@ -381,7 +381,7 @@ private fun CourseItem( apiHostUrl: String, enrolledCourse: EnrolledCourse, windowSize: WindowSize, - onClick: (EnrolledCourse) -> Unit + onClick: (EnrolledCourse) -> Unit, ) { val imageWidth by remember(key1 = windowSize) { mutableStateOf( diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt index 812e52f2e..82814561a 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -27,7 +27,7 @@ class DashboardListViewModel( private val resourceManager: ResourceManager, private val discoveryNotifier: DiscoveryNotifier, private val analytics: DashboardAnalytics, - private val appUpgradeNotifier: AppUpgradeNotifier + private val appUpgradeNotifier: AppUpgradeNotifier, ) : BaseViewModel() { private val coursesList = mutableListOf<EnrolledCourse>()