From 44c703b7bb3a757410b6e83223d114ad0925d14b Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 10 Dec 2023 00:30:19 +0100 Subject: [PATCH 01/29] start adding database table and classes for app storage stats --- .../home_page/database/AppDatabase.kt | 18 +++++- .../home_page/database/AppStorageStat.kt | 59 +++++++++++++++++++ .../home_page/database/AppStorageStatsDao.kt | 39 ++++++++++++ .../home_page/database/StorageStatsPerApp.kt | 33 +++++++++++ .../database/StorageStatsPerAppDao.kt | 49 +++++++++++++++ 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStat.kt create mode 100644 app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt create mode 100644 app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerApp.kt create mode 100644 app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerAppDao.kt diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt index 0ad9c991..2d2b934d 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt @@ -37,7 +37,7 @@ import com.amaze.fileutilities.utilis.DbConverters SimilarImagesAnalysisMetadata::class ], exportSchema = true, - version = 4 + version = 5 ) @TypeConverters(DbConverters::class) abstract class AppDatabase : RoomDatabase() { @@ -124,5 +124,21 @@ abstract class AppDatabase : RoomDatabase() { ) } } + + private val MIGRATION_4_5 = object : Migration(4, 5) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + "CREATE TABLE IF NOT EXISTS `AppStorageStats` " + + "(`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`package_id` INTEGER NOT NULL, " + // see `InstalledApps._id` + "`timestamp` LONG NOT NULL, " + + "`package_size` LONG NOT NULL)" + ) + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_AppStorageStats_package_id`" + + " ON `AppStorageStats` (`package_id`)" + ) + } + } } } diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStat.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStat.kt new file mode 100644 index 00000000..260fb724 --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStat.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.fileutilities.home_page.database + +import androidx.annotation.Keep +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.NO_ACTION +import androidx.room.Ignore +import androidx.room.Index +import androidx.room.PrimaryKey +import java.util.Date + +@Entity( + indices = [Index(value = ["package_id"], unique = false)], + foreignKeys = [ + ForeignKey( + entity = InstalledApps::class, + parentColumns = ["_id"], + childColumns = ["package_id"], + onDelete = NO_ACTION + ) + ] +) +@Keep +data class AppStorageStat( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "_id") + val uid: Int, + @ColumnInfo(name = "package_id") val packageId: Int, + @ColumnInfo(name = "timestamp") val timestamp: Date, + @ColumnInfo(name = "package_size") val packageSize: Long +) { + @Ignore + constructor( + packageId: Int, + timestamp: Date, + packageSize: Long + ) : this(0, packageId, timestamp, packageSize) +} diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt new file mode 100644 index 00000000..675965bc --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.fileutilities.home_page.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import java.util.Date + +@Dao +interface AppStorageStatsDao { + @Query("SELECT * FROM AppStorageStat") + fun findAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(appStorageStats: List) + + @Query("DELETE FROM AppStorageStat WHERE timestamp < :date") + fun deleteOlderThan(date: Date) +} diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerApp.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerApp.kt new file mode 100644 index 00000000..97e76d1e --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerApp.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.fileutilities.home_page.database + +import androidx.room.Embedded +import androidx.room.Relation + +data class StorageStatsPerApp( + @Embedded val app: InstalledApps, + @Relation( + parentColumn = "_id", + entityColumn = "package_id" + ) + val appStorageStats: List +) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerAppDao.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerAppDao.kt new file mode 100644 index 00000000..7a2258b4 --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerAppDao.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.fileutilities.home_page.database + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import java.util.Date + +@Dao +interface StorageStatsPerAppDao { + @Transaction + @Query("SELECT * FROM InstalledApps") + fun findAll(): List + + @Transaction + @Query("SELECT * FROM InstalledApps WHERE package_name=:packageName") + fun findByPackageName(packageName: String): StorageStatsPerApp? + + @Transaction + fun findByDay(dayStart: Date, dayEnd: Date): List { + val all = findAll() + return all.map { storageStatsPerApp -> + storageStatsPerApp.copy( + appStorageStats = storageStatsPerApp.appStorageStats.filter { + it.timestamp.after(dayStart) && it.timestamp.before(dayEnd) + } + ) + } + } +} From 93d148084c08624f4730eda5709fa2ef7f245f72 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Wed, 13 Dec 2023 16:22:40 +0100 Subject: [PATCH 02/29] make `package_id` a foreign key in create table query and define onChange policy --- .../amaze/fileutilities/home_page/database/AppDatabase.kt | 5 +++-- .../amaze/fileutilities/home_page/database/AppStorageStat.kt | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt index 2d2b934d..417c0c2a 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt @@ -130,9 +130,10 @@ abstract class AppDatabase : RoomDatabase() { database.execSQL( "CREATE TABLE IF NOT EXISTS `AppStorageStats` " + "(`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "`package_id` INTEGER NOT NULL, " + // see `InstalledApps._id` "`timestamp` LONG NOT NULL, " + - "`package_size` LONG NOT NULL)" + "`package_size` LONG NOT NULL," + + "FOREIGN KEY(`package_id`) REFERENCES `InstalledApps`(`_id`) ON UPDATE " + + "CASCADE ON DELETE NO ACTION)" ) database.execSQL( "CREATE INDEX IF NOT EXISTS `index_AppStorageStats_package_id`" + diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStat.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStat.kt index 260fb724..35d9c228 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStat.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStat.kt @@ -24,6 +24,7 @@ import androidx.annotation.Keep import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey +import androidx.room.ForeignKey.CASCADE import androidx.room.ForeignKey.NO_ACTION import androidx.room.Ignore import androidx.room.Index @@ -37,7 +38,8 @@ import java.util.Date entity = InstalledApps::class, parentColumns = ["_id"], childColumns = ["package_id"], - onDelete = NO_ACTION + onDelete = NO_ACTION, + onUpdate = CASCADE ) ] ) From 099918bb4f45e5628386537c84c14adc5e727a2e Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Wed, 13 Dec 2023 16:27:31 +0100 Subject: [PATCH 03/29] change index to include `timestamp` --- .../com/amaze/fileutilities/home_page/database/AppDatabase.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt index 417c0c2a..1c6ce2f4 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt @@ -136,8 +136,8 @@ abstract class AppDatabase : RoomDatabase() { "CASCADE ON DELETE NO ACTION)" ) database.execSQL( - "CREATE INDEX IF NOT EXISTS `index_AppStorageStats_package_id`" + - " ON `AppStorageStats` (`package_id`)" + "CREATE INDEX IF NOT EXISTS `index_AppStorageStats_timestamp_package_id`" + + " ON `AppStorageStats` (`timestamp`,`package_id`)" ) } } From a5f3f7383b829e54b99c774c4fce0ed65816eaa3 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Wed, 13 Dec 2023 16:28:35 +0100 Subject: [PATCH 04/29] rename to `AppStorageStats`and add inserts for single `AppStorageStats` in dao --- .../{AppStorageStat.kt => AppStorageStats.kt} | 2 +- .../home_page/database/AppStorageStatsDao.kt | 27 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) rename app/src/main/java/com/amaze/fileutilities/home_page/database/{AppStorageStat.kt => AppStorageStats.kt} (98%) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStat.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt similarity index 98% rename from app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStat.kt rename to app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt index 35d9c228..66c36b32 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStat.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt @@ -44,7 +44,7 @@ import java.util.Date ] ) @Keep -data class AppStorageStat( +data class AppStorageStats( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "_id") val uid: Int, diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt index 675965bc..25b32b5d 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt @@ -24,16 +24,35 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction import java.util.Date @Dao interface AppStorageStatsDao { - @Query("SELECT * FROM AppStorageStat") - fun findAll(): List + fun installedAppsDao(): InstalledAppsDao + + @Query("SELECT * FROM AppStorageStats") + fun findAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(appStorageStats: List) @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(appStorageStats: List) + fun insert(appStorageStats: AppStorageStats) + + /** + * Inserts a new [AppStorageStats] associated with the [packageName] and containing [timestamp] + * and [size] + */ + @Transaction + fun insert(packageName: String, timestamp: Date, size: Long) { + val app = installedAppsDao().findByPackageName(packageName) + if (app != null) { + val appStorageStats = AppStorageStats(app.uid, timestamp, size) + insert(appStorageStats) + } + } - @Query("DELETE FROM AppStorageStat WHERE timestamp < :date") + @Query("DELETE FROM AppStorageStats WHERE timestamp < :date") fun deleteOlderThan(date: Date) } From 14e7233a8d80d69953387d10313d396fec6e6ded Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Wed, 13 Dec 2023 16:32:13 +0100 Subject: [PATCH 05/29] change to use intermediate data class instead of embedded objects to combine `AppStorageStats` and `InstalledApps` --- ...StatsPerApp.kt => StorageStatToAppName.kt} | 15 ++--- .../database/StorageStatToAppNameDao.kt | 67 +++++++++++++++++++ .../database/StorageStatsPerAppDao.kt | 49 -------------- 3 files changed, 73 insertions(+), 58 deletions(-) rename app/src/main/java/com/amaze/fileutilities/home_page/database/{StorageStatsPerApp.kt => StorageStatToAppName.kt} (78%) create mode 100644 app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDao.kt delete mode 100644 app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerAppDao.kt diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerApp.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppName.kt similarity index 78% rename from app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerApp.kt rename to app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppName.kt index 97e76d1e..7a131759 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerApp.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppName.kt @@ -20,14 +20,11 @@ package com.amaze.fileutilities.home_page.database -import androidx.room.Embedded -import androidx.room.Relation +import androidx.room.ColumnInfo +import java.util.Date -data class StorageStatsPerApp( - @Embedded val app: InstalledApps, - @Relation( - parentColumn = "_id", - entityColumn = "package_id" - ) - val appStorageStats: List +data class StorageStatToAppName( + @ColumnInfo(name = "package_name") val packageName: String, + @ColumnInfo(name = "timestamp") val timestamp: Date, + @ColumnInfo(name = "package_size") val packageSize: Long ) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDao.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDao.kt new file mode 100644 index 00000000..ed964867 --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDao.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.fileutilities.home_page.database + +import androidx.room.Dao +import androidx.room.Query +import java.util.Date + +@Dao +interface StorageStatToAppNameDao { + @Query( + "SELECT I.package_name AS package_name, " + + "A.timestamp AS timestamp, " + + "A.package_size AS package_size " + + "FROM AppStorageStats AS A " + + "INNER JOIN InstalledApps AS I " + + "ON I._id = A.package_id" + ) + fun findAll(): List + + @Query( + "SELECT I.package_name AS package_name, " + + "A.timestamp AS timestamp, " + + "A.package_size AS package_size " + + "FROM AppStorageStats AS A " + + "INNER JOIN InstalledApps AS I " + + "ON I._id = A.package_id " + + "WHERE I.package_name=:packageName" + ) + fun findByPackageName(packageName: String): List + + @Query( + "SELECT I.package_name AS package_name, " + + "A.timestamp AS timestamp, " + + "A.package_size AS package_size " + + "FROM AppStorageStats AS A " + + "INNER JOIN InstalledApps AS I " + + "ON I._id = A.package_id " + + "WHERE I.package_name=:packageName " + + "AND A.timestamp >= :periodStart " + // Ensure that timestamp is after `periodStart` + "AND A.timestamp < :periodEnd " + // Ensure that timestamp is before `periodEnd` + "ORDER BY A.timestamp ASC LIMIT 1" // Get the oldest entry based on timestamp + ) + fun findOldestWithinPeriod( + packageName: String, + periodStart: Date, + periodEnd: Date + ): StorageStatToAppName +} diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerAppDao.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerAppDao.kt deleted file mode 100644 index 7a2258b4..00000000 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatsPerAppDao.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , - * Emmanuel Messulam, Raymond Lai and Contributors. - * - * This file is part of Amaze File Utilities. - * - * Amaze File Utilities is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.amaze.fileutilities.home_page.database - -import androidx.room.Dao -import androidx.room.Query -import androidx.room.Transaction -import java.util.Date - -@Dao -interface StorageStatsPerAppDao { - @Transaction - @Query("SELECT * FROM InstalledApps") - fun findAll(): List - - @Transaction - @Query("SELECT * FROM InstalledApps WHERE package_name=:packageName") - fun findByPackageName(packageName: String): StorageStatsPerApp? - - @Transaction - fun findByDay(dayStart: Date, dayEnd: Date): List { - val all = findAll() - return all.map { storageStatsPerApp -> - storageStatsPerApp.copy( - appStorageStats = storageStatsPerApp.appStorageStats.filter { - it.timestamp.after(dayStart) && it.timestamp.before(dayEnd) - } - ) - } - } -} From acd36fe13211418d52ced4c678362ed9c1a5a902 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Wed, 13 Dec 2023 16:36:01 +0100 Subject: [PATCH 06/29] add new daos to `AppDatabase` --- .../com/amaze/fileutilities/home_page/database/AppDatabase.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt index 1c6ce2f4..a82046ca 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt @@ -53,6 +53,8 @@ abstract class AppDatabase : RoomDatabase() { abstract fun trialValidatorDao(): TrialValidatorDao abstract fun installedAppsDao(): InstalledAppsDao abstract fun lyricsDao(): LyricsDao + abstract fun appStorageStatsDao(): AppStorageStatsDao + abstract fun storageStatsPerAppDao(): StorageStatToAppNameDao companion object { private var appDatabase: AppDatabase? = null From c39a1b7046fb078e93dc7eae8a91270f3f68b4ca Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Wed, 13 Dec 2023 16:36:58 +0100 Subject: [PATCH 07/29] add new function where the size of the `MediaFileInfo` can be specified --- .../home_page/ui/files/MediaFileInfo.kt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/MediaFileInfo.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/MediaFileInfo.kt index 7fa75ab9..6ff50f9c 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/MediaFileInfo.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/MediaFileInfo.kt @@ -150,6 +150,19 @@ data class MediaFileInfo( context: Context, applicationInfo: ApplicationInfo, packageInfo: PackageInfo? + ): MediaFileInfo? { + if (applicationInfo.sourceDir == null) { + return null + } + val size = Utils.findApplicationInfoSize(context, applicationInfo) + return fromApplicationInfoWithSize(context, applicationInfo, packageInfo, size) + } + + fun fromApplicationInfoWithSize( + context: Context, + applicationInfo: ApplicationInfo, + packageInfo: PackageInfo?, + size: Long ): MediaFileInfo? { if (applicationInfo.sourceDir == null) { return null @@ -162,7 +175,8 @@ data class MediaFileInfo( applicationInfo.loadLabel(packageManager) as String, applicationInfo.sourceDir, apkFile.lastModified(), - Utils.findApplicationInfoSize(context, applicationInfo), false + size, + false ) val extraInfo = ExtraInfo( MEDIA_TYPE_APK, From c8f11fad3eb8a9a4980df4abf28012eb58bc57b4 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Wed, 13 Dec 2023 21:50:57 +0100 Subject: [PATCH 08/29] fix some issues with the new database table Add the `AppStorageStats` entity to the database, fix the index in the annotation of `AppStorageStats` and fix `AppStorageStatsDao` to get `InstalledAppsDao` from database --- .../home_page/database/AppDatabase.kt | 2 +- .../home_page/database/AppStorageStats.kt | 2 +- .../home_page/database/AppStorageStatsDao.kt | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt index a82046ca..752d3474 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt @@ -34,7 +34,7 @@ import com.amaze.fileutilities.utilis.DbConverters ImageAnalysis::class, InternalStorageAnalysis::class, PathPreferences::class, BlurAnalysis::class, LowLightAnalysis::class, MemeAnalysis::class, VideoPlayerState::class, Trial::class, Lyrics::class, InstalledApps::class, SimilarImagesAnalysis::class, - SimilarImagesAnalysisMetadata::class + SimilarImagesAnalysisMetadata::class, AppStorageStats::class ], exportSchema = true, version = 5 diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt index 66c36b32..fa2030ed 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt @@ -32,7 +32,7 @@ import androidx.room.PrimaryKey import java.util.Date @Entity( - indices = [Index(value = ["package_id"], unique = false)], + indices = [Index(value = ["timestamp", "package_id"], unique = false)], foreignKeys = [ ForeignKey( entity = InstalledApps::class, diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt index 25b32b5d..ffae0fbb 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt @@ -28,25 +28,25 @@ import androidx.room.Transaction import java.util.Date @Dao -interface AppStorageStatsDao { - fun installedAppsDao(): InstalledAppsDao +abstract class AppStorageStatsDao(database: AppDatabase) { + private val installedAppsDao: InstalledAppsDao = database.installedAppsDao() @Query("SELECT * FROM AppStorageStats") - fun findAll(): List + abstract fun findAll(): List @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(appStorageStats: List) + abstract fun insert(appStorageStats: List) @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(appStorageStats: AppStorageStats) + abstract fun insert(appStorageStats: AppStorageStats) /** * Inserts a new [AppStorageStats] associated with the [packageName] and containing [timestamp] * and [size] */ @Transaction - fun insert(packageName: String, timestamp: Date, size: Long) { - val app = installedAppsDao().findByPackageName(packageName) + open fun insert(packageName: String, timestamp: Date, size: Long) { + val app = installedAppsDao.findByPackageName(packageName) if (app != null) { val appStorageStats = AppStorageStats(app.uid, timestamp, size) insert(appStorageStats) @@ -54,5 +54,5 @@ interface AppStorageStatsDao { } @Query("DELETE FROM AppStorageStats WHERE timestamp < :date") - fun deleteOlderThan(date: Date) + abstract fun deleteOlderThan(date: Date) } From 397b0964f94b4762b8d61e5561483eaa6d7788f9 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Wed, 13 Dec 2023 16:47:03 +0100 Subject: [PATCH 09/29] add a priority queue with fixed size --- .../utilis/FixedSizePriorityQueue.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 app/src/main/java/com/amaze/fileutilities/utilis/FixedSizePriorityQueue.kt diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/FixedSizePriorityQueue.kt b/app/src/main/java/com/amaze/fileutilities/utilis/FixedSizePriorityQueue.kt new file mode 100644 index 00000000..49fd996e --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/utilis/FixedSizePriorityQueue.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.fileutilities.utilis + +import java.util.PriorityQueue + +/** + * A [PriorityQueue] that will at most only contain [fixedSize] many elements. + * If more elements than [fixedSize] are added, the smallest elements based on [comparator] will be removed. + * Therefore, this priority queue will only contain the largest elements that were added to it. + */ +class FixedSizePriorityQueue( + private val fixedSize: Int, + comparator: Comparator +) : PriorityQueue(fixedSize + 1, comparator) { + // initial capacity is set to fixedSize + 1 because we first add the new element and then fix the size + /** + * Adds [element] to the priority queue. + * If there are already [fixedSize] many elements in the queue then the smallest element is removed. + */ + override fun add(element: E): Boolean { + super.add(element) + // Makes sure that the size of the priority queue doesn't exceed fixedSize + if (this.size > fixedSize) { + this.remove() + } + return true + } +} From 0d915bc4afb1405b3f771de86b81c142d41c5141 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Wed, 13 Dec 2023 21:54:41 +0100 Subject: [PATCH 10/29] change to use new `FixedSizePrioityQueue` --- .../home_page/ui/analyse/AnalyseViewModel.kt | 8 ++--- .../home_page/ui/files/FilesViewModel.kt | 35 ++++++------------- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseViewModel.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseViewModel.kt index 6f3c9cc0..d0380af0 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseViewModel.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseViewModel.kt @@ -39,6 +39,7 @@ import com.amaze.fileutilities.home_page.database.SimilarImagesAnalysis import com.amaze.fileutilities.home_page.database.SimilarImagesAnalysisDao import com.amaze.fileutilities.home_page.ui.files.MediaFileInfo import com.amaze.fileutilities.utilis.AbstractMediaFilesAdapter +import com.amaze.fileutilities.utilis.FixedSizePriorityQueue import com.amaze.fileutilities.utilis.PreferencesConstants import com.amaze.fileutilities.utilis.invalidate import kotlinx.coroutines.Dispatchers @@ -287,13 +288,10 @@ class AnalyseViewModel : ViewModel() { private fun processLargeVideos(videosList: List) { viewModelScope.launch(Dispatchers.IO) { - val priorityQueue = PriorityQueue( + val priorityQueue = FixedSizePriorityQueue( 100 ) { o1, o2 -> o1.longSize.compareTo(o2.longSize) } - videosList.forEachIndexed { index, mediaFileInfo -> - if (index > 99) { - priorityQueue.remove() - } + videosList.forEach { mediaFileInfo -> priorityQueue.add(mediaFileInfo) } val result = ArrayList() diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt index 66d400c5..ebfd40b0 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt @@ -72,6 +72,7 @@ import com.amaze.fileutilities.home_page.ui.AggregatedMediaFileInfoObserver import com.amaze.fileutilities.home_page.ui.options.Billing import com.amaze.fileutilities.utilis.CursorUtils import com.amaze.fileutilities.utilis.FileUtils +import com.amaze.fileutilities.utilis.FixedSizePriorityQueue import com.amaze.fileutilities.utilis.ImgUtils import com.amaze.fileutilities.utilis.MLUtils import com.amaze.fileutilities.utilis.PreferencesConstants @@ -112,7 +113,6 @@ import java.util.Calendar import java.util.Collections import java.util.Date import java.util.GregorianCalendar -import java.util.PriorityQueue import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicReference import kotlin.streams.toList @@ -1671,7 +1671,7 @@ class FilesViewModel(val applicationContext: Application) : viewModelScope.launch(Dispatchers.IO) { loadAllInstalledApps(packageManager) - val priorityQueue = PriorityQueue( + val priorityQueue = FixedSizePriorityQueue( 50 ) { o1, o2 -> o2.extraInfo?.apkMetaData?.networkBytes?.let { @@ -1681,10 +1681,7 @@ class FilesViewModel(val applicationContext: Application) : } ?: 0 } - allApps.get()?.forEachIndexed { index, applicationInfo -> - if (index > 49 && priorityQueue.isNotEmpty()) { - priorityQueue.remove() - } + allApps.get()?.forEach { applicationInfo -> MediaFileInfo.fromApplicationInfo( applicationContext, applicationInfo.first, applicationInfo.second @@ -1832,14 +1829,11 @@ class FilesViewModel(val applicationContext: Application) : viewModelScope.launch(Dispatchers.IO) { loadAllInstalledApps(packageManager) - val priorityQueue = PriorityQueue( + val priorityQueue = FixedSizePriorityQueue( 50 ) { o1, o2 -> o1.longSize.compareTo(o2.longSize) } - allApps.get()?.forEachIndexed { index, applicationInfo -> - if (index > 49 && priorityQueue.isNotEmpty()) { - priorityQueue.remove() - } + allApps.get()?.forEach { applicationInfo -> MediaFileInfo.fromApplicationInfo( applicationContext, applicationInfo.first, applicationInfo.second @@ -1876,7 +1870,7 @@ class FilesViewModel(val applicationContext: Application) : PreferencesConstants.DEFAULT_NEWLY_INSTALLED_APPS_DAYS ) val pastDate = LocalDateTime.now().minusDays(days.toLong()) - val priorityQueue = PriorityQueue( + val priorityQueue = FixedSizePriorityQueue( 50 ) { o1, o2 -> o1.longSize.compareTo(o2.longSize) } @@ -1885,10 +1879,7 @@ class FilesViewModel(val applicationContext: Application) : val installDateTime = Instant.ofEpochMilli(it.second?.firstInstallTime!!) .atZone(ZoneId.systemDefault()).toLocalDate() installDateTime.isAfter(pastDate.toLocalDate()) - }?.forEachIndexed { index, applicationInfo -> - if (index > 49 && priorityQueue.isNotEmpty()) { - priorityQueue.remove() - } + }?.forEach { applicationInfo -> MediaFileInfo.fromApplicationInfo( applicationContext, applicationInfo.first, applicationInfo.second @@ -1925,7 +1916,7 @@ class FilesViewModel(val applicationContext: Application) : PreferencesConstants.DEFAULT_RECENTLY_UPDATED_APPS_DAYS ) val pastDate = LocalDateTime.now().minusDays(days.toLong()) - val priorityQueue = PriorityQueue( + val priorityQueue = FixedSizePriorityQueue( 50 ) { o1, o2 -> o1.longSize.compareTo(o2.longSize) } @@ -1934,10 +1925,7 @@ class FilesViewModel(val applicationContext: Application) : val updateDateTime = Instant.ofEpochMilli(it.second?.lastUpdateTime!!) .atZone(ZoneId.systemDefault()).toLocalDate() updateDateTime.isAfter(pastDate.toLocalDate()) - }?.forEachIndexed { index, applicationInfo -> - if (index > 49 && priorityQueue.isNotEmpty()) { - priorityQueue.remove() - } + }?.forEach { applicationInfo -> MediaFileInfo.fromApplicationInfo( applicationContext, applicationInfo.first, applicationInfo.second @@ -2169,7 +2157,7 @@ class FilesViewModel(val applicationContext: Application) : paths: List, limit: Int ): ArrayList { - val priorityQueue = PriorityQueue(limit, sortBy) + val priorityQueue = FixedSizePriorityQueue(limit, sortBy) if (allMediaFilesPair == null) { allMediaFilesPair = CursorUtils.listAll(applicationContext) } @@ -2179,9 +2167,6 @@ class FilesViewModel(val applicationContext: Application) : it.path.contains(pathPref, true) } }?.forEach { - if (priorityQueue.isNotEmpty() && priorityQueue.size > limit - 1) { - priorityQueue.remove() - } priorityQueue.add(it) } From 48992737ffa38965a99311f3d31ab8ed242f253c Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sat, 16 Dec 2023 20:24:17 +0100 Subject: [PATCH 11/29] change `findOldestWithinPeriod` to return Nullable object since there might be no matching entry --- .../fileutilities/home_page/database/StorageStatToAppNameDao.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDao.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDao.kt index ed964867..4ec83226 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDao.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDao.kt @@ -63,5 +63,5 @@ interface StorageStatToAppNameDao { packageName: String, periodStart: Date, periodEnd: Date - ): StorageStatToAppName + ): StorageStatToAppName? } From 9ba8ee0cc6be89945c11ed5562598a973f9e73d8 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 17 Dec 2023 00:06:07 +0100 Subject: [PATCH 12/29] fix migration to use correct datatypes in sql statement and add index for foreign key --- .../fileutilities/home_page/database/AppDatabase.kt | 11 ++++++++--- .../home_page/database/AppStorageStats.kt | 5 ++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt index 752d3474..a13f5d62 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt @@ -65,7 +65,7 @@ abstract class AppDatabase : RoomDatabase() { applicationContext, AppDatabase::class.java, "amaze-utils" ).allowMainThreadQueries() - .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) + .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5) .build() } return appDatabase!! @@ -132,8 +132,9 @@ abstract class AppDatabase : RoomDatabase() { database.execSQL( "CREATE TABLE IF NOT EXISTS `AppStorageStats` " + "(`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "`timestamp` LONG NOT NULL, " + - "`package_size` LONG NOT NULL," + + "`package_id` INTEGER NOT NULL, " + + "`timestamp` INTEGER NOT NULL, " + + "`package_size` INTEGER NOT NULL," + "FOREIGN KEY(`package_id`) REFERENCES `InstalledApps`(`_id`) ON UPDATE " + "CASCADE ON DELETE NO ACTION)" ) @@ -141,6 +142,10 @@ abstract class AppDatabase : RoomDatabase() { "CREATE INDEX IF NOT EXISTS `index_AppStorageStats_timestamp_package_id`" + " ON `AppStorageStats` (`timestamp`,`package_id`)" ) + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_AppStorageStats_package_id`" + + " ON `AppStorageStats` (`package_id`)" + ) } } } diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt index fa2030ed..a7711343 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt @@ -32,7 +32,10 @@ import androidx.room.PrimaryKey import java.util.Date @Entity( - indices = [Index(value = ["timestamp", "package_id"], unique = false)], + indices = [ + Index(value = ["timestamp", "package_id"], unique = false), + Index(value = ["package_id"], unique = false) // separate index because it is foreign key + ], foreignKeys = [ ForeignKey( entity = InstalledApps::class, From da9232dc6ad4ae144d8b4df43dc310561716faf8 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 17 Dec 2023 02:05:08 +0100 Subject: [PATCH 13/29] add setting for the number of days for the size diff --- .../ui/settings/AnalysisPrefFragment.kt | 24 +++++++- .../utilis/InputFilterMinMaxLong.kt | 57 +++++++++++++++++++ .../utilis/PreferencesConstants.kt | 4 ++ .../com/amaze/fileutilities/utilis/Utils.kt | 17 ++++-- app/src/main/res/values/strings.xml | 4 ++ app/src/main/res/xml/analysis_prefs.xml | 5 ++ 6 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/amaze/fileutilities/utilis/InputFilterMinMaxLong.kt diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/settings/AnalysisPrefFragment.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/settings/AnalysisPrefFragment.kt index 8758e342..be397dc1 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/settings/AnalysisPrefFragment.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/settings/AnalysisPrefFragment.kt @@ -51,12 +51,14 @@ class AnalysisPrefFragment : PreferenceFragmentCompat(), Preference.OnPreference private const val KEY_LEAST_USED_APPS = "least_used_apps" private const val KEY_NEWLY_INSTALLED_APPS = "newly_installed_apps" private const val KEY_RECENTLY_UPDATED_APPS = "recently_updated_apps" + private const val KEY_LARGE_SIZE_DIFF_APPS = "large_size_diff_apps" private const val KEY_WHATSAPP_MEDIA = "whatsapp_media" private val KEYS = listOf( KEY_DUPLICATES, KEY_MEMES, KEY_BLUR, KEY_LOW_LIGHT, KEY_FEATURES, KEY_SIMILAR_IMAGES, KEY_DOWNLOAD, KEY_RECORDING, KEY_SCREENSHOT, KEY_UNUSED_APPS, KEY_MOST_USED_APPS, KEY_LEAST_USED_APPS, - KEY_NEWLY_INSTALLED_APPS, KEY_RECENTLY_UPDATED_APPS, KEY_WHATSAPP_MEDIA, KEY_TELEGRAM + KEY_NEWLY_INSTALLED_APPS, KEY_RECENTLY_UPDATED_APPS, KEY_WHATSAPP_MEDIA, KEY_TELEGRAM, + KEY_LARGE_SIZE_DIFF_APPS ) } @@ -253,6 +255,26 @@ class AnalysisPrefFragment : PreferenceFragmentCompat(), Preference.OnPreference ).apply() } } + KEY_LARGE_SIZE_DIFF_APPS -> { + val days = prefs.getInt( + PreferencesConstants.KEY_LARGE_SIZE_DIFF_APPS_DAYS, + PreferencesConstants.DEFAULT_LARGE_SIZE_DIFF_APPS_DAYS + ) + Utils.buildDigitInputDialog( + requireContext(), + getString(R.string.large_size_diff_apps), + getString(R.string.large_size_diff_apps_summary) + + " (max. ${PreferencesConstants.MAX_LARGE_SIZE_DIFF_APPS_DAYS} days)", + days.toLong(), + { + prefs.edit().putInt( + PreferencesConstants.KEY_LARGE_SIZE_DIFF_APPS_DAYS, + it?.toInt() ?: PreferencesConstants.DEFAULT_LARGE_SIZE_DIFF_APPS_DAYS + ).apply() + }, + max = PreferencesConstants.MAX_LARGE_SIZE_DIFF_APPS_DAYS.toLong() + ) + } } return true } diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/InputFilterMinMaxLong.kt b/app/src/main/java/com/amaze/fileutilities/utilis/InputFilterMinMaxLong.kt new file mode 100644 index 00000000..675049e8 --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/utilis/InputFilterMinMaxLong.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.fileutilities.utilis + +import android.text.InputFilter +import android.text.Spanned +import java.lang.NumberFormatException + +class InputFilterMinMaxLong( + private val min: Long, + private val max: Long +) : InputFilter { + override fun filter( + source: CharSequence?, + start: Int, + end: Int, + dest: Spanned?, + dstart: Int, + dend: Int + ): CharSequence? { + if (dest == null || source == null) { + return "" + } + return try { + val input = + "${dest.subSequence(0, dstart)}$source${dest.subSequence(dend, dest.length)}" + val number = input.toInt() + if (number < min) { + "" + } else if (number > max) { + "" + } else { + null + } + } catch (e: NumberFormatException) { + "" + } + } +} diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/PreferencesConstants.kt b/app/src/main/java/com/amaze/fileutilities/utilis/PreferencesConstants.kt index da4384f7..acc500ce 100644 --- a/app/src/main/java/com/amaze/fileutilities/utilis/PreferencesConstants.kt +++ b/app/src/main/java/com/amaze/fileutilities/utilis/PreferencesConstants.kt @@ -52,6 +52,7 @@ class PreferencesConstants { const val KEY_LEAST_USED_APPS_DAYS = "least_used_apps_days" const val KEY_NEWLY_INSTALLED_APPS_DAYS = "newly_installed_apps_days" const val KEY_RECENTLY_UPDATED_APPS_DAYS = "recently_updated_apps_days" + const val KEY_LARGE_SIZE_DIFF_APPS_DAYS = "large_size_diff_apps_days" const val KEY_TRASH_BIN_RETENTION_DAYS = "trash_bin_retention_days" const val KEY_TRASH_BIN_RETENTION_BYTES = "trash_bin_retention_bytes" const val KEY_TRASH_BIN_RETENTION_NUM_OF_FILES = "trash_bin_retention_num_of_files" @@ -93,5 +94,8 @@ class PreferencesConstants { const val DEFAULT_LEAST_USED_APPS_DAYS = 7 const val DEFAULT_NEWLY_INSTALLED_APPS_DAYS = 7 const val DEFAULT_RECENTLY_UPDATED_APPS_DAYS = 7 + const val DEFAULT_LARGE_SIZE_DIFF_APPS_DAYS = 7 + + const val MAX_LARGE_SIZE_DIFF_APPS_DAYS = 180 } } diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/Utils.kt b/app/src/main/java/com/amaze/fileutilities/utilis/Utils.kt index ce62576c..8f1dfb0f 100644 --- a/app/src/main/java/com/amaze/fileutilities/utilis/Utils.kt +++ b/app/src/main/java/com/amaze/fileutilities/utilis/Utils.kt @@ -1239,12 +1239,17 @@ class Utils { summary: String, days: Long, callback: (Long?) -> Unit, - neutralCallback: () -> Unit + max: Long? = null, + neutralCallback: (() -> Unit)? = null ) { val inputEditTextViewPair = getEditTextViewForDialog(context, "$days") inputEditTextViewPair.second.inputType = InputType.TYPE_CLASS_NUMBER inputEditTextViewPair.second.setText("$days") - val dialog = AlertDialog.Builder(context, R.style.Custom_Dialog_Dark) + if (max != null) { + inputEditTextViewPair.second.filters = arrayOf(InputFilterMinMaxLong(1, max)) + } + + val dialogBuilder = AlertDialog.Builder(context, R.style.Custom_Dialog_Dark) .setTitle(title) .setMessage(summary) .setView(inputEditTextViewPair.first) @@ -1258,10 +1263,14 @@ class Utils { R.string.cancel ) { dialog, _ -> dialog.dismiss() - }.setNeutralButton(R.string.default_alert_dialog) { dialog, _ -> + } + if (neutralCallback != null) { + dialogBuilder.setNeutralButton(R.string.default_alert_dialog) { dialog, _ -> neutralCallback.invoke() dialog.dismiss() - }.create() + } + } + val dialog = dialogBuilder.create() dialog.show() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7edcdf5e..92f6e89e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -934,6 +934,10 @@ Recently updated apps Apps recently updated (in days) + + Growing apps + + Apps with large size increase in the last number of days Memory (RAM) Usage diff --git a/app/src/main/res/xml/analysis_prefs.xml b/app/src/main/res/xml/analysis_prefs.xml index e335ab51..ed7a86c0 100644 --- a/app/src/main/res/xml/analysis_prefs.xml +++ b/app/src/main/res/xml/analysis_prefs.xml @@ -30,6 +30,11 @@ app:title="@string/recently_updated_apps" android:summary="@string/recently_updated_apps_summary" /> + Date: Sun, 17 Dec 2023 19:56:52 +0100 Subject: [PATCH 14/29] add androidx.work dependency --- app/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index b61da4d4..bd33daf6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,6 +6,7 @@ buildscript { fragment_version = "1.3.6" roomVersion = "2.4.0" billing_version = "6.0.1" + work_version = "2.8.0" BASE_CLOUD_FUNC = "https://us-central1-useful-cathode-91310.cloudfunctions.net" BASE_API_STICKER_PACK = "https://us-central1-useful-cathode-91310.cloudfunctions.net/amaze-utils-sticker-pack/" API_REQ_TRIAL_URI = "/amaze-utils-fdroid-trial-validator" @@ -294,6 +295,7 @@ dependencies { exclude group: 'androidx.core', module: 'core' exclude group: 'androidx.core', module: 'core-ktx' } + implementation "androidx.work:work-runtime-ktx:$work_version" //Detect memory leaks // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' From cba89c89d0e7610fe267c74abc08a891b3448ae6 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 17 Dec 2023 19:58:31 +0100 Subject: [PATCH 15/29] update androidx.room version for compatibility with androidx.work --- app/build.gradle | 2 +- .../amaze/fileutilities/home_page/database/AppStorageStats.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bd33daf6..b069e067 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,7 @@ buildscript { androidXLifecycleVersion = "2.3.1" glideVersion = '4.11.0' fragment_version = "1.3.6" - roomVersion = "2.4.0" + roomVersion = "2.5.2" billing_version = "6.0.1" work_version = "2.8.0" BASE_CLOUD_FUNC = "https://us-central1-useful-cathode-91310.cloudfunctions.net" diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt index a7711343..209f1fed 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt @@ -24,8 +24,8 @@ import androidx.annotation.Keep import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey -import androidx.room.ForeignKey.CASCADE -import androidx.room.ForeignKey.NO_ACTION +import androidx.room.ForeignKey.Companion.CASCADE +import androidx.room.ForeignKey.Companion.NO_ACTION import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey From 26b7276a0264ab6326fab537ee5e8970b80c5f44 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 17 Dec 2023 20:00:04 +0100 Subject: [PATCH 16/29] fix foreign key constraint issue The InstalledApps were inserted with OnConflictStrategy.REPLACE which would delete the entry and replace it. However, this would also delete the associated AppStorageStats entries so now insert ignores conflicts and there is a new `updateOrInsert` function which updates the entry or inserts it if it is not yet in the table --- .../home_page/database/AppDatabase.kt | 2 +- .../home_page/database/AppStorageStats.kt | 3 +-- .../home_page/database/InstalledAppsDao.kt | 20 +++++++++++++++++-- .../home_page/ui/files/FilesViewModel.kt | 2 +- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt index a13f5d62..72f487e4 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt @@ -136,7 +136,7 @@ abstract class AppDatabase : RoomDatabase() { "`timestamp` INTEGER NOT NULL, " + "`package_size` INTEGER NOT NULL," + "FOREIGN KEY(`package_id`) REFERENCES `InstalledApps`(`_id`) ON UPDATE " + - "CASCADE ON DELETE NO ACTION)" + "CASCADE ON DELETE CASCADE)" ) database.execSQL( "CREATE INDEX IF NOT EXISTS `index_AppStorageStats_timestamp_package_id`" + diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt index 209f1fed..a4888062 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt @@ -25,7 +25,6 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.ForeignKey.Companion.CASCADE -import androidx.room.ForeignKey.Companion.NO_ACTION import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey @@ -41,7 +40,7 @@ import java.util.Date entity = InstalledApps::class, parentColumns = ["_id"], childColumns = ["package_id"], - onDelete = NO_ACTION, + onDelete = CASCADE, onUpdate = CASCADE ) ] diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/InstalledAppsDao.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/InstalledAppsDao.kt index 1294d580..d22fda71 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/InstalledAppsDao.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/InstalledAppsDao.kt @@ -24,6 +24,8 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update @Dao interface InstalledAppsDao { @@ -34,8 +36,22 @@ interface InstalledAppsDao { @Query("SELECT * FROM installedapps") fun findAll(): List - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(installedAppsList: List) + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(installedApp: InstalledApps): Long + + @Update + fun update(installedApp: InstalledApps) + + @Transaction + fun updateOrInsert(installedAppsList: List) { + for (installedApp in installedAppsList) { + val rowId = insert(installedApp) + if (rowId == -1L) { + // there is already an entry so we want to update + update(installedApp) + } + } + } @Query("DELETE FROM installedapps") fun deleteAll() diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt index ebfd40b0..0249679b 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt @@ -2387,7 +2387,7 @@ class FilesViewModel(val applicationContext: Application) : InstalledApps(it.first.packageName, listOf(it.first.sourceDir, it.first.dataDir)) } val dao = AppDatabase.getInstance(applicationContext).installedAppsDao() - dao.insert(installedApps) + dao.updateOrInsert(installedApps) } } From 9e7cdd26aa483929aaffd7345634c4f6791c3ee7 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 17 Dec 2023 20:09:32 +0100 Subject: [PATCH 17/29] register a Worker to store the size of every app once a day --- .../fileutilities/home_page/MainActivity.kt | 14 ++++ .../utilis/QueryAppSizeWorker.kt | 73 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 app/src/main/java/com/amaze/fileutilities/utilis/QueryAppSizeWorker.kt diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/MainActivity.kt b/app/src/main/java/com/amaze/fileutilities/home_page/MainActivity.kt index c604bf16..e13009db 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/MainActivity.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/MainActivity.kt @@ -38,6 +38,9 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupWithNavController +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager import com.amaze.fileutilities.BuildConfig import com.amaze.fileutilities.R import com.amaze.fileutilities.WifiP2PActivity @@ -56,6 +59,7 @@ import com.amaze.fileutilities.home_page.ui.settings.PreferenceActivity import com.amaze.fileutilities.home_page.ui.transfer.TransferFragment import com.amaze.fileutilities.utilis.ItemsActionBarFragment import com.amaze.fileutilities.utilis.PreferencesConstants +import com.amaze.fileutilities.utilis.QueryAppSizeWorker import com.amaze.fileutilities.utilis.UpdateChecker import com.amaze.fileutilities.utilis.Utils import com.amaze.fileutilities.utilis.getAppCommonSharedPreferences @@ -70,6 +74,7 @@ import com.stephentuso.welcome.WelcomeHelper import org.slf4j.Logger import org.slf4j.LoggerFactory import java.util.Date +import java.util.concurrent.TimeUnit class MainActivity : WifiP2PActivity(), @@ -250,6 +255,15 @@ class MainActivity : .edit().putLong(PreferencesConstants.KEY_INSTALL_DATE, Date().time) .apply() } + + // schedule PeriodicWorkRequest to store the size of each app in the database every day + val periodicWorkRequest = PeriodicWorkRequestBuilder( + 24, + TimeUnit.HOURS + ).build() + WorkManager.getInstance(this).enqueueUniquePeriodicWork( + QueryAppSizeWorker.NAME, ExistingPeriodicWorkPolicy.KEEP, periodicWorkRequest + ) } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/QueryAppSizeWorker.kt b/app/src/main/java/com/amaze/fileutilities/utilis/QueryAppSizeWorker.kt new file mode 100644 index 00000000..ff68090f --- /dev/null +++ b/app/src/main/java/com/amaze/fileutilities/utilis/QueryAppSizeWorker.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.fileutilities.utilis + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.amaze.fileutilities.home_page.database.AppDatabase +import java.time.ZonedDateTime +import java.util.Date + +/** + * A [CoroutineWorker] to insert the size of each app into the database and + * delete entries that are older than [PreferencesConstants.MAX_LARGE_SIZE_DIFF_APPS_DAYS] days. + */ +class QueryAppSizeWorker( + context: Context, + workerParameters: WorkerParameters +) : CoroutineWorker(context, workerParameters) { + override suspend fun doWork(): Result { + val appStorageStatsDao = AppDatabase.getInstance(applicationContext).appStorageStatsDao() + val packageManager = applicationContext.packageManager + // Get all currently installed apps + val allApps = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getInstalledApplications( + PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()) + ) + } else { + packageManager.getInstalledApplications(PackageManager.GET_META_DATA) + } + // Find the current size of each app and store them in the database + for (appInfo in allApps) { + val currentSize = Utils.findApplicationInfoSize(applicationContext, appInfo) + val timestamp = Date.from(ZonedDateTime.now().toInstant()) + appStorageStatsDao.insert(appInfo.packageName, timestamp, currentSize) + } + + // Delete all AppStorageStats entries that are older than MAX_LARGE_SIZE_DIFF_APPS_DAYS + val minDate = Date.from( + ZonedDateTime + .now() + .minusDays(PreferencesConstants.MAX_LARGE_SIZE_DIFF_APPS_DAYS.toLong()) + .toInstant() + ) + appStorageStatsDao.deleteOlderThan(minDate) + + return Result.success() + } + + companion object { + const val NAME: String = "query_app_size_worker" + } +} From 9b03836e871bd1414d90676fbc48a0c733d1202e Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 17 Dec 2023 20:11:44 +0100 Subject: [PATCH 18/29] change MediaFileInfo to store a size diff in ApkMetaData --- .../home_page/ui/files/MediaFileInfo.kt | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/MediaFileInfo.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/MediaFileInfo.kt index 6ff50f9c..e662a14f 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/MediaFileInfo.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/MediaFileInfo.kt @@ -147,22 +147,10 @@ data class MediaFileInfo( } fun fromApplicationInfo( - context: Context, - applicationInfo: ApplicationInfo, - packageInfo: PackageInfo? - ): MediaFileInfo? { - if (applicationInfo.sourceDir == null) { - return null - } - val size = Utils.findApplicationInfoSize(context, applicationInfo) - return fromApplicationInfoWithSize(context, applicationInfo, packageInfo, size) - } - - fun fromApplicationInfoWithSize( context: Context, applicationInfo: ApplicationInfo, packageInfo: PackageInfo?, - size: Long + sizeDiff: Long = -1 ): MediaFileInfo? { if (applicationInfo.sourceDir == null) { return null @@ -175,7 +163,7 @@ data class MediaFileInfo( applicationInfo.loadLabel(packageManager) as String, applicationInfo.sourceDir, apkFile.lastModified(), - size, + Utils.findApplicationInfoSize(context, applicationInfo), false ) val extraInfo = ExtraInfo( @@ -184,7 +172,8 @@ data class MediaFileInfo( ApkMetaData( applicationInfo.packageName, packageManager.getApplicationIcon(applicationInfo.packageName), - Utils.getApplicationNetworkBytes(context, applicationInfo) + Utils.getApplicationNetworkBytes(context, applicationInfo), + sizeDiff ) ) mediaFileInfo.extraInfo = extraInfo @@ -419,7 +408,8 @@ data class MediaFileInfo( data class ApkMetaData( val packageName: String, val drawable: Drawable?, - val networkBytes: Long + val networkBytes: Long, + val sizeDiff: Long = -1 ) data class ExtraMetaData(val checksum: String) data class Playlist(var id: Long, var name: String) From 5360618c5ea4e0d0d87b8ecbe7be528487b385aa Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 17 Dec 2023 20:13:44 +0100 Subject: [PATCH 19/29] implement calcuation of size diff of apps --- .../home_page/ui/files/FilesViewModel.kt | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt index 0249679b..23f22dd6 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt @@ -109,6 +109,7 @@ import java.nio.charset.Charset import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId +import java.time.ZonedDateTime import java.util.Calendar import java.util.Collections import java.util.Date @@ -140,6 +141,7 @@ class FilesViewModel(val applicationContext: Application) : var largeAppsLiveData: MutableLiveData?>? = null var newlyInstalledAppsLiveData: MutableLiveData?>? = null var recentlyUpdatedAppsLiveData: MutableLiveData?>? = null + var largeSizeDiffAppsLiveData: MutableLiveData?>? = null var junkFilesLiveData: MutableLiveData, String>?>? = null var apksLiveData: MutableLiveData?>? = null var hiddenFilesLiveData: MutableLiveData?>? = null @@ -1944,6 +1946,63 @@ class FilesViewModel(val applicationContext: Application) : } } + fun getLargeSizeDiffApps(): LiveData?> { + if (largeSizeDiffAppsLiveData == null) { + largeSizeDiffAppsLiveData = MutableLiveData() + largeSizeDiffAppsLiveData?.value = null + processLargeSizeDiffApps(applicationContext.packageManager) + } + return largeSizeDiffAppsLiveData!! + } + + private fun processLargeSizeDiffApps(packageManager: PackageManager) { + viewModelScope.launch(Dispatchers.IO) { + loadAllInstalledApps(packageManager) + val sharedPrefs = applicationContext.getAppCommonSharedPreferences() + val days = sharedPrefs.getInt( + PreferencesConstants.KEY_LARGE_SIZE_DIFF_APPS_DAYS, + PreferencesConstants.DEFAULT_LARGE_SIZE_DIFF_APPS_DAYS + ) + val pastDate = LocalDateTime.now().minusDays(days.toLong()).toLocalDate() + val periodStart = Date.from(pastDate.atStartOfDay(ZoneId.systemDefault()).toInstant()) + val periodEnd = Date.from(ZonedDateTime.now().toInstant()) + + val dao = AppDatabase.getInstance(applicationContext).storageStatsPerAppDao() + + val priorityQueue = FixedSizePriorityQueue(50) { o1, o2 -> + val diff1 = o1.extraInfo?.apkMetaData?.sizeDiff ?: 0 + val diff2 = o2.extraInfo?.apkMetaData?.sizeDiff ?: 0 + diff1.compareTo(diff2) + } + + allApps.get()?.forEach { (applicationInfo, packageInfo) -> + val storageStatToAppName = dao.findOldestWithinPeriod( + applicationInfo.packageName, + periodStart, + periodEnd + ) + if (storageStatToAppName != null) { + val currentPackageSize = + Utils.findApplicationInfoSize(applicationContext, applicationInfo) + val sizeDiff = currentPackageSize - storageStatToAppName.packageSize + if (sizeDiff > 0) { + MediaFileInfo + .fromApplicationInfo( + applicationContext, + applicationInfo, + packageInfo, + sizeDiff + ) + ?.let { priorityQueue.add(it) } + } + } + } + + val result = priorityQueue.reversed() + largeSizeDiffAppsLiveData?.postValue(result.toMutableList()) + } + } + fun getJunkFilesLiveData(): LiveData, String>?> { if (junkFilesLiveData == null) { junkFilesLiveData = MutableLiveData() From 7e896cf30b63827b7eb32d4e53481a05960fef23 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 17 Dec 2023 20:15:53 +0100 Subject: [PATCH 20/29] add analysis preview of growing apps in `AnalyseFragment` --- .../home_page/ui/analyse/AnalyseFragment.kt | 19 +++++++++++++++++++ app/src/main/res/layout/fragment_analyse.xml | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseFragment.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseFragment.kt index 6cc9c5ee..3166cb9d 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseFragment.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseFragment.kt @@ -613,6 +613,18 @@ class AnalyseFragment : AbstractMediaFileInfoOperationsFragment() { } } } + filesViewModel.getLargeSizeDiffApps() + .observe(viewLifecycleOwner) { mediaFileInfoList -> + largeSizeDiffAppsPreview.invalidateProgress(true, null) + mediaFileInfoList?.let { + largeSizeDiffAppsPreview.invalidateProgress(false, null) + largeSizeDiffAppsPreview.loadPreviews(mediaFileInfoList) { + cleanButtonClick(it, true) { + filesViewModel.largeSizeDiffAppsLiveData = null + } + } + } + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { gamesPreview.visibility = View.VISIBLE @@ -1022,6 +1034,13 @@ class AnalyseFragment : AbstractMediaFileInfoOperationsFragment() { this@AnalyseFragment ) } + largeSizeDiffAppsPreview.setOnClickListener { + shouldCallbackAppUninstall = false + ReviewImagesFragment.newInstance( + ReviewImagesFragment.TYPE_LARGE_SIZE_DIFF_APPS, + this@AnalyseFragment + ) + } allApksPreview.setOnClickListener { shouldCallbackAppUninstall = false ReviewImagesFragment.newInstance( diff --git a/app/src/main/res/layout/fragment_analyse.xml b/app/src/main/res/layout/fragment_analyse.xml index 2d57b220..b9a08497 100644 --- a/app/src/main/res/layout/fragment_analyse.xml +++ b/app/src/main/res/layout/fragment_analyse.xml @@ -305,6 +305,14 @@ app:showPreview="true" android:layout_marginTop="@dimen/material_generic" /> + Date: Sun, 17 Dec 2023 20:16:45 +0100 Subject: [PATCH 21/29] add analysis review of growing apps --- .../home_page/ui/analyse/ReviewAnalysisAdapter.kt | 14 ++++++++++++++ .../home_page/ui/analyse/ReviewImagesFragment.kt | 12 +++++++++++- app/src/main/res/values/strings.xml | 4 ++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewAnalysisAdapter.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewAnalysisAdapter.kt index c6dfad97..b9d129f2 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewAnalysisAdapter.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewAnalysisAdapter.kt @@ -83,6 +83,20 @@ class ReviewAnalysisAdapter( } else { holder.infoSummary.text = this.mediaFileInfo?.getFormattedSize(context) } + } else if (analysisType == ReviewImagesFragment.TYPE_LARGE_SIZE_DIFF_APPS) { + val sizeDiff = this.mediaFileInfo?.extraInfo?.apkMetaData?.sizeDiff + val totalSize = this.mediaFileInfo?.getFormattedSize(context) + if (sizeDiff != null) { + val summary = + "${context.getString(R.string.size_diff)}: ${FileUtils + .formatStorageLength(context, sizeDiff) + }" + val extra = "${context.getString(R.string.size_total)}: $totalSize" + holder.infoSummary.text = summary + holder.extraInfo.text = extra + } else { + holder.infoSummary.text = totalSize + } } else { holder.infoSummary.text = this.mediaFileInfo?.getFormattedSize(context) } diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewImagesFragment.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewImagesFragment.kt index 0e6b6743..d00e6820 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewImagesFragment.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewImagesFragment.kt @@ -113,6 +113,7 @@ class ReviewImagesFragment : ItemsActionBarFragment() { const val TYPE_SIMILAR_IMAGES = 30 const val TYPE_HIDDEN_FILES = 31 const val TYPE_TRASH_BIN = 32 + const val TYPE_LARGE_SIZE_DIFF_APPS = 33 fun newInstance(type: Int, fragment: Fragment) { val analyseFragment = ReviewImagesFragment() @@ -191,7 +192,7 @@ class ReviewImagesFragment : ItemsActionBarFragment() { } TYPE_LARGE_APPS, TYPE_UNUSED_APPS, TYPE_MOST_USED_APPS, TYPE_LEAST_USED_APPS, TYPE_GAMES_INSTALLED, TYPE_APK_FILES, TYPE_NEWLY_INSTALLED_APPS, - TYPE_RECENTLY_UPDATED_APPS, TYPE_NETWORK_INTENSIVE_APPS -> { + TYPE_RECENTLY_UPDATED_APPS, TYPE_NETWORK_INTENSIVE_APPS, TYPE_LARGE_SIZE_DIFF_APPS -> { MediaFileAdapter.MEDIA_TYPE_APKS } TYPE_TRASH_BIN -> { @@ -636,6 +637,15 @@ class ReviewImagesFragment : ItemsActionBarFragment() { } } } + TYPE_LARGE_SIZE_DIFF_APPS -> { + filesViewModel.getLargeSizeDiffApps().observe(viewLifecycleOwner) { largeDiffApps -> + invalidateProcessing(true, false) + largeDiffApps?.let { + setMediaInfoList(it, false) + invalidateProcessing(false, false) + } + } + } TYPE_GAMES_INSTALLED -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { filesViewModel.getGamesInstalled() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 92f6e89e..0b8ee717 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -938,6 +938,10 @@ Growing apps Apps with large size increase in the last number of days + + Change + + Total Memory (RAM) Usage From 332d9ef284a88f8d14075a2e3c614a7f03eb65d0 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 17 Dec 2023 20:18:12 +0100 Subject: [PATCH 22/29] add new database schema --- .../5.json | 710 ++++++++++++++++++ 1 file changed, 710 insertions(+) create mode 100644 app/schemas/com.amaze.fileutilities.home_page.database.AppDatabase/5.json diff --git a/app/schemas/com.amaze.fileutilities.home_page.database.AppDatabase/5.json b/app/schemas/com.amaze.fileutilities.home_page.database.AppDatabase/5.json new file mode 100644 index 00000000..a84d4ca2 --- /dev/null +++ b/app/schemas/com.amaze.fileutilities.home_page.database.AppDatabase/5.json @@ -0,0 +1,710 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "09c5ff70859c472f6f1cb4ea43b43003", + "entities": [ + { + "tableName": "ImageAnalysis", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `file_path` TEXT NOT NULL, `is_sad` INTEGER NOT NULL, `is_distracted` INTEGER NOT NULL, `is_sleeping` INTEGER NOT NULL, `face_count` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSad", + "columnName": "is_sad", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDistracted", + "columnName": "is_distracted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSleeping", + "columnName": "is_sleeping", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "faceCount", + "columnName": "face_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_ImageAnalysis_file_path", + "unique": true, + "columnNames": [ + "file_path" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ImageAnalysis_file_path` ON `${TABLE_NAME}` (`file_path`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InternalStorageAnalysis", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `sha256_checksum` TEXT NOT NULL, `files_path` TEXT NOT NULL, `is_empty` INTEGER NOT NULL, `is_junk` INTEGER NOT NULL, `is_directory` INTEGER NOT NULL, `is_mediastore` INTEGER NOT NULL, `depth` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "checksum", + "columnName": "sha256_checksum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "files", + "columnName": "files_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isEmpty", + "columnName": "is_empty", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isJunk", + "columnName": "is_junk", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMediaStore", + "columnName": "is_mediastore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "depth", + "columnName": "depth", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_InternalStorageAnalysis_sha256_checksum", + "unique": true, + "columnNames": [ + "sha256_checksum" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_InternalStorageAnalysis_sha256_checksum` ON `${TABLE_NAME}` (`sha256_checksum`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PathPreferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `path` TEXT NOT NULL, `feature` INTEGER NOT NULL, `excludes` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "feature", + "columnName": "feature", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excludes", + "columnName": "excludes", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_PathPreferences_path_feature", + "unique": true, + "columnNames": [ + "path", + "feature" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_PathPreferences_path_feature` ON `${TABLE_NAME}` (`path`, `feature`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "BlurAnalysis", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `file_path` TEXT NOT NULL, `is_blur` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isBlur", + "columnName": "is_blur", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_BlurAnalysis_file_path", + "unique": true, + "columnNames": [ + "file_path" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_BlurAnalysis_file_path` ON `${TABLE_NAME}` (`file_path`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "LowLightAnalysis", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `file_path` TEXT NOT NULL, `is_low_light` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isLowLight", + "columnName": "is_low_light", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_LowLightAnalysis_file_path", + "unique": true, + "columnNames": [ + "file_path" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LowLightAnalysis_file_path` ON `${TABLE_NAME}` (`file_path`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "MemeAnalysis", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `file_path` TEXT NOT NULL, `is_meme` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isMeme", + "columnName": "is_meme", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_MemeAnalysis_file_path", + "unique": true, + "columnNames": [ + "file_path" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_MemeAnalysis_file_path` ON `${TABLE_NAME}` (`file_path`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "VideoPlayerState", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `file_path` TEXT NOT NULL, `playback_position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playbackPosition", + "columnName": "playback_position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_VideoPlayerState_file_path", + "unique": true, + "columnNames": [ + "file_path" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_VideoPlayerState_file_path` ON `${TABLE_NAME}` (`file_path`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Trial", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `device_id` TEXT NOT NULL, `trial_status` TEXT NOT NULL, `trial_days_left` INTEGER NOT NULL, `fetch_time` INTEGER NOT NULL, `subscription_status` INTEGER NOT NULL, `purchase_token` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trialStatus", + "columnName": "trial_status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trialDaysLeft", + "columnName": "trial_days_left", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fetchTime", + "columnName": "fetch_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscriptionStatus", + "columnName": "subscription_status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "purchaseToken", + "columnName": "purchase_token", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_Trial_device_id", + "unique": true, + "columnNames": [ + "device_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Trial_device_id` ON `${TABLE_NAME}` (`device_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Lyrics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `file_path` TEXT NOT NULL, `lyrics_text` TEXT NOT NULL, `is_synced` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lyricsText", + "columnName": "lyrics_text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSynced", + "columnName": "is_synced", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_Lyrics_file_path", + "unique": true, + "columnNames": [ + "file_path" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Lyrics_file_path` ON `${TABLE_NAME}` (`file_path`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstalledApps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `package_name` TEXT NOT NULL, `data_dirs` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dataDirs", + "columnName": "data_dirs", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_InstalledApps_package_name", + "unique": true, + "columnNames": [ + "package_name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_InstalledApps_package_name` ON `${TABLE_NAME}` (`package_name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "SimilarImagesAnalysis", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `histogram_checksum` TEXT NOT NULL, `files_path` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "histogram_checksum", + "columnName": "histogram_checksum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "files", + "columnName": "files_path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_SimilarImagesAnalysis_histogram_checksum", + "unique": true, + "columnNames": [ + "histogram_checksum" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SimilarImagesAnalysis_histogram_checksum` ON `${TABLE_NAME}` (`histogram_checksum`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "SimilarImagesAnalysisMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `parent_path` TEXT NOT NULL, `file_path` TEXT NOT NULL, `blue_channel` TEXT NOT NULL, `green_channel` TEXT NOT NULL, `red_channel` TEXT NOT NULL, `datapoints` INTEGER NOT NULL, `threshold` INTEGER NOT NULL, `is_analysed` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentPath", + "columnName": "parent_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blueChannel", + "columnName": "blue_channel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "greenChannel", + "columnName": "green_channel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "redChannel", + "columnName": "red_channel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "datapoints", + "columnName": "datapoints", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "threshold", + "columnName": "threshold", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAnalysed", + "columnName": "is_analysed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_SimilarImagesAnalysisMetadata_file_path_parent_path", + "unique": true, + "columnNames": [ + "file_path", + "parent_path" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SimilarImagesAnalysisMetadata_file_path_parent_path` ON `${TABLE_NAME}` (`file_path`, `parent_path`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "AppStorageStats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `package_id` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `package_size` INTEGER NOT NULL, FOREIGN KEY(`package_id`) REFERENCES `InstalledApps`(`_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageId", + "columnName": "package_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageSize", + "columnName": "package_size", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_AppStorageStats_timestamp_package_id", + "unique": false, + "columnNames": [ + "timestamp", + "package_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AppStorageStats_timestamp_package_id` ON `${TABLE_NAME}` (`timestamp`, `package_id`)" + }, + { + "name": "index_AppStorageStats_package_id", + "unique": false, + "columnNames": [ + "package_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AppStorageStats_package_id` ON `${TABLE_NAME}` (`package_id`)" + } + ], + "foreignKeys": [ + { + "table": "InstalledApps", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "package_id" + ], + "referencedColumns": [ + "_id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '09c5ff70859c472f6f1cb4ea43b43003')" + ] + } +} \ No newline at end of file From 8fd70d7f3a3e87252f80912b501b232e2f3bdc78 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 17 Dec 2023 20:29:21 +0100 Subject: [PATCH 23/29] add comments --- .../home_page/ui/analyse/ReviewAnalysisAdapter.kt | 2 ++ .../fileutilities/home_page/ui/files/FilesViewModel.kt | 7 +++++++ .../com/amaze/fileutilities/utilis/PreferencesConstants.kt | 2 ++ 3 files changed, 11 insertions(+) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewAnalysisAdapter.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewAnalysisAdapter.kt index b9d129f2..b2e8de4e 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewAnalysisAdapter.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/ReviewAnalysisAdapter.kt @@ -92,7 +92,9 @@ class ReviewAnalysisAdapter( .formatStorageLength(context, sizeDiff) }" val extra = "${context.getString(R.string.size_total)}: $totalSize" + // Show the size increase in `infoSummary` holder.infoSummary.text = summary + // Show total size in `extraInfo` holder.extraInfo.text = extra } else { holder.infoSummary.text = totalSize diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt index 23f22dd6..38baf3b2 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt @@ -1959,12 +1959,16 @@ class FilesViewModel(val applicationContext: Application) : viewModelScope.launch(Dispatchers.IO) { loadAllInstalledApps(packageManager) val sharedPrefs = applicationContext.getAppCommonSharedPreferences() + // Get the number of days which the analysis should consider val days = sharedPrefs.getInt( PreferencesConstants.KEY_LARGE_SIZE_DIFF_APPS_DAYS, PreferencesConstants.DEFAULT_LARGE_SIZE_DIFF_APPS_DAYS ) val pastDate = LocalDateTime.now().minusDays(days.toLong()).toLocalDate() + // The start of the last number of days as specified in the preferences + // It is the start of the search period val periodStart = Date.from(pastDate.atStartOfDay(ZoneId.systemDefault()).toInstant()) + // The end of the search period is now val periodEnd = Date.from(ZonedDateTime.now().toInstant()) val dao = AppDatabase.getInstance(applicationContext).storageStatsPerAppDao() @@ -1976,16 +1980,19 @@ class FilesViewModel(val applicationContext: Application) : } allApps.get()?.forEach { (applicationInfo, packageInfo) -> + // Find the oldest entry for the app within the last number of days val storageStatToAppName = dao.findOldestWithinPeriod( applicationInfo.packageName, periodStart, periodEnd ) if (storageStatToAppName != null) { + // Calculate the size difference compared to the app size now val currentPackageSize = Utils.findApplicationInfoSize(applicationContext, applicationInfo) val sizeDiff = currentPackageSize - storageStatToAppName.packageSize if (sizeDiff > 0) { + // If the app size grew, add it to the priority queue MediaFileInfo .fromApplicationInfo( applicationContext, diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/PreferencesConstants.kt b/app/src/main/java/com/amaze/fileutilities/utilis/PreferencesConstants.kt index acc500ce..33386348 100644 --- a/app/src/main/java/com/amaze/fileutilities/utilis/PreferencesConstants.kt +++ b/app/src/main/java/com/amaze/fileutilities/utilis/PreferencesConstants.kt @@ -96,6 +96,8 @@ class PreferencesConstants { const val DEFAULT_RECENTLY_UPDATED_APPS_DAYS = 7 const val DEFAULT_LARGE_SIZE_DIFF_APPS_DAYS = 7 + // max days that the size is stored in database and + // therefore the max days that the size diff can be calculated const val MAX_LARGE_SIZE_DIFF_APPS_DAYS = 180 } } From 263c6089a47f83e00536c51828e7654afa90b498 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 17 Dec 2023 22:11:42 +0100 Subject: [PATCH 24/29] add dependencies for room database tests --- app/build.gradle | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b069e067..dd9558b9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -131,6 +131,11 @@ android { } } + sourceSets { + // Adds exported schema location as test app assets. + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + } + manifestPlaceholders = [ app_display_name:"@string/app_name_launcher"] } @@ -300,9 +305,12 @@ dependencies { //Detect memory leaks // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' - testImplementation 'junit:junit:4.+' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + // For tests + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test:rules:1.5.0' + androidTestImplementation "androidx.room:room-testing:$roomVersion" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' } From a4178b48367b197d844eb26d4f97b8db285f10e6 Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 17 Dec 2023 22:12:18 +0100 Subject: [PATCH 25/29] add test for database migrations --- .../database/DatabaseMigrationTest.kt | 67 +++++++++++++++++++ .../home_page/database/AppDatabase.kt | 8 +-- 2 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 app/src/androidTest/java/com/amaze/fileutilities/home_page/database/DatabaseMigrationTest.kt diff --git a/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/DatabaseMigrationTest.kt b/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/DatabaseMigrationTest.kt new file mode 100644 index 00000000..a9389833 --- /dev/null +++ b/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/DatabaseMigrationTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.fileutilities.home_page.database + +import androidx.room.Room +import androidx.room.testing.MigrationTestHelper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.amaze.fileutilities.home_page.database.AppDatabase.Companion.MIGRATION_4_5 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class DatabaseMigrationTest { + private val TEST_DB = "migration-test" + + // Array of all migrations. + private val ALL_MIGRATIONS = arrayOf( + MIGRATION_4_5 + ) + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java + ) + + @Test + @Throws(IOException::class) + fun migrateAllAvailable() { + // Create earliest version of the database + // We only have schema starting from version 4 + helper.createDatabase(TEST_DB, 4).apply { + close() + } + + // Open latest version of the database. Room validates the schema + // once all migrations execute. + Room.databaseBuilder( + InstrumentationRegistry.getInstrumentation().targetContext, + AppDatabase::class.java, + TEST_DB + ).addMigrations(*ALL_MIGRATIONS).build().apply { + openHelper.writableDatabase.close() + } + } +} diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt index 72f487e4..b317b12c 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt @@ -71,7 +71,7 @@ abstract class AppDatabase : RoomDatabase() { return appDatabase!! } - private val MIGRATION_1_2 = object : Migration(1, 2) { + val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( "CREATE TABLE IF NOT EXISTS `Lyrics` (`_id` INTEGER " + @@ -85,7 +85,7 @@ abstract class AppDatabase : RoomDatabase() { } } - private val MIGRATION_2_3 = object : Migration(2, 3) { + val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( "CREATE TABLE IF NOT EXISTS `InstalledApps` (`_id` INTEGER " + @@ -99,7 +99,7 @@ abstract class AppDatabase : RoomDatabase() { } } - private val MIGRATION_3_4 = object : Migration(3, 4) { + val MIGRATION_3_4 = object : Migration(3, 4) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( "CREATE TABLE IF NOT EXISTS `SimilarImagesAnalysis` " + @@ -127,7 +127,7 @@ abstract class AppDatabase : RoomDatabase() { } } - private val MIGRATION_4_5 = object : Migration(4, 5) { + val MIGRATION_4_5 = object : Migration(4, 5) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( "CREATE TABLE IF NOT EXISTS `AppStorageStats` " + From 6b5f45247b133692d92470d67a9bbfc680f9c38d Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Sun, 17 Dec 2023 22:12:30 +0100 Subject: [PATCH 26/29] add tests for new daos --- .../database/AppStorageStatsDaoTest.kt | 106 +++++++++++++ .../database/StorageStatToAppNameDaoTest.kt | 150 ++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 app/src/androidTest/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDaoTest.kt create mode 100644 app/src/androidTest/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDaoTest.kt diff --git a/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDaoTest.kt b/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDaoTest.kt new file mode 100644 index 00000000..36997d68 --- /dev/null +++ b/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDaoTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.fileutilities.home_page.database + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException +import java.util.Date + +@RunWith(AndroidJUnit4::class) +class AppStorageStatsDaoTest { + private lateinit var db: AppDatabase + private lateinit var appStorageStatsDao: AppStorageStatsDao + private lateinit var installedAppsDao: InstalledAppsDao + private lateinit var appEntry: InstalledApps + + private val appName = "testApp" + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, AppDatabase::class.java + ).build() + appStorageStatsDao = db.appStorageStatsDao() + installedAppsDao = db.installedAppsDao() + installedAppsDao.insert(InstalledApps(appName, listOf())) + val entry = installedAppsDao.findByPackageName(appName) + Assert.assertNotNull(entry) + appEntry = entry!! + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + @Test + @Throws(IOException::class) + fun insertTest() { + val date = Date() + val size = 1234L + + appStorageStatsDao.insert(appName, date, size) + val allAppStorageStats = appStorageStatsDao.findAll() + Assert.assertNotNull( + "Did not contain correct entry with packageId ${appEntry.uid}, timestamp $date " + + "and packageSize $size: $allAppStorageStats", + allAppStorageStats.find { + it.packageSize == size && it.timestamp == date && it.packageId == appEntry.uid + } + ) + } + + @Test + @Throws(IOException::class) + fun deleteOlderThanDateTest() { + val minDate = Date(1000) + val size = 1234L + + val earlierDate = Date(100) + val laterDate = Date(5000) + appStorageStatsDao.insert(appName, earlierDate, size) + appStorageStatsDao.insert(appName, laterDate, size) + + appStorageStatsDao.deleteOlderThan(minDate) + val allStorageStats = appStorageStatsDao.findAll() + Assert.assertEquals( + "Database should only contain one AppStorageStats entry " + + "but was $allStorageStats", + 1, + allStorageStats.size + ) + Assert.assertNotNull( + "Database did not contain expected entry with timestamp $laterDate " + + "but was $allStorageStats", + allStorageStats.find { it.timestamp == laterDate && appEntry.uid == it.packageId } + ) + } +} diff --git a/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDaoTest.kt b/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDaoTest.kt new file mode 100644 index 00000000..7c233462 --- /dev/null +++ b/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDaoTest.kt @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Utilities. + * + * Amaze File Utilities is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.fileutilities.home_page.database + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException +import java.util.Date + +@RunWith(AndroidJUnit4::class) +class StorageStatToAppNameDaoTest { + private lateinit var db: AppDatabase + private lateinit var storageStatToAppNameDao: StorageStatToAppNameDao + private lateinit var appStorageStatsDao: AppStorageStatsDao + private lateinit var installedAppsDao: InstalledAppsDao + private lateinit var appEntry: InstalledApps + + private val appName = "testApp" + private val appName2 = "testApp2" + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, AppDatabase::class.java + ).build() + storageStatToAppNameDao = db.storageStatsPerAppDao() + appStorageStatsDao = db.appStorageStatsDao() + installedAppsDao = db.installedAppsDao() + installedAppsDao.insert(InstalledApps(appName, listOf())) + installedAppsDao.insert(InstalledApps(appName2, listOf())) + val entry = installedAppsDao.findByPackageName(appName) + Assert.assertNotNull(entry) + appEntry = entry!! + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + @Test + @Throws(IOException::class) + fun findAllTest() { + val range = LongRange(0, 3) + val appNames = listOf(appName, appName2) + for (i in range) { + for (name in appNames) { + appStorageStatsDao.insert(name, Date(i), i) + } + } + + val allStorageStatToAppName = storageStatToAppNameDao.findAll() + Assert.assertEquals( + "Did not return correct number of entries but $allStorageStatToAppName", + range.count() * 2, + allStorageStatToAppName.size + ) + + for (i in range) { + for (name in appNames) { + Assert.assertNotNull( + "", + allStorageStatToAppName.find { + it.packageName == name && it.packageSize == i && it.timestamp == Date(i) + } + ) + } + } + } + + @Test + @Throws(IOException::class) + fun findByPackageNameTest() { + val range = LongRange(0, 3) + val appNames = listOf(appName, appName2) + for (i in range) { + for (name in appNames) { + appStorageStatsDao.insert(name, Date(i), i) + } + } + + val storageStats = storageStatToAppNameDao.findByPackageName(appNames.last()) + Assert.assertTrue( + "Returned storage stats that were not associated to ${appNames.last()}: $storageStats", + storageStats.all { it.packageName == appNames.last() } + ) + Assert.assertEquals( + "Did not return expected number of entries: $storageStats", + range.count(), + storageStats.size + ) + for (i in range) { + Assert.assertNotNull( + "Did not find expected entry with packageSize $i and timestamp ${Date(i)}: " + + "$storageStats", + storageStats.find { + it.packageSize == i && it.timestamp == Date(i) + } + ) + } + } + + @Test + @Throws(IOException::class) + fun findOldestInPeriodTest() { + val range = LongProgression.fromClosedRange(0, 30, 10) + val appNames = listOf(appName, appName2) + for (i in range) { + for (name in appNames) { + appStorageStatsDao.insert(name, Date(i), i) + } + } + val periodStart = Date(4) + val periodEnd = Date(43) + val entry = storageStatToAppNameDao.findOldestWithinPeriod( + appNames.first(), periodStart, periodEnd + ) + Assert.assertNotNull(entry) + val statToAppName = entry!! + Assert.assertEquals(appNames.first(), statToAppName.packageName) + Assert.assertEquals(Date(10), statToAppName.timestamp) + } +} From bd213dfd45133e4f38a69173623997194d09e5ef Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Fri, 22 Dec 2023 17:22:23 +0100 Subject: [PATCH 27/29] remove relationship between `InstalledApps` and `AppStorageStats` tables --- .../5.json | 43 ++--- .../database/AppStorageStatsDaoTest.kt | 62 +++++++- .../database/StorageStatToAppNameDaoTest.kt | 150 ------------------ .../home_page/database/AppDatabase.kt | 16 +- .../home_page/database/AppStorageStats.kt | 20 +-- .../home_page/database/AppStorageStatsDao.kt | 36 +++-- .../database/StorageStatToAppName.kt | 30 ---- .../database/StorageStatToAppNameDao.kt | 67 -------- .../home_page/ui/files/FilesViewModel.kt | 2 +- 9 files changed, 105 insertions(+), 321 deletions(-) delete mode 100644 app/src/androidTest/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDaoTest.kt delete mode 100644 app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppName.kt delete mode 100644 app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDao.kt diff --git a/app/schemas/com.amaze.fileutilities.home_page.database.AppDatabase/5.json b/app/schemas/com.amaze.fileutilities.home_page.database.AppDatabase/5.json index a84d4ca2..028614c0 100644 --- a/app/schemas/com.amaze.fileutilities.home_page.database.AppDatabase/5.json +++ b/app/schemas/com.amaze.fileutilities.home_page.database.AppDatabase/5.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 5, - "identityHash": "09c5ff70859c472f6f1cb4ea43b43003", + "identityHash": "7ce3d27f8810a242aa5248f0eb1d4e1f", "entities": [ { "tableName": "ImageAnalysis", @@ -632,7 +632,7 @@ }, { "tableName": "AppStorageStats", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `package_id` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `package_size` INTEGER NOT NULL, FOREIGN KEY(`package_id`) REFERENCES `InstalledApps`(`_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `package_name` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `package_size` INTEGER NOT NULL)", "fields": [ { "fieldPath": "uid", @@ -641,9 +641,9 @@ "notNull": true }, { - "fieldPath": "packageId", - "columnName": "package_id", - "affinity": "INTEGER", + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", "notNull": true }, { @@ -667,44 +667,23 @@ }, "indices": [ { - "name": "index_AppStorageStats_timestamp_package_id", - "unique": false, + "name": "index_AppStorageStats_timestamp_package_name", + "unique": true, "columnNames": [ "timestamp", - "package_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_AppStorageStats_timestamp_package_id` ON `${TABLE_NAME}` (`timestamp`, `package_id`)" - }, - { - "name": "index_AppStorageStats_package_id", - "unique": false, - "columnNames": [ - "package_id" + "package_name" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_AppStorageStats_package_id` ON `${TABLE_NAME}` (`package_id`)" + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AppStorageStats_timestamp_package_name` ON `${TABLE_NAME}` (`timestamp`, `package_name`)" } ], - "foreignKeys": [ - { - "table": "InstalledApps", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "package_id" - ], - "referencedColumns": [ - "_id" - ] - } - ] + "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '09c5ff70859c472f6f1cb4ea43b43003')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7ce3d27f8810a242aa5248f0eb1d4e1f')" ] } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDaoTest.kt b/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDaoTest.kt index 36997d68..2d7fda0a 100644 --- a/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDaoTest.kt +++ b/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDaoTest.kt @@ -40,6 +40,7 @@ class AppStorageStatsDaoTest { private lateinit var appEntry: InstalledApps private val appName = "testApp" + private val appName2 = "testApp2" @Before fun createDb() { @@ -73,7 +74,9 @@ class AppStorageStatsDaoTest { "Did not contain correct entry with packageId ${appEntry.uid}, timestamp $date " + "and packageSize $size: $allAppStorageStats", allAppStorageStats.find { - it.packageSize == size && it.timestamp == date && it.packageId == appEntry.uid + it.packageSize == size && + it.timestamp == date && + it.packageName == appEntry.packageName } ) } @@ -100,7 +103,62 @@ class AppStorageStatsDaoTest { Assert.assertNotNull( "Database did not contain expected entry with timestamp $laterDate " + "but was $allStorageStats", - allStorageStats.find { it.timestamp == laterDate && appEntry.uid == it.packageId } + allStorageStats.find { + it.timestamp == laterDate && appEntry.packageName == it.packageName + } ) } + + @Test + @Throws(IOException::class) + fun findByPackageNameTest() { + val range = LongRange(0, 3) + val appNames = listOf(appName, appName2) + for (i in range) { + for (name in appNames) { + appStorageStatsDao.insert(name, Date(i), i) + } + } + + val storageStats = appStorageStatsDao.findByPackageName(appNames.last()) + Assert.assertTrue( + "Returned storage stats that were not associated to ${appNames.last()}: $storageStats", + storageStats.all { it.packageName == appNames.last() } + ) + Assert.assertEquals( + "Did not return expected number of entries: $storageStats", + range.count(), + storageStats.size + ) + for (i in range) { + Assert.assertNotNull( + "Did not find expected entry with packageSize $i and timestamp ${Date(i)}: " + + "$storageStats", + storageStats.find { + it.packageSize == i && it.timestamp == Date(i) + } + ) + } + } + + @Test + @Throws(IOException::class) + fun findOldestInPeriodTest() { + val range = LongProgression.fromClosedRange(0, 30, 10) + val appNames = listOf(appName, appName2) + for (i in range) { + for (name in appNames) { + appStorageStatsDao.insert(name, Date(i), i) + } + } + val periodStart = Date(4) + val periodEnd = Date(43) + val entry = appStorageStatsDao.findOldestWithinPeriod( + appNames.first(), periodStart, periodEnd + ) + Assert.assertNotNull(entry) + val statToAppName = entry!! + Assert.assertEquals(appNames.first(), statToAppName.packageName) + Assert.assertEquals(Date(10), statToAppName.timestamp) + } } diff --git a/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDaoTest.kt b/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDaoTest.kt deleted file mode 100644 index 7c233462..00000000 --- a/app/src/androidTest/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDaoTest.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , - * Emmanuel Messulam, Raymond Lai and Contributors. - * - * This file is part of Amaze File Utilities. - * - * Amaze File Utilities is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.amaze.fileutilities.home_page.database - -import android.content.Context -import androidx.room.Room -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.After -import org.junit.Assert -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import java.io.IOException -import java.util.Date - -@RunWith(AndroidJUnit4::class) -class StorageStatToAppNameDaoTest { - private lateinit var db: AppDatabase - private lateinit var storageStatToAppNameDao: StorageStatToAppNameDao - private lateinit var appStorageStatsDao: AppStorageStatsDao - private lateinit var installedAppsDao: InstalledAppsDao - private lateinit var appEntry: InstalledApps - - private val appName = "testApp" - private val appName2 = "testApp2" - - @Before - fun createDb() { - val context = ApplicationProvider.getApplicationContext() - db = Room.inMemoryDatabaseBuilder( - context, AppDatabase::class.java - ).build() - storageStatToAppNameDao = db.storageStatsPerAppDao() - appStorageStatsDao = db.appStorageStatsDao() - installedAppsDao = db.installedAppsDao() - installedAppsDao.insert(InstalledApps(appName, listOf())) - installedAppsDao.insert(InstalledApps(appName2, listOf())) - val entry = installedAppsDao.findByPackageName(appName) - Assert.assertNotNull(entry) - appEntry = entry!! - } - - @After - @Throws(IOException::class) - fun closeDb() { - db.close() - } - - @Test - @Throws(IOException::class) - fun findAllTest() { - val range = LongRange(0, 3) - val appNames = listOf(appName, appName2) - for (i in range) { - for (name in appNames) { - appStorageStatsDao.insert(name, Date(i), i) - } - } - - val allStorageStatToAppName = storageStatToAppNameDao.findAll() - Assert.assertEquals( - "Did not return correct number of entries but $allStorageStatToAppName", - range.count() * 2, - allStorageStatToAppName.size - ) - - for (i in range) { - for (name in appNames) { - Assert.assertNotNull( - "", - allStorageStatToAppName.find { - it.packageName == name && it.packageSize == i && it.timestamp == Date(i) - } - ) - } - } - } - - @Test - @Throws(IOException::class) - fun findByPackageNameTest() { - val range = LongRange(0, 3) - val appNames = listOf(appName, appName2) - for (i in range) { - for (name in appNames) { - appStorageStatsDao.insert(name, Date(i), i) - } - } - - val storageStats = storageStatToAppNameDao.findByPackageName(appNames.last()) - Assert.assertTrue( - "Returned storage stats that were not associated to ${appNames.last()}: $storageStats", - storageStats.all { it.packageName == appNames.last() } - ) - Assert.assertEquals( - "Did not return expected number of entries: $storageStats", - range.count(), - storageStats.size - ) - for (i in range) { - Assert.assertNotNull( - "Did not find expected entry with packageSize $i and timestamp ${Date(i)}: " + - "$storageStats", - storageStats.find { - it.packageSize == i && it.timestamp == Date(i) - } - ) - } - } - - @Test - @Throws(IOException::class) - fun findOldestInPeriodTest() { - val range = LongProgression.fromClosedRange(0, 30, 10) - val appNames = listOf(appName, appName2) - for (i in range) { - for (name in appNames) { - appStorageStatsDao.insert(name, Date(i), i) - } - } - val periodStart = Date(4) - val periodEnd = Date(43) - val entry = storageStatToAppNameDao.findOldestWithinPeriod( - appNames.first(), periodStart, periodEnd - ) - Assert.assertNotNull(entry) - val statToAppName = entry!! - Assert.assertEquals(appNames.first(), statToAppName.packageName) - Assert.assertEquals(Date(10), statToAppName.timestamp) - } -} diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt index b317b12c..4c37d93f 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppDatabase.kt @@ -54,7 +54,6 @@ abstract class AppDatabase : RoomDatabase() { abstract fun installedAppsDao(): InstalledAppsDao abstract fun lyricsDao(): LyricsDao abstract fun appStorageStatsDao(): AppStorageStatsDao - abstract fun storageStatsPerAppDao(): StorageStatToAppNameDao companion object { private var appDatabase: AppDatabase? = null @@ -132,19 +131,14 @@ abstract class AppDatabase : RoomDatabase() { database.execSQL( "CREATE TABLE IF NOT EXISTS `AppStorageStats` " + "(`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "`package_id` INTEGER NOT NULL, " + + "`package_name` TEXT NOT NULL, " + "`timestamp` INTEGER NOT NULL, " + - "`package_size` INTEGER NOT NULL," + - "FOREIGN KEY(`package_id`) REFERENCES `InstalledApps`(`_id`) ON UPDATE " + - "CASCADE ON DELETE CASCADE)" + "`package_size` INTEGER NOT NULL)" ) database.execSQL( - "CREATE INDEX IF NOT EXISTS `index_AppStorageStats_timestamp_package_id`" + - " ON `AppStorageStats` (`timestamp`,`package_id`)" - ) - database.execSQL( - "CREATE INDEX IF NOT EXISTS `index_AppStorageStats_package_id`" + - " ON `AppStorageStats` (`package_id`)" + "CREATE UNIQUE INDEX IF NOT EXISTS " + + "`index_AppStorageStats_timestamp_package_id` " + + "ON `AppStorageStats` (`timestamp`,`package_name`)" ) } } diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt index a4888062..58562a57 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStats.kt @@ -23,8 +23,6 @@ package com.amaze.fileutilities.home_page.database import androidx.annotation.Keep import androidx.room.ColumnInfo import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.Companion.CASCADE import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey @@ -32,17 +30,7 @@ import java.util.Date @Entity( indices = [ - Index(value = ["timestamp", "package_id"], unique = false), - Index(value = ["package_id"], unique = false) // separate index because it is foreign key - ], - foreignKeys = [ - ForeignKey( - entity = InstalledApps::class, - parentColumns = ["_id"], - childColumns = ["package_id"], - onDelete = CASCADE, - onUpdate = CASCADE - ) + Index(value = ["timestamp", "package_name"], unique = true) ] ) @Keep @@ -50,14 +38,14 @@ data class AppStorageStats( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "_id") val uid: Int, - @ColumnInfo(name = "package_id") val packageId: Int, + @ColumnInfo(name = "package_name") val packageName: String, @ColumnInfo(name = "timestamp") val timestamp: Date, @ColumnInfo(name = "package_size") val packageSize: Long ) { @Ignore constructor( - packageId: Int, + packageName: String, timestamp: Date, packageSize: Long - ) : this(0, packageId, timestamp, packageSize) + ) : this(0, packageName, timestamp, packageSize) } diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt index ffae0fbb..47f147ed 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/database/AppStorageStatsDao.kt @@ -28,31 +28,43 @@ import androidx.room.Transaction import java.util.Date @Dao -abstract class AppStorageStatsDao(database: AppDatabase) { - private val installedAppsDao: InstalledAppsDao = database.installedAppsDao() +interface AppStorageStatsDao { @Query("SELECT * FROM AppStorageStats") - abstract fun findAll(): List + fun findAll(): List @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insert(appStorageStats: List) + fun insert(appStorageStats: List) @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insert(appStorageStats: AppStorageStats) + fun insert(appStorageStats: AppStorageStats) /** * Inserts a new [AppStorageStats] associated with the [packageName] and containing [timestamp] * and [size] */ @Transaction - open fun insert(packageName: String, timestamp: Date, size: Long) { - val app = installedAppsDao.findByPackageName(packageName) - if (app != null) { - val appStorageStats = AppStorageStats(app.uid, timestamp, size) - insert(appStorageStats) - } + fun insert(packageName: String, timestamp: Date, size: Long) { + val appStorageStats = AppStorageStats(packageName, timestamp, size) + insert(appStorageStats) } @Query("DELETE FROM AppStorageStats WHERE timestamp < :date") - abstract fun deleteOlderThan(date: Date) + fun deleteOlderThan(date: Date) + + @Query("SELECT * FROM AppStorageStats WHERE package_name=:packageName") + fun findByPackageName(packageName: String): List + + @Query( + "SELECT * FROM AppStorageStats " + + "WHERE package_name = :packageName " + + "AND timestamp >= :periodStart " + // Ensure that timestamp is after `periodStart` + "AND timestamp < :periodEnd " + // Ensure that timestamp is before `periodEnd` + "ORDER BY timestamp ASC LIMIT 1" // Get the oldest entry based on timestamp + ) + fun findOldestWithinPeriod( + packageName: String, + periodStart: Date, + periodEnd: Date + ): AppStorageStats? } diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppName.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppName.kt deleted file mode 100644 index 7a131759..00000000 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppName.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , - * Emmanuel Messulam, Raymond Lai and Contributors. - * - * This file is part of Amaze File Utilities. - * - * Amaze File Utilities is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.amaze.fileutilities.home_page.database - -import androidx.room.ColumnInfo -import java.util.Date - -data class StorageStatToAppName( - @ColumnInfo(name = "package_name") val packageName: String, - @ColumnInfo(name = "timestamp") val timestamp: Date, - @ColumnInfo(name = "package_size") val packageSize: Long -) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDao.kt b/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDao.kt deleted file mode 100644 index 4ec83226..00000000 --- a/app/src/main/java/com/amaze/fileutilities/home_page/database/StorageStatToAppNameDao.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2021-2023 Arpit Khurana , Vishal Nehra , - * Emmanuel Messulam, Raymond Lai and Contributors. - * - * This file is part of Amaze File Utilities. - * - * Amaze File Utilities is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.amaze.fileutilities.home_page.database - -import androidx.room.Dao -import androidx.room.Query -import java.util.Date - -@Dao -interface StorageStatToAppNameDao { - @Query( - "SELECT I.package_name AS package_name, " + - "A.timestamp AS timestamp, " + - "A.package_size AS package_size " + - "FROM AppStorageStats AS A " + - "INNER JOIN InstalledApps AS I " + - "ON I._id = A.package_id" - ) - fun findAll(): List - - @Query( - "SELECT I.package_name AS package_name, " + - "A.timestamp AS timestamp, " + - "A.package_size AS package_size " + - "FROM AppStorageStats AS A " + - "INNER JOIN InstalledApps AS I " + - "ON I._id = A.package_id " + - "WHERE I.package_name=:packageName" - ) - fun findByPackageName(packageName: String): List - - @Query( - "SELECT I.package_name AS package_name, " + - "A.timestamp AS timestamp, " + - "A.package_size AS package_size " + - "FROM AppStorageStats AS A " + - "INNER JOIN InstalledApps AS I " + - "ON I._id = A.package_id " + - "WHERE I.package_name=:packageName " + - "AND A.timestamp >= :periodStart " + // Ensure that timestamp is after `periodStart` - "AND A.timestamp < :periodEnd " + // Ensure that timestamp is before `periodEnd` - "ORDER BY A.timestamp ASC LIMIT 1" // Get the oldest entry based on timestamp - ) - fun findOldestWithinPeriod( - packageName: String, - periodStart: Date, - periodEnd: Date - ): StorageStatToAppName? -} diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt index 38baf3b2..4bc71902 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/files/FilesViewModel.kt @@ -1971,7 +1971,7 @@ class FilesViewModel(val applicationContext: Application) : // The end of the search period is now val periodEnd = Date.from(ZonedDateTime.now().toInstant()) - val dao = AppDatabase.getInstance(applicationContext).storageStatsPerAppDao() + val dao = AppDatabase.getInstance(applicationContext).appStorageStatsDao() val priorityQueue = FixedSizePriorityQueue(50) { o1, o2 -> val diff1 = o1.extraInfo?.apkMetaData?.sizeDiff ?: 0 From d9aeeca17d17e5117a1689f604b5fdec4006dcec Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Fri, 29 Dec 2023 23:00:41 +0100 Subject: [PATCH 28/29] change `QueryAppSizeWorker` to only store precise app sizes --- .../fileutilities/utilis/QueryAppSizeWorker.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/QueryAppSizeWorker.kt b/app/src/main/java/com/amaze/fileutilities/utilis/QueryAppSizeWorker.kt index ff68090f..6e1cd0f7 100644 --- a/app/src/main/java/com/amaze/fileutilities/utilis/QueryAppSizeWorker.kt +++ b/app/src/main/java/com/amaze/fileutilities/utilis/QueryAppSizeWorker.kt @@ -23,6 +23,7 @@ package com.amaze.fileutilities.utilis import android.content.Context import android.content.pm.PackageManager import android.os.Build +import androidx.annotation.RequiresApi import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.amaze.fileutilities.home_page.database.AppDatabase @@ -38,6 +39,13 @@ class QueryAppSizeWorker( workerParameters: WorkerParameters ) : CoroutineWorker(context, workerParameters) { override suspend fun doWork(): Result { + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + !isUsageStatsPermissionGranted(this.applicationContext) + ) { + return Result.failure() + } + val appStorageStatsDao = AppDatabase.getInstance(applicationContext).appStorageStatsDao() val packageManager = applicationContext.packageManager // Get all currently installed apps @@ -67,6 +75,15 @@ class QueryAppSizeWorker( return Result.success() } + @RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) + private fun isUsageStatsPermissionGranted(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Utils.checkUsageStatsPermission(context) + } else { + Utils.getAppsUsageStats(context, 30).isNotEmpty() + } + } + companion object { const val NAME: String = "query_app_size_worker" } From cfaac5de48244a661bf5df6e59a870388951bdbc Mon Sep 17 00:00:00 2001 From: Selina Lin Date: Fri, 29 Dec 2023 23:01:57 +0100 Subject: [PATCH 29/29] add permission loader for `largeSizeDiffAppsPreview` and restart worker if permission is given --- .../fileutilities/home_page/MainActivity.kt | 13 +---- .../home_page/ui/analyse/AnalyseFragment.kt | 56 ++++++++++++------- .../com/amaze/fileutilities/utilis/Utils.kt | 16 ++++++ 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/MainActivity.kt b/app/src/main/java/com/amaze/fileutilities/home_page/MainActivity.kt index e13009db..13bac881 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/MainActivity.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/MainActivity.kt @@ -39,8 +39,6 @@ import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupWithNavController import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager import com.amaze.fileutilities.BuildConfig import com.amaze.fileutilities.R import com.amaze.fileutilities.WifiP2PActivity @@ -59,7 +57,6 @@ import com.amaze.fileutilities.home_page.ui.settings.PreferenceActivity import com.amaze.fileutilities.home_page.ui.transfer.TransferFragment import com.amaze.fileutilities.utilis.ItemsActionBarFragment import com.amaze.fileutilities.utilis.PreferencesConstants -import com.amaze.fileutilities.utilis.QueryAppSizeWorker import com.amaze.fileutilities.utilis.UpdateChecker import com.amaze.fileutilities.utilis.Utils import com.amaze.fileutilities.utilis.getAppCommonSharedPreferences @@ -74,7 +71,6 @@ import com.stephentuso.welcome.WelcomeHelper import org.slf4j.Logger import org.slf4j.LoggerFactory import java.util.Date -import java.util.concurrent.TimeUnit class MainActivity : WifiP2PActivity(), @@ -256,14 +252,7 @@ class MainActivity : .apply() } - // schedule PeriodicWorkRequest to store the size of each app in the database every day - val periodicWorkRequest = PeriodicWorkRequestBuilder( - 24, - TimeUnit.HOURS - ).build() - WorkManager.getInstance(this).enqueueUniquePeriodicWork( - QueryAppSizeWorker.NAME, ExistingPeriodicWorkPolicy.KEEP, periodicWorkRequest - ) + Utils.scheduleQueryAppSizeWorker(this, ExistingPeriodicWorkPolicy.KEEP) } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseFragment.kt b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseFragment.kt index 3166cb9d..6c914d66 100644 --- a/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseFragment.kt +++ b/app/src/main/java/com/amaze/fileutilities/home_page/ui/analyse/AnalyseFragment.kt @@ -35,6 +35,7 @@ import androidx.annotation.RequiresApi import androidx.fragment.app.activityViewModels import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.NavHostFragment +import androidx.work.ExistingPeriodicWorkPolicy import com.amaze.fileutilities.BuildConfig import com.amaze.fileutilities.R import com.amaze.fileutilities.databinding.FragmentAnalyseBinding @@ -483,9 +484,7 @@ class AnalyseFragment : AbstractMediaFileInfoOperationsFragment() { unusedAppsPreview.loadRequireElevatedPermission({ val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS) startActivity(intent) - }, { - reloadFragment() - }) + }, ::usageStatsPermissionReload) } else { filesViewModel.getUnusedApps().observe(viewLifecycleOwner) { mediaFileInfoList -> @@ -509,15 +508,11 @@ class AnalyseFragment : AbstractMediaFileInfoOperationsFragment() { mostUsedAppsPreview.loadRequireElevatedPermission({ val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS) startActivity(intent) - }, { - reloadFragment() - }) + }, ::usageStatsPermissionReload) leastUsedAppsPreview.loadRequireElevatedPermission({ val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS) startActivity(intent) - }, { - reloadFragment() - }) + }, ::usageStatsPermissionReload) } else { filesViewModel.getMostUsedApps().observe(viewLifecycleOwner) { mediaFileInfoList -> @@ -557,9 +552,7 @@ class AnalyseFragment : AbstractMediaFileInfoOperationsFragment() { networkIntensiveAppsPreview.loadRequireElevatedPermission({ val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS) startActivity(intent) - }, { - reloadFragment() - }) + }, ::usageStatsPermissionReload) } else { filesViewModel.getNetworkIntensiveApps().observe(viewLifecycleOwner) { mediaFileInfoList -> @@ -613,18 +606,29 @@ class AnalyseFragment : AbstractMediaFileInfoOperationsFragment() { } } } - filesViewModel.getLargeSizeDiffApps() - .observe(viewLifecycleOwner) { mediaFileInfoList -> - largeSizeDiffAppsPreview.invalidateProgress(true, null) - mediaFileInfoList?.let { - largeSizeDiffAppsPreview.invalidateProgress(false, null) - largeSizeDiffAppsPreview.loadPreviews(mediaFileInfoList) { - cleanButtonClick(it, true) { - filesViewModel.largeSizeDiffAppsLiveData = null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + !isUsageStatsPermissionGranted() + ) { + // Starting with version O, the PACKAGE_USAGE_STATS permission is necessary to query + // the size of other apps + largeSizeDiffAppsPreview.loadRequireElevatedPermission({ + val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS) + startActivity(intent) + }, ::usageStatsPermissionReload) + } else { + filesViewModel.getLargeSizeDiffApps() + .observe(viewLifecycleOwner) { mediaFileInfoList -> + largeSizeDiffAppsPreview.invalidateProgress(true, null) + mediaFileInfoList?.let { + largeSizeDiffAppsPreview.invalidateProgress(false, null) + largeSizeDiffAppsPreview.loadPreviews(mediaFileInfoList) { + cleanButtonClick(it, true) { + filesViewModel.largeSizeDiffAppsLiveData = null + } } } } - } + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { gamesPreview.visibility = View.VISIBLE @@ -747,6 +751,16 @@ class AnalyseFragment : AbstractMediaFileInfoOperationsFragment() { } } + private fun usageStatsPermissionReload() { + // When the permission is given, the worker is reenqueued to store the size of + // each app in the database for the analysis + Utils.scheduleQueryAppSizeWorker( + requireContext(), + ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE + ) + reloadFragment() + } + @RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) private fun isUsageStatsPermissionGranted(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { diff --git a/app/src/main/java/com/amaze/fileutilities/utilis/Utils.kt b/app/src/main/java/com/amaze/fileutilities/utilis/Utils.kt index 8f1dfb0f..293c0a1a 100644 --- a/app/src/main/java/com/amaze/fileutilities/utilis/Utils.kt +++ b/app/src/main/java/com/amaze/fileutilities/utilis/Utils.kt @@ -83,6 +83,9 @@ import androidx.core.graphics.drawable.toBitmap import androidx.core.view.isVisible import androidx.palette.graphics.Palette import androidx.recyclerview.widget.GridLayoutManager +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager import com.abedelazizshe.lightcompressorlibrary.VideoQuality import com.amaze.fileutilities.BuildConfig import com.amaze.fileutilities.R @@ -1516,6 +1519,19 @@ class Utils { return hexString.toString() } + /** + * Schedules PeriodicWorkRequest to store the size of each app in the database every day + */ + fun scheduleQueryAppSizeWorker(context: Context, policy: ExistingPeriodicWorkPolicy) { + val periodicWorkRequest = PeriodicWorkRequestBuilder( + 24, + TimeUnit.HOURS + ).build() + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + QueryAppSizeWorker.NAME, policy, periodicWorkRequest + ) + } + private fun findApplicationInfoSizeFallback(applicationInfo: ApplicationInfo): Long { var cacheSize = 0L File(applicationInfo.sourceDir).parentFile?.let {