From fda2e169bcbbacb75a216c933f7bc76990ea64dd Mon Sep 17 00:00:00 2001 From: Mark Kim Date: Tue, 24 Oct 2023 19:23:39 +0000 Subject: [PATCH] Move launch button from 3-buttons panel to the top right corner Test: AppButtonsTest, TopBarAppLaunchButtonTest Bug: 304255179 Change-Id: Ib8ac1670e0910436f4200e2200714c65b2a593f9 --- .../settings/spa/app/appinfo/AppButtons.kt | 16 ++- .../spa/app/appinfo/AppInfoSettings.kt | 4 + .../spa/app/appinfo/TopBarAppLaunchButton.kt | 59 +++++++++ .../spa/app/appinfo/AppButtonsTest.kt | 28 ++++- .../app/appinfo/TopBarAppLaunchButtonTest.kt | 119 ++++++++++++++++++ 5 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButton.kt create mode 100644 tests/spa_unit/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButtonTest.kt diff --git a/src/com/android/settings/spa/app/appinfo/AppButtons.kt b/src/com/android/settings/spa/app/appinfo/AppButtons.kt index 3200b81934b..307ff11b5ff 100644 --- a/src/com/android/settings/spa/app/appinfo/AppButtons.kt +++ b/src/com/android/settings/spa/app/appinfo/AppButtons.kt @@ -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 @@ -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) @@ -50,7 +58,7 @@ private class AppButtonsPresenter(private val packageInfoPresenter: PackageInfoP @Composable private fun getActionButtons(app: ApplicationInfo): List = listOfNotNull( - appLaunchButton.getActionButton(app), + if (featureFlags.archiving()) null else appLaunchButton.getActionButton(app), appInstallButton.getActionButton(app), appDisableButton.getActionButton(app), appUninstallButton.getActionButton(app), diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt index a9d16ae26a5..ed912c3331c 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt @@ -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 @@ -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) } ) { diff --git a/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButton.kt b/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButton.kt new file mode 100644 index 00000000000..92ad13980a6 --- /dev/null +++ b/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButton.kt @@ -0,0 +1,59 @@ +/* + * 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 diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt index 8faf5c91fe3..e2f55ef8979 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt @@ -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 @@ -57,6 +62,8 @@ class AppButtonsTest { @Mock private lateinit var packageManager: PackageManager + private val featureFlags = FakeFeatureFlagsImpl() + @Before fun setUp() { mockSession = ExtendedMockito.mockitoSession() @@ -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 @@ -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() diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButtonTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButtonTest.kt new file mode 100644 index 00000000000..7b542478227 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButtonTest.kt @@ -0,0 +1,119 @@ +/* + * 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" + } +}