diff options
| author | Aleksandr Granin <aleksandr@mullvad.net> | 2021-03-22 13:27:22 +0100 |
|---|---|---|
| committer | Aleksandr Granin <aleksandr@mullvad.net> | 2021-03-23 08:22:19 +0100 |
| commit | 44fa7173f45d12e9267d15534a83d86dcf185034 (patch) | |
| tree | 10c9b1330dd28e1c7cfd0019710201400ad70a89 /android/src | |
| parent | d07025afe68ff8d21a9ab51b0075d5fdb5fc47b7 (diff) | |
| download | mullvadvpn-44fa7173f45d12e9267d15534a83d86dcf185034.tar.xz mullvadvpn-44fa7173f45d12e9267d15534a83d86dcf185034.zip | |
Create ApplicationsProvider and AppplicationsIcon cache
Diffstat (limited to 'android/src')
5 files changed, 260 insertions, 0 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppData.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppData.kt new file mode 100644 index 0000000000..5bed89c149 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppData.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.applist + +data class AppData(val packageName: String, val iconRes: Int, val name: String) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManager.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManager.kt new file mode 100644 index 0000000000..e266f3d044 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManager.kt @@ -0,0 +1,26 @@ +package net.mullvad.mullvadvpn.applist + +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.os.Looper +import androidx.annotation.WorkerThread +import androidx.collection.LruCache + +class ApplicationsIconManager(private val packageManager: PackageManager) { + private val iconsCache = LruCache<String, Drawable>(500) + + @WorkerThread + fun getAppIcon(packageName: String): Drawable { + check(!Looper.getMainLooper().isCurrentThread) { "Should not be called from MainThread" } + iconsCache.get(packageName)?.let { + return it + } + return packageManager.getApplicationIcon(packageName).also { + iconsCache.put(packageName, it) + } + } + + fun dispose() { + iconsCache.evictAll() + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProvider.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProvider.kt new file mode 100644 index 0000000000..a097ffd231 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProvider.kt @@ -0,0 +1,39 @@ +package net.mullvad.mullvadvpn.applist + +import android.Manifest +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager + +class ApplicationsProvider( + private val packageManager: PackageManager, + private val thisPackageName: String +) { + private val applicationFilterPredicate: (ApplicationInfo) -> Boolean = { appInfo -> + hasInternetPermission(appInfo.packageName) && + isLaunchable(appInfo.packageName) && + !isSelfApplication(appInfo.packageName) + } + + fun getAppsList(): List<AppData> { + return packageManager.getInstalledApplications(PackageManager.GET_META_DATA) + .asSequence() + .filter(applicationFilterPredicate) + .map { info -> + AppData(info.packageName, info.icon, info.loadLabel(packageManager).toString()) + } + .toList() + } + + private fun hasInternetPermission(packageName: String): Boolean { + return PackageManager.PERMISSION_GRANTED == + packageManager.checkPermission(Manifest.permission.INTERNET, packageName) + } + + private fun isLaunchable(packageName: String): Boolean { + return packageManager.getLaunchIntentForPackage(packageName) != null + } + + private fun isSelfApplication(packageName: String): Boolean { + return packageName == thisPackageName + } +} diff --git a/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt b/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt new file mode 100644 index 0000000000..6f871a28ee --- /dev/null +++ b/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt @@ -0,0 +1,98 @@ +package net.mullvad.mullvadvpn.applist + +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.os.Looper +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import kotlin.test.assertEquals +import kotlin.test.assertFails +import org.junit.After +import org.junit.Before +import org.junit.Test + +class ApplicationsIconManagerTest { + private val mockedPackageManager = mockk<PackageManager>() + private val mockedMainLooper = mockk<Looper>() + private val testSubject = ApplicationsIconManager(mockedPackageManager) + + @Before + fun setUp() { + mockkStatic(Looper::class) + every { Looper.getMainLooper() } returns mockedMainLooper + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun test_first_time_load_icon_from_PM() { + val testPackageName = "test" + val mockedDrawable = mockk<Drawable>() + every { mockedPackageManager.getApplicationIcon(testPackageName) } returns mockedDrawable + every { mockedMainLooper.isCurrentThread } returns false + + val result = testSubject.getAppIcon(testPackageName) + + assertEquals(mockedDrawable, result) + verify { + mockedMainLooper.isCurrentThread + mockedPackageManager.getApplicationIcon(testPackageName) + } + } + + @Test + fun test_second_time_load_icon_from_cache() { + val testPackageName = "test" + val mockedDrawable = mockk<Drawable>() + every { mockedPackageManager.getApplicationIcon(testPackageName) } returns mockedDrawable + every { mockedMainLooper.isCurrentThread } returns false + + val result = testSubject.getAppIcon(testPackageName) + val result2 = testSubject.getAppIcon(testPackageName) + + assertEquals(mockedDrawable, result) + assertEquals(mockedDrawable, result2) + verify(exactly = 2) { + mockedMainLooper.isCurrentThread + } + verify(exactly = 1) { + mockedPackageManager.getApplicationIcon(testPackageName) + } + } + + @Test + fun test_second_time_load_icon_from_PM_after_clear() { + val testPackageName = "test" + val mockedDrawable = mockk<Drawable>() + every { mockedPackageManager.getApplicationIcon(testPackageName) } returns mockedDrawable + every { mockedMainLooper.isCurrentThread } returns false + + val result = testSubject.getAppIcon(testPackageName) + testSubject.dispose() + val result2 = testSubject.getAppIcon(testPackageName) + + assertEquals(mockedDrawable, result) + assertEquals(mockedDrawable, result2) + verify(exactly = 2) { + mockedMainLooper.isCurrentThread + mockedPackageManager.getApplicationIcon(testPackageName) + } + } + + @Test + fun throw_exception_when_invoke_from_MainThread() { + val testPackageName = "test" + every { mockedMainLooper.isCurrentThread } returns true + + assertFails("Should not be called from MainThread") { + testSubject.getAppIcon(testPackageName) + } + verify { mockedMainLooper.isCurrentThread } + } +} diff --git a/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt b/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt new file mode 100644 index 0000000000..cb24158422 --- /dev/null +++ b/android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt @@ -0,0 +1,94 @@ +package net.mullvad.mullvadvpn.applist + +import android.Manifest +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verifyAll +import org.junit.After +import org.junit.Test + +class ApplicationsProviderTest { + private val mockedPackageManager = mockk<PackageManager>() + private val selfPackageName = "self_package_name" + private val testSubject = ApplicationsProvider(mockedPackageManager, selfPackageName) + private val internet = Manifest.permission.INTERNET + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun test_get_apps() { + val launchWithInternetPackageName = "launch_with_internet_package_name" + val launchWithoutInternetPackageName = "launch_without_internet_package_name" + val nonLaunchWithInternetPackageName = "non_launch_with_internet_package_name" + val nonLaunchWithoutInternetPackageName = "non_launch_without_internet_package_name" + + every { + mockedPackageManager.getInstalledApplications(PackageManager.GET_META_DATA) + } returns listOf( + createApplicationInfo(launchWithInternetPackageName, launch = true, internet = true), + createApplicationInfo(launchWithoutInternetPackageName, launch = true), + createApplicationInfo(nonLaunchWithInternetPackageName, internet = true), + createApplicationInfo(nonLaunchWithoutInternetPackageName), + createApplicationInfo(selfPackageName, internet = true, launch = true) + ) + + val result = testSubject.getAppsList() + val expected = listOf( + AppData(launchWithInternetPackageName, 0, launchWithInternetPackageName) + ) + assert( + expected.size == result.size && + expected.containsAll(result) && + result.containsAll(expected) + ) + + verifyAll { + mockedPackageManager.getInstalledApplications(PackageManager.GET_META_DATA) + + mockedPackageManager.checkPermission(internet, launchWithInternetPackageName) + mockedPackageManager.checkPermission(internet, launchWithoutInternetPackageName) + mockedPackageManager.checkPermission(internet, nonLaunchWithInternetPackageName) + mockedPackageManager.checkPermission(internet, nonLaunchWithoutInternetPackageName) + mockedPackageManager.checkPermission(internet, selfPackageName) + + mockedPackageManager.getLaunchIntentForPackage(launchWithInternetPackageName) + mockedPackageManager.getLaunchIntentForPackage(nonLaunchWithInternetPackageName) + mockedPackageManager.getLaunchIntentForPackage(selfPackageName) + } + } + + private fun createApplicationInfo( + packageName: String, + launch: Boolean = false, + internet: Boolean = false + ): ApplicationInfo { + val mockApplicationInfo = mockk<ApplicationInfo>() + + mockApplicationInfo.packageName = packageName + mockApplicationInfo.icon = 0 + + every { mockApplicationInfo.loadLabel(mockedPackageManager) } returns packageName + + every { + mockedPackageManager.getLaunchIntentForPackage(packageName) + } returns if (launch) + mockk() + else + null + + every { + mockedPackageManager.checkPermission(Manifest.permission.INTERNET, packageName) + } returns if (internet) + PackageManager.PERMISSION_GRANTED + else + PackageManager.PERMISSION_DENIED + + return mockApplicationInfo + } +} |
