summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/feature/share
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
commitd8bbc7858622b6d9c278469aab701ca0b609cddf (patch)
treeeff41dc61d9f714852212739e6b3738b82a2af87 /mobile/android/android-components/components/feature/share
parentReleasing progress-linux version 125.0.3-1~progress7.99u1. (diff)
downloadfirefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz
firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/feature/share')
-rw-r--r--mobile/android/android-components/components/feature/share/README.md19
-rw-r--r--mobile/android/android-components/components/feature/share/build.gradle73
-rw-r--r--mobile/android/android-components/components/feature/share/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/1.json40
-rw-r--r--mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/2.json40
-rw-r--r--mobile/android/android-components/components/feature/share/src/androidTest/java/mozilla/components/feature/share/RecentAppsDaoTest.kt105
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentApp.kt24
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentAppsStorage.kt60
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/adapter/RecentAppAdapter.kt30
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppEntity.kt22
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDao.kt73
-rw-r--r--mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDatabase.kt60
-rw-r--r--mobile/android/android-components/components/feature/share/src/test/java/mozilla/components/feature/share/RecentAppStorageTest.kt76
14 files changed, 647 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/feature/share/README.md b/mobile/android/android-components/components/feature/share/README.md
new file mode 100644
index 0000000000..6f188d49d9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Recents
+
+Feature implementation for saving and sorting recent apps used for sharing.
+
+## Usage
+
+### Setting up the dependency
+
+Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
+
+```Groovy
+implementation "org.mozilla.components:feature-share:{latest-version}"
+```
+
+## License
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/
diff --git a/mobile/android/android-components/components/feature/share/build.gradle b/mobile/android/android-components/components/feature/share/build.gradle
new file mode 100644
index 0000000000..a44df774e1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/build.gradle
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'com.google.devtools.ksp'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ arg("room.generateKotlin", "true")
+ }
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += ["room.incremental": "true"]
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ packagingOptions {
+ exclude 'META-INF/proguard/androidx-annotations.pro'
+ }
+
+ sourceSets {
+ androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ }
+
+ namespace 'mozilla.components.feature.share'
+}
+
+dependencies {
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_lifecycle_runtime
+ implementation ComponentsDependencies.androidx_room_runtime
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ androidTestImplementation project(':support-android-test')
+
+ androidTestImplementation ComponentsDependencies.androidx_room_testing
+ androidTestImplementation ComponentsDependencies.androidx_test_core
+ androidTestImplementation ComponentsDependencies.androidx_test_runner
+ androidTestImplementation ComponentsDependencies.androidx_test_rules
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
diff --git a/mobile/android/android-components/components/feature/share/proguard-rules.pro b/mobile/android/android-components/components/feature/share/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/1.json b/mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/1.json
new file mode 100644
index 0000000000..6a26877289
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/1.json
@@ -0,0 +1,40 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "05028edca931d077160a0d3a3e19b20f",
+ "entities": [
+ {
+ "tableName": "RECENT_APPS_TABLE",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `score` REAL NOT NULL, PRIMARY KEY(`packageName`))",
+ "fields": [
+ {
+ "fieldPath": "packageName",
+ "columnName": "packageName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "score",
+ "columnName": "score",
+ "affinity": "REAL",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "packageName"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "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, '05028edca931d077160a0d3a3e19b20f')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/2.json b/mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/2.json
new file mode 100644
index 0000000000..241b612fa9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/schemas/mozilla.components.feature.share.db.RecentAppsDatabase/2.json
@@ -0,0 +1,40 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "8ab7d915ce81c5d52503917cbafbe4df",
+ "entities": [
+ {
+ "tableName": "RECENT_APPS_TABLE",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`activityName` TEXT NOT NULL, `score` REAL NOT NULL, PRIMARY KEY(`activityName`))",
+ "fields": [
+ {
+ "fieldPath": "activityName",
+ "columnName": "activityName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "score",
+ "columnName": "score",
+ "affinity": "REAL",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "activityName"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "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, '8ab7d915ce81c5d52503917cbafbe4df')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/src/androidTest/java/mozilla/components/feature/share/RecentAppsDaoTest.kt b/mobile/android/android-components/components/feature/share/src/androidTest/java/mozilla/components/feature/share/RecentAppsDaoTest.kt
new file mode 100644
index 0000000000..b0986964f1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/androidTest/java/mozilla/components/feature/share/RecentAppsDaoTest.kt
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.share
+
+import android.content.Context
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import mozilla.components.feature.share.db.RecentAppEntity
+import mozilla.components.feature.share.db.RecentAppsDao
+import mozilla.components.feature.share.db.RecentAppsDatabase
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class RecentAppsDaoTest {
+
+ private lateinit var database: RecentAppsDatabase
+ private lateinit var dao: RecentAppsDao
+
+ @Before
+ fun setup() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ database = Room.inMemoryDatabaseBuilder(context, RecentAppsDatabase::class.java).build()
+ dao = database.recentAppsDao()
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ }
+
+ @Test
+ fun testGetTwoMostRecentApps() {
+ assertEquals(emptyList<RecentApp>(), dao.getRecentAppsUpTo(2))
+
+ dao.insertRecentApps(
+ listOf(
+ RecentAppEntity("first"),
+ RecentAppEntity("second"),
+ RecentAppEntity("third"),
+ ),
+ )
+
+ assertEquals(
+ listOf(
+ RecentAppEntity("first"),
+ RecentAppEntity("second"),
+ ),
+ dao.getRecentAppsUpTo(2),
+ )
+ }
+
+ @Test
+ fun testIncrementSelectedAppCountAndDecayAllOthers() {
+ val activityName = "activityName"
+ val selectedAppEntity = RecentAppEntity(score = 1.0, activityName = activityName)
+ val otherAppEntity = RecentAppEntity(score = 100.0, activityName = "other")
+ val allRecentApps = listOf(selectedAppEntity, otherAppEntity)
+
+ dao.insertRecentApps(allRecentApps)
+
+ dao.updateRecentAppAndDecayRest(activityName)
+
+ val allAppsResult = dao.getRecentAppsUpTo(2)
+ val selectedResult = allAppsResult.first { it.activityName == activityName }
+ val otherResult = allAppsResult.first { it.activityName == "other" }
+
+ assertEquals(95.0, otherResult.score, 0.0001)
+ assertEquals(2.0, selectedResult.score, 0.0001)
+ }
+
+ @Test
+ fun testAddNewlyInstalledAppsToOurDatabase() {
+ val firstActivityName = "first"
+ val secondActivityName = "second"
+ val thirdActivityName = "third"
+ val fourthActivityName = "fourth"
+ val appsInDatabase = listOf(
+ RecentAppEntity(firstActivityName),
+ RecentAppEntity(secondActivityName),
+ RecentAppEntity(thirdActivityName),
+ )
+ val additionalApp = RecentAppEntity(fourthActivityName)
+
+ dao.insertRecentApps(appsInDatabase)
+ dao.insertRecentApps(appsInDatabase + additionalApp)
+
+ assertEquals(appsInDatabase + additionalApp, dao.getRecentAppsUpTo(7))
+ }
+
+ @Test
+ fun testDeleteAnAppFromOurDatabase() {
+ val deleteAppName = "delete"
+ val recentApp = RecentAppEntity(score = 1.0, activityName = deleteAppName)
+
+ dao.insertRecentApps(listOf(recentApp))
+
+ dao.deleteRecentApp(recentApp.activityName)
+
+ assertEquals(emptyList<RecentApp>(), dao.getRecentAppsUpTo(1))
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/share/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentApp.kt b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentApp.kt
new file mode 100644
index 0000000000..cacfdc8192
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentApp.kt
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.share
+
+/**
+ * Interface used for adapting recent apps database entities
+ *
+ * @property activityName - unique identifier of the app
+ * @property score - value used for sorting in descending order the recent apps (most recent first)
+ */
+interface RecentApp {
+
+ /**
+ * The activityName of the recent app.
+ */
+ val activityName: String
+
+ /**
+ * The score of the recent app (calculated based on number of selections - decay)
+ */
+ val score: Double
+}
diff --git a/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentAppsStorage.kt b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentAppsStorage.kt
new file mode 100644
index 0000000000..563eff85d9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/RecentAppsStorage.kt
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.share
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import mozilla.components.feature.share.db.RecentAppEntity
+import mozilla.components.feature.share.db.RecentAppsDao
+import mozilla.components.feature.share.db.RecentAppsDatabase
+
+/**
+ * Class used for storing and retrieving the most recent apps
+ */
+class RecentAppsStorage(context: Context) {
+
+ @VisibleForTesting
+ internal var recentAppsDao: Lazy<RecentAppsDao> = lazy {
+ RecentAppsDatabase.get(context).recentAppsDao()
+ }
+
+ /**
+ * Increment the value stored in the database for the selected app. Then, apply a decay to all
+ * other apps in the database. This allows newly installed apps to catch up and appear in the
+ * most recent section faster. We do not need to handle overflow as it's not reasonably expected
+ * to reach Double.MAX_VALUE for users
+ */
+ fun updateRecentApp(selectedActivityName: String) {
+ recentAppsDao.value.updateRecentAppAndDecayRest(selectedActivityName)
+ }
+
+ /**
+ * Deletes an app form the recent apps list
+ * @param activityName - name of the activity of the app
+ */
+ fun deleteRecentApp(activityName: String) {
+ recentAppsDao.value.deleteRecentApp(activityName)
+ }
+
+ /**
+ * Get a descending ordered list of the most recent apps
+ * @param limit - size of list
+ */
+ fun getRecentAppsUpTo(limit: Int): List<RecentApp> {
+ return recentAppsDao.value.getRecentAppsUpTo(limit)
+ }
+
+ /**
+ * If there are apps that could resolve our share and are not added in our database, we add them
+ * with a 0 count, so they can be updated later when a user uses that app
+ */
+ fun updateDatabaseWithNewApps(activityNames: List<String>) {
+ recentAppsDao.value.insertRecentApps(
+ activityNames.map { activityName ->
+ RecentAppEntity(activityName)
+ },
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/adapter/RecentAppAdapter.kt b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/adapter/RecentAppAdapter.kt
new file mode 100644
index 0000000000..ff0a582b8f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/adapter/RecentAppAdapter.kt
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.share.adapter
+
+import mozilla.components.feature.share.RecentApp
+import mozilla.components.feature.share.db.RecentAppEntity
+
+internal class RecentAppAdapter(
+ internal val entity: RecentAppEntity,
+) : RecentApp {
+
+ override val activityName: String
+ get() = entity.activityName
+
+ override val score: Double
+ get() = entity.score
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is RecentAppAdapter) {
+ return false
+ }
+ return entity == other.entity
+ }
+
+ override fun hashCode(): Int {
+ return entity.hashCode()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppEntity.kt b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppEntity.kt
new file mode 100644
index 0000000000..c519c91b4b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppEntity.kt
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.share.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.feature.share.RecentApp
+import mozilla.components.feature.share.db.RecentAppsDatabase.Companion.RECENT_APPS_TABLE
+
+@Entity(tableName = RECENT_APPS_TABLE)
+internal data class RecentAppEntity(
+
+ @PrimaryKey
+ @ColumnInfo(name = "activityName")
+ override var activityName: String,
+
+ @ColumnInfo(name = "score")
+ override var score: Double = 0.0,
+) : RecentApp
diff --git a/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDao.kt b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDao.kt
new file mode 100644
index 0000000000..9493ca20c0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDao.kt
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.share.db
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+
+private const val DECAY_MULTIPLIER = 0.95
+
+@Dao
+internal abstract class RecentAppsDao {
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ abstract fun insertRecentApps(recentApps: List<RecentAppEntity>)
+
+ @Query(
+ """
+ DELETE FROM recent_apps_table
+ WHERE activityName = :activityName
+ """,
+ )
+ abstract fun deleteRecentApp(activityName: String)
+
+ @Query(
+ """
+ SELECT * FROM recent_apps_table
+ ORDER BY score DESC
+ LIMIT :limit
+ """,
+ )
+ abstract fun getRecentAppsUpTo(limit: Int): List<RecentAppEntity>
+
+ /**
+ * Increments the score of a recent app.
+ * @param activityName - Name of the recent app to update.
+ */
+ @Query(
+ """
+ UPDATE recent_apps_table
+ SET score = score + 1
+ WHERE activityName = :activityName
+ """,
+ )
+ abstract fun updateRecentAppScore(activityName: String)
+
+ /**
+ * Decreases the score of all but one app (exponential decay).
+ * @param exceptActivity - ID of recent app to leave alone
+ * @param decay - Amount to decay by. Should be between 0 and 1.
+ */
+ @Query(
+ """
+ UPDATE recent_apps_table
+ SET score = score * :decay
+ WHERE activityName != :exceptActivity
+ """,
+ )
+ abstract fun decayAllRecentApps(
+ exceptActivity: String,
+ decay: Double = DECAY_MULTIPLIER,
+ )
+
+ @Transaction
+ open fun updateRecentAppAndDecayRest(activityName: String) {
+ updateRecentAppScore(activityName)
+ decayAllRecentApps(exceptActivity = activityName)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDatabase.kt b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDatabase.kt
new file mode 100644
index 0000000000..d554bd3209
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/main/java/mozilla/components/feature/share/db/RecentAppsDatabase.kt
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.share.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import mozilla.components.feature.share.db.RecentAppsDatabase.Companion.RECENT_APPS_TABLE
+
+/**
+ * Internal database for storing apps and their scores that determine if they are most recently used.
+ */
+@Database(entities = [RecentAppEntity::class], version = 2)
+internal abstract class RecentAppsDatabase : RoomDatabase() {
+ abstract fun recentAppsDao(): RecentAppsDao
+
+ companion object {
+
+ const val RECENT_APPS_TABLE = "RECENT_APPS_TABLE"
+
+ @Volatile
+ private var instance: RecentAppsDatabase? = null
+
+ @Synchronized
+ fun get(context: Context): RecentAppsDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ RecentAppsDatabase::class.java,
+ RECENT_APPS_TABLE,
+ ).addMigrations(
+ Migrations.migration_1_2,
+ ).build().also {
+ instance = it
+ }
+ }
+ }
+}
+
+internal object Migrations {
+ val migration_1_2 = object : Migration(1, 2) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("DROP TABLE RECENT_APPS_TABLE")
+ db.execSQL(
+ "CREATE TABLE IF NOT EXISTS " + RECENT_APPS_TABLE +
+ "(" +
+ "`activityName` TEXT NOT NULL, " +
+ "`score` DOUBLE NOT NULL, " +
+ " PRIMARY KEY(`activityName`)" +
+ ")",
+ )
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/share/src/test/java/mozilla/components/feature/share/RecentAppStorageTest.kt b/mobile/android/android-components/components/feature/share/src/test/java/mozilla/components/feature/share/RecentAppStorageTest.kt
new file mode 100644
index 0000000000..6a8dee764b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/share/src/test/java/mozilla/components/feature/share/RecentAppStorageTest.kt
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.share
+
+import android.content.Context
+import mozilla.components.feature.share.db.RecentAppEntity
+import mozilla.components.feature.share.db.RecentAppsDao
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class RecentAppStorageTest {
+
+ private lateinit var context: Context
+ private lateinit var recentAppsDao: RecentAppsDao
+ private lateinit var recentAppsStorage: RecentAppsStorage
+
+ @Before
+ fun setup() {
+ context = mock()
+ recentAppsDao = mock()
+
+ recentAppsStorage = RecentAppsStorage(context)
+ recentAppsStorage.recentAppsDao = lazyOf(recentAppsDao)
+ }
+
+ @Test
+ fun `get the two most recent apps`() {
+ whenever(recentAppsDao.getRecentAppsUpTo(2)).thenReturn(emptyList())
+
+ assertEquals(emptyList<RecentApp>(), recentAppsStorage.getRecentAppsUpTo(2))
+ }
+
+ @Test
+ fun `increment selected app count and decay all others`() {
+ val activityName = "activityName"
+
+ recentAppsStorage.updateRecentApp(activityName)
+
+ verify(recentAppsDao).updateRecentAppAndDecayRest(activityName)
+ }
+
+ @Test
+ fun `add newly installed apps to our database`() {
+ val firstActivityName = "first"
+ val secondActivityName = "second"
+ val thirdActivityName = "third"
+ val fourthActivityName = "fourth"
+ val currentApps = listOf(firstActivityName, secondActivityName, thirdActivityName, fourthActivityName)
+ val appsInDatabase = listOf(
+ RecentAppEntity(firstActivityName),
+ RecentAppEntity(secondActivityName),
+ RecentAppEntity(thirdActivityName),
+ RecentAppEntity(fourthActivityName),
+ )
+
+ recentAppsStorage.updateDatabaseWithNewApps(currentApps)
+
+ verify(recentAppsDao).insertRecentApps(appsInDatabase)
+ }
+
+ @Test
+ fun `delete an app from our database`() {
+ val deleteAppName = "delete"
+ val recentApp = RecentAppEntity(score = 1.0, activityName = deleteAppName)
+
+ recentAppsStorage.deleteRecentApp(recentApp.activityName)
+
+ verify(recentAppsDao).deleteRecentApp(deleteAppName)
+ }
+}