[BWA-162] Add getPackageInstallationSourceOrNull to BitwardenPackageManager (#5418)

This commit is contained in:
Patrick Honkonen 2025-06-25 17:19:17 -04:00 committed by GitHub
parent b94d59ba6b
commit 5f525d9d95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 139 additions and 10 deletions

View File

@ -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.
*/

View File

@ -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),

View File

@ -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) }
}
}