Loading src/com/android/settings/spa/app/appinfo/AppButtons.kt +12 −4 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.settings.spa.app.appinfo import android.content.pm.ApplicationInfo import android.content.pm.FeatureFlags import android.content.pm.FeatureFlagsImpl import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle Loading @@ -25,16 +27,22 @@ import com.android.settingslib.spa.widget.button.ActionButton import com.android.settingslib.spa.widget.button.ActionButtons @Composable fun AppButtons(packageInfoPresenter: PackageInfoPresenter) { /** * @param featureFlags can be overridden in tests */ fun AppButtons(packageInfoPresenter: PackageInfoPresenter, featureFlags: FeatureFlags = FeatureFlagsImpl()) { if (remember(packageInfoPresenter) { packageInfoPresenter.isMainlineModule() }) return val presenter = remember { AppButtonsPresenter(packageInfoPresenter) } val presenter = remember { AppButtonsPresenter(packageInfoPresenter, featureFlags) } ActionButtons(actionButtons = presenter.getActionButtons()) } private fun PackageInfoPresenter.isMainlineModule(): Boolean = AppUtils.isMainlineModule(userPackageManager, packageName) private class AppButtonsPresenter(private val packageInfoPresenter: PackageInfoPresenter) { private class AppButtonsPresenter( private val packageInfoPresenter: PackageInfoPresenter, private val featureFlags: FeatureFlags ) { private val appLaunchButton = AppLaunchButton(packageInfoPresenter) private val appInstallButton = AppInstallButton(packageInfoPresenter) private val appDisableButton = AppDisableButton(packageInfoPresenter) Loading @@ -50,7 +58,7 @@ private class AppButtonsPresenter(private val packageInfoPresenter: PackageInfoP @Composable private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull( appLaunchButton.getActionButton(app), if (featureFlags.archiving()) null else appLaunchButton.getActionButton(app), appInstallButton.getActionButton(app), appDisableButton.getActionButton(app), appUninstallButton.getActionButton(app), Loading src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt +4 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ package com.android.settings.spa.app.appinfo import android.app.settings.SettingsEnums import android.content.pm.ApplicationInfo import android.content.pm.FeatureFlags import android.content.pm.FeatureFlagsImpl import android.os.Bundle import android.os.UserHandle import android.util.FeatureFlagUtils Loading Loading @@ -119,9 +121,11 @@ private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) { LifecycleEffect(onStart = { packageInfoPresenter.reloadPackageInfo() }) val packageInfo = packageInfoPresenter.flow.collectAsStateWithLifecycle().value ?: return val app = checkNotNull(packageInfo.applicationInfo) val featureFlags: FeatureFlags = FeatureFlagsImpl() RegularScaffold( title = stringResource(R.string.application_info_label), actions = { if (featureFlags.archiving()) TopBarAppLaunchButton(packageInfoPresenter, app) AppInfoSettingsMoreOptions(packageInfoPresenter, app) } ) { Loading src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButton.kt 0 → 100644 +59 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.spa.app.appinfo import android.content.ActivityNotFoundException import android.content.Intent import android.content.pm.ApplicationInfo import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Launch import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.android.settings.R import com.android.settingslib.spaprivileged.model.app.userHandle @Composable fun TopBarAppLaunchButton(packageInfoPresenter: PackageInfoPresenter, app: ApplicationInfo) { val intent = packageInfoPresenter.launchIntent(app = app) ?: return IconButton({ launchButtonAction(intent, app, packageInfoPresenter) }) { Icon( imageVector = Icons.AutoMirrored.Outlined.Launch, contentDescription = stringResource(R.string.launch_instant_app), ) } } private fun PackageInfoPresenter.launchIntent( app: ApplicationInfo ): Intent? { return userPackageManager.getLaunchIntentForPackage(app.packageName) } private fun launchButtonAction( intent: Intent, app: ApplicationInfo, packageInfoPresenter: PackageInfoPresenter ) { try { packageInfoPresenter.context.startActivityAsUser(intent, app.userHandle) } catch (_: ActivityNotFoundException) { // Only happens after package changes like uninstall, and before page auto refresh or // close, so ignore this exception is safe. } } No newline at end of file tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt +27 −1 Original line number Diff line number Diff line Loading @@ -17,16 +17,21 @@ package com.android.settings.spa.app.appinfo import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.FakeFeatureFlagsImpl import android.content.pm.Flags import android.content.pm.PackageInfo import android.content.pm.PackageManager import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.settings.R import com.android.settingslib.applications.AppUtils import com.android.settingslib.spa.testutils.delay import kotlinx.coroutines.flow.MutableStateFlow Loading Loading @@ -57,6 +62,8 @@ class AppButtonsTest { @Mock private lateinit var packageManager: PackageManager private val featureFlags = FakeFeatureFlagsImpl() @Before fun setUp() { mockSession = ExtendedMockito.mockitoSession() Loading @@ -69,6 +76,7 @@ class AppButtonsTest { whenever(packageInfoPresenter.userPackageManager).thenReturn(packageManager) whenever(packageManager.getPackageInfo(PACKAGE_NAME, 0)).thenReturn(PACKAGE_INFO) whenever(AppUtils.isMainlineModule(packageManager, PACKAGE_NAME)).thenReturn(false) featureFlags.setFlag(Flags.FLAG_ARCHIVING, true) } @After Loading @@ -92,10 +100,28 @@ class AppButtonsTest { composeTestRule.onRoot().assertIsDisplayed() } @Test fun launchButton_displayed_archivingDisabled() { whenever(packageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(Intent()) featureFlags.setFlag(Flags.FLAG_ARCHIVING, false) setContent() composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsDisplayed() } @Test fun launchButton_notDisplayed_archivingEnabled() { whenever(packageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(Intent()) featureFlags.setFlag(Flags.FLAG_ARCHIVING, true) setContent() composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsNotDisplayed() } private fun setContent() { whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(PACKAGE_INFO)) composeTestRule.setContent { AppButtons(packageInfoPresenter) AppButtons(packageInfoPresenter, featureFlags) } composeTestRule.delay() Loading tests/spa_unit/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButtonTest.kt 0 → 100644 +119 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.spa.app.appinfo import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.performClick import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.settings.R import com.android.settingslib.spa.testutils.waitUntilExists import com.android.settingslib.spaprivileged.model.app.userHandle import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoSession import org.mockito.Spy import org.mockito.quality.Strictness import org.mockito.Mockito.`when` as whenever @RunWith(AndroidJUnit4::class) class TopBarAppLaunchButtonTest { @get:Rule val composeTestRule = createComposeRule() private lateinit var mockSession: MockitoSession @Spy private val context: Context = ApplicationProvider.getApplicationContext() @Mock private lateinit var packageInfoPresenter: PackageInfoPresenter @Mock private lateinit var userPackageManager: PackageManager @Before fun setUp() { mockSession = ExtendedMockito.mockitoSession() .initMocks(this) .strictness(Strictness.LENIENT) .startMocking() whenever(packageInfoPresenter.context).thenReturn(context) whenever(packageInfoPresenter.userPackageManager).thenReturn(userPackageManager) val intent = Intent() whenever(userPackageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(intent) } @After fun tearDown() { mockSession.finishMocking() } @Test fun topBarAppLaunchButton_isDisplayed() { val app = ApplicationInfo().apply { packageName = PACKAGE_NAME } setContent(app) composeTestRule.waitUntilExists( hasContentDescription(context.getString(R.string.launch_instant_app)) ) } @Test fun topBarAppLaunchButton_opensApp() { val app = ApplicationInfo().apply { packageName = PACKAGE_NAME } setContent(app) composeTestRule.onNodeWithContentDescription(context.getString(R.string.launch_instant_app)) .performClick() verify(context).startActivityAsUser(any(), eq(app.userHandle)) } private fun setContent(app: ApplicationInfo) { composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { TopBarAppLaunchButton(packageInfoPresenter, app) } } } private companion object { const val PACKAGE_NAME = "package.name" } } Loading
src/com/android/settings/spa/app/appinfo/AppButtons.kt +12 −4 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.settings.spa.app.appinfo import android.content.pm.ApplicationInfo import android.content.pm.FeatureFlags import android.content.pm.FeatureFlagsImpl import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle Loading @@ -25,16 +27,22 @@ import com.android.settingslib.spa.widget.button.ActionButton import com.android.settingslib.spa.widget.button.ActionButtons @Composable fun AppButtons(packageInfoPresenter: PackageInfoPresenter) { /** * @param featureFlags can be overridden in tests */ fun AppButtons(packageInfoPresenter: PackageInfoPresenter, featureFlags: FeatureFlags = FeatureFlagsImpl()) { if (remember(packageInfoPresenter) { packageInfoPresenter.isMainlineModule() }) return val presenter = remember { AppButtonsPresenter(packageInfoPresenter) } val presenter = remember { AppButtonsPresenter(packageInfoPresenter, featureFlags) } ActionButtons(actionButtons = presenter.getActionButtons()) } private fun PackageInfoPresenter.isMainlineModule(): Boolean = AppUtils.isMainlineModule(userPackageManager, packageName) private class AppButtonsPresenter(private val packageInfoPresenter: PackageInfoPresenter) { private class AppButtonsPresenter( private val packageInfoPresenter: PackageInfoPresenter, private val featureFlags: FeatureFlags ) { private val appLaunchButton = AppLaunchButton(packageInfoPresenter) private val appInstallButton = AppInstallButton(packageInfoPresenter) private val appDisableButton = AppDisableButton(packageInfoPresenter) Loading @@ -50,7 +58,7 @@ private class AppButtonsPresenter(private val packageInfoPresenter: PackageInfoP @Composable private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull( appLaunchButton.getActionButton(app), if (featureFlags.archiving()) null else appLaunchButton.getActionButton(app), appInstallButton.getActionButton(app), appDisableButton.getActionButton(app), appUninstallButton.getActionButton(app), Loading
src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt +4 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ package com.android.settings.spa.app.appinfo import android.app.settings.SettingsEnums import android.content.pm.ApplicationInfo import android.content.pm.FeatureFlags import android.content.pm.FeatureFlagsImpl import android.os.Bundle import android.os.UserHandle import android.util.FeatureFlagUtils Loading Loading @@ -119,9 +121,11 @@ private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) { LifecycleEffect(onStart = { packageInfoPresenter.reloadPackageInfo() }) val packageInfo = packageInfoPresenter.flow.collectAsStateWithLifecycle().value ?: return val app = checkNotNull(packageInfo.applicationInfo) val featureFlags: FeatureFlags = FeatureFlagsImpl() RegularScaffold( title = stringResource(R.string.application_info_label), actions = { if (featureFlags.archiving()) TopBarAppLaunchButton(packageInfoPresenter, app) AppInfoSettingsMoreOptions(packageInfoPresenter, app) } ) { Loading
src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButton.kt 0 → 100644 +59 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.spa.app.appinfo import android.content.ActivityNotFoundException import android.content.Intent import android.content.pm.ApplicationInfo import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Launch import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.android.settings.R import com.android.settingslib.spaprivileged.model.app.userHandle @Composable fun TopBarAppLaunchButton(packageInfoPresenter: PackageInfoPresenter, app: ApplicationInfo) { val intent = packageInfoPresenter.launchIntent(app = app) ?: return IconButton({ launchButtonAction(intent, app, packageInfoPresenter) }) { Icon( imageVector = Icons.AutoMirrored.Outlined.Launch, contentDescription = stringResource(R.string.launch_instant_app), ) } } private fun PackageInfoPresenter.launchIntent( app: ApplicationInfo ): Intent? { return userPackageManager.getLaunchIntentForPackage(app.packageName) } private fun launchButtonAction( intent: Intent, app: ApplicationInfo, packageInfoPresenter: PackageInfoPresenter ) { try { packageInfoPresenter.context.startActivityAsUser(intent, app.userHandle) } catch (_: ActivityNotFoundException) { // Only happens after package changes like uninstall, and before page auto refresh or // close, so ignore this exception is safe. } } No newline at end of file
tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt +27 −1 Original line number Diff line number Diff line Loading @@ -17,16 +17,21 @@ package com.android.settings.spa.app.appinfo import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.FakeFeatureFlagsImpl import android.content.pm.Flags import android.content.pm.PackageInfo import android.content.pm.PackageManager import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.settings.R import com.android.settingslib.applications.AppUtils import com.android.settingslib.spa.testutils.delay import kotlinx.coroutines.flow.MutableStateFlow Loading Loading @@ -57,6 +62,8 @@ class AppButtonsTest { @Mock private lateinit var packageManager: PackageManager private val featureFlags = FakeFeatureFlagsImpl() @Before fun setUp() { mockSession = ExtendedMockito.mockitoSession() Loading @@ -69,6 +76,7 @@ class AppButtonsTest { whenever(packageInfoPresenter.userPackageManager).thenReturn(packageManager) whenever(packageManager.getPackageInfo(PACKAGE_NAME, 0)).thenReturn(PACKAGE_INFO) whenever(AppUtils.isMainlineModule(packageManager, PACKAGE_NAME)).thenReturn(false) featureFlags.setFlag(Flags.FLAG_ARCHIVING, true) } @After Loading @@ -92,10 +100,28 @@ class AppButtonsTest { composeTestRule.onRoot().assertIsDisplayed() } @Test fun launchButton_displayed_archivingDisabled() { whenever(packageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(Intent()) featureFlags.setFlag(Flags.FLAG_ARCHIVING, false) setContent() composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsDisplayed() } @Test fun launchButton_notDisplayed_archivingEnabled() { whenever(packageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(Intent()) featureFlags.setFlag(Flags.FLAG_ARCHIVING, true) setContent() composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsNotDisplayed() } private fun setContent() { whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(PACKAGE_INFO)) composeTestRule.setContent { AppButtons(packageInfoPresenter) AppButtons(packageInfoPresenter, featureFlags) } composeTestRule.delay() Loading
tests/spa_unit/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButtonTest.kt 0 → 100644 +119 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.spa.app.appinfo import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.performClick import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.settings.R import com.android.settingslib.spa.testutils.waitUntilExists import com.android.settingslib.spaprivileged.model.app.userHandle import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoSession import org.mockito.Spy import org.mockito.quality.Strictness import org.mockito.Mockito.`when` as whenever @RunWith(AndroidJUnit4::class) class TopBarAppLaunchButtonTest { @get:Rule val composeTestRule = createComposeRule() private lateinit var mockSession: MockitoSession @Spy private val context: Context = ApplicationProvider.getApplicationContext() @Mock private lateinit var packageInfoPresenter: PackageInfoPresenter @Mock private lateinit var userPackageManager: PackageManager @Before fun setUp() { mockSession = ExtendedMockito.mockitoSession() .initMocks(this) .strictness(Strictness.LENIENT) .startMocking() whenever(packageInfoPresenter.context).thenReturn(context) whenever(packageInfoPresenter.userPackageManager).thenReturn(userPackageManager) val intent = Intent() whenever(userPackageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(intent) } @After fun tearDown() { mockSession.finishMocking() } @Test fun topBarAppLaunchButton_isDisplayed() { val app = ApplicationInfo().apply { packageName = PACKAGE_NAME } setContent(app) composeTestRule.waitUntilExists( hasContentDescription(context.getString(R.string.launch_instant_app)) ) } @Test fun topBarAppLaunchButton_opensApp() { val app = ApplicationInfo().apply { packageName = PACKAGE_NAME } setContent(app) composeTestRule.onNodeWithContentDescription(context.getString(R.string.launch_instant_app)) .performClick() verify(context).startActivityAsUser(any(), eq(app.userHandle)) } private fun setContent(app: ApplicationInfo) { composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { TopBarAppLaunchButton(packageInfoPresenter, app) } } } private companion object { const val PACKAGE_NAME = "package.name" } }