summaryrefslogtreecommitdiffhomepage
path: root/android/src
diff options
context:
space:
mode:
authorAleksandr Granin <aleksandr@mullvad.net>2021-03-22 13:27:22 +0100
committerAleksandr Granin <aleksandr@mullvad.net>2021-03-23 08:22:19 +0100
commit44fa7173f45d12e9267d15534a83d86dcf185034 (patch)
tree10c9b1330dd28e1c7cfd0019710201400ad70a89 /android/src
parentd07025afe68ff8d21a9ab51b0075d5fdb5fc47b7 (diff)
downloadmullvadvpn-44fa7173f45d12e9267d15534a83d86dcf185034.tar.xz
mullvadvpn-44fa7173f45d12e9267d15534a83d86dcf185034.zip
Create ApplicationsProvider and AppplicationsIcon cache
Diffstat (limited to 'android/src')
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppData.kt3
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManager.kt26
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProvider.kt39
-rw-r--r--android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsIconManagerTest.kt98
-rw-r--r--android/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt94
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
+ }
+}