diff --git a/data/src/main/kotlin/com/bitwarden/data/manager/BitwardenPackageManager.kt b/data/src/main/kotlin/com/bitwarden/data/manager/BitwardenPackageManager.kt index a2e4e9f035..1ea6e3bad4 100644 --- a/data/src/main/kotlin/com/bitwarden/data/manager/BitwardenPackageManager.kt +++ b/data/src/main/kotlin/com/bitwarden/data/manager/BitwardenPackageManager.kt @@ -4,6 +4,14 @@ package com.bitwarden.data.manager * Abstraction for interacting with Android package manager. */ interface BitwardenPackageManager { + + /** + * Gets the package installation source. The result may be `null` if the package is not + * installed, the package is a system application, or the installing application has been + * uninstalled. + */ + fun getPackageInstallationSourceOrNull(packageName: String): String? + /** * Checks if the package is installed. */ diff --git a/data/src/main/kotlin/com/bitwarden/data/manager/BitwardenPackageManagerImpl.kt b/data/src/main/kotlin/com/bitwarden/data/manager/BitwardenPackageManagerImpl.kt index 0c81f30fdd..9b2e049cfb 100644 --- a/data/src/main/kotlin/com/bitwarden/data/manager/BitwardenPackageManagerImpl.kt +++ b/data/src/main/kotlin/com/bitwarden/data/manager/BitwardenPackageManagerImpl.kt @@ -2,6 +2,7 @@ package com.bitwarden.data.manager import android.content.Context import android.content.pm.PackageManager import android.os.Build +import com.bitwarden.core.util.isBuildVersionAtLeast /** * Primary implementation of [BitwardenPackageManager]. @@ -12,9 +13,24 @@ class BitwardenPackageManagerImpl( private val nativePackageManager = context.packageManager + override fun getPackageInstallationSourceOrNull(packageName: String): String? = + try { + if (isBuildVersionAtLeast(Build.VERSION_CODES.R)) { + nativePackageManager + .getInstallSourceInfo(packageName) + .installingPackageName + } else { + @Suppress("DEPRECATION") + nativePackageManager + .getInstallerPackageName(packageName) + } + } catch (_: PackageManager.NameNotFoundException) { + null + } + override fun isPackageInstalled(packageName: String): Boolean { return try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (isBuildVersionAtLeast(Build.VERSION_CODES.TIRAMISU)) { nativePackageManager.getApplicationInfo( packageName, PackageManager.ApplicationInfoFlags.of(0L), diff --git a/data/src/test/kotlin/com/bitwarden/data/manager/BitwardenPackageManagerTest.kt b/data/src/test/kotlin/com/bitwarden/data/manager/BitwardenPackageManagerTest.kt index 09f3b256d4..214c90ada5 100644 --- a/data/src/test/kotlin/com/bitwarden/data/manager/BitwardenPackageManagerTest.kt +++ b/data/src/test/kotlin/com/bitwarden/data/manager/BitwardenPackageManagerTest.kt @@ -3,13 +3,19 @@ package com.bitwarden.data.manager import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager +import com.bitwarden.core.util.isBuildVersionAtLeast import io.mockk.every import io.mockk.mockk -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Test +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import io.mockk.verify +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test class BitwardenPackageManagerTest { @@ -19,10 +25,33 @@ class BitwardenPackageManagerTest { } private val bitwardenPackageManager = BitwardenPackageManagerImpl(context) + @BeforeEach + fun setUp() { + mockkStatic( + PackageManager.ApplicationInfoFlags::of, + ::isBuildVersionAtLeast, + ) + // Set the default API level to simulate the latest version + every { isBuildVersionAtLeast(any()) } returns true + } + + @AfterEach + fun tearDown() { + unmockkStatic( + PackageManager.ApplicationInfoFlags::of, + ::isBuildVersionAtLeast, + ) + } + @Test fun `isPackageInstalled returns true for installed package`() { val packageName = "com.example.installed" - every { mockPackageManager.getApplicationInfo(packageName, 0) } returns ApplicationInfo() + every { + mockPackageManager.getApplicationInfo( + packageName, + PackageManager.ApplicationInfoFlags.of(0L), + ) + } returns ApplicationInfo() val result = bitwardenPackageManager.isPackageInstalled(packageName) assertTrue(result) } @@ -31,7 +60,10 @@ class BitwardenPackageManagerTest { fun `isPackageInstalled returns false for non existent package`() { val packageName = "com.example.nonexistent" every { - mockPackageManager.getApplicationInfo(packageName, 0) + mockPackageManager.getApplicationInfo( + packageName, + PackageManager.ApplicationInfoFlags.of(0L), + ) } throws PackageManager.NameNotFoundException() val result = bitwardenPackageManager.isPackageInstalled(packageName) assertFalse(result) @@ -41,7 +73,10 @@ class BitwardenPackageManagerTest { fun `isPackageInstalled handles empty package name`() { val packageName = "" every { - mockPackageManager.getApplicationInfo(packageName, 0) + mockPackageManager.getApplicationInfo( + packageName, + PackageManager.ApplicationInfoFlags.of(0L), + ) } throws PackageManager.NameNotFoundException() val result = bitwardenPackageManager.isPackageInstalled(packageName) assertFalse(result) @@ -51,12 +86,27 @@ class BitwardenPackageManagerTest { fun `isPackageInstalled handles package name with special characters`() { val packageName = "com.example.invalid name!" every { - mockPackageManager.getApplicationInfo(packageName, 0) + mockPackageManager.getApplicationInfo( + packageName, + PackageManager.ApplicationInfoFlags.of(0L), + ) } throws PackageManager.NameNotFoundException() val result = bitwardenPackageManager.isPackageInstalled(packageName) assertFalse(result) } + @Suppress("MaxLineLength") + @Test + fun `isPackageInstalled invokes correct getApplicationInfo overload when API level is below 33`() { + val packageName = "com.example.installed" + every { isBuildVersionAtLeast(33) } returns false + every { + mockPackageManager.getApplicationInfo(packageName, 0) + } returns mockk() + bitwardenPackageManager.isPackageInstalled(packageName) + verify { mockPackageManager.getApplicationInfo(packageName, 0) } + } + @Test fun `getAppLabelForPackageOrNull returns correct label for installed package`() { val packageName = "com.example.installed" @@ -107,4 +157,59 @@ class BitwardenPackageManagerTest { val result = bitwardenPackageManager.getAppLabelForPackageOrNull(packageName) assertNull(result) } + + @Test + fun `getPackageInstallationSourceOrNull returns correct source for installed package`() { + val packageName = "com.example.installed" + val installationSource = "com.example.source" + every { mockPackageManager.getInstallSourceInfo(packageName) } returns mockk { + every { installingPackageName } returns installationSource + } + val result = bitwardenPackageManager.getPackageInstallationSourceOrNull(packageName) + assertEquals(installationSource, result) + } + + @Test + fun `getPackageInstallationSourceOrNull returns null when installation source is null`() { + val packageName = "com.example.installed" + every { + mockPackageManager.getInstallSourceInfo(packageName) + } returns mockk { + every { installingPackageName } returns null + } + val result = bitwardenPackageManager.getPackageInstallationSourceOrNull(packageName) + assertNull(result) + } + + @Test + fun `getPackageInstallationSourceOrNull returns null for non existent package`() { + val packageName = "com.example.nonexistent" + every { + mockPackageManager.getInstallSourceInfo(packageName) + } throws PackageManager.NameNotFoundException() + val result = bitwardenPackageManager.getPackageInstallationSourceOrNull(packageName) + assertNull(result) + } + + @Suppress("DEPRECATION") + @Test + fun `getPackageInstallationSourceOrNull invokes getInstallerPackageName on API 29 or lower`() { + val packageName = "com.example.installed" + val installationSource = "com.example.source" + every { isBuildVersionAtLeast(30) } returns false + every { mockPackageManager.getInstallerPackageName(packageName) } returns installationSource + bitwardenPackageManager.getPackageInstallationSourceOrNull(packageName) + verify { mockPackageManager.getInstallerPackageName(packageName) } + } + + @Test + fun `getPackageInstallationSourceOrNull invokes getInstallSourceInfo on API 30 or higher`() { + val packageName = "com.example.installed" + val installationSource = "com.example.source" + every { mockPackageManager.getInstallSourceInfo(packageName) } returns mockk { + every { installingPackageName } returns installationSource + } + bitwardenPackageManager.getPackageInstallationSourceOrNull(packageName) + verify { mockPackageManager.getInstallSourceInfo(packageName) } + } }