diff --git a/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java b/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java index ca3d123d1d5..4149e23c88b 100644 --- a/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java +++ b/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java @@ -402,8 +402,7 @@ public class InteractAcrossProfilesDetails extends AppInfoBase * @return the summary for the current state of whether the app associated with the given * {@code packageName} is allowed to interact across profiles. */ - public static CharSequence getPreferenceSummary( - Context context, String packageName) { + public static String getPreferenceSummary(Context context, String packageName) { return context.getString(isInteractAcrossProfilesEnabled(context, packageName) ? R.string.interact_across_profiles_summary_allowed : R.string.interact_across_profiles_summary_not_allowed); diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt index da8b72a2d23..e3d0805434b 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt @@ -115,7 +115,7 @@ private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) { ModifySystemSettingsAppListProvider.InfoPageEntryItem(app) PictureInPictureListProvider.InfoPageEntryItem(app) InstallUnknownAppsListProvider.InfoPageEntryItem(app) - // TODO: interact_across_profiles + InteractAcrossProfilesDetailsPreference(app) AlarmsAndRemindersAppListProvider.InfoPageEntryItem(app) } diff --git a/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreference.kt b/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreference.kt new file mode 100644 index 00000000000..15d05010a05 --- /dev/null +++ b/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreference.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 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.pm.ApplicationInfo +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.android.settings.R +import com.android.settings.applications.appinfo.AppInfoDashboardFragment +import com.android.settings.applications.specialaccess.interactacrossprofiles.InteractAcrossProfilesDetails +import com.android.settingslib.spa.framework.compose.collectAsStateWithLifecycle +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spaprivileged.framework.common.crossProfileApps +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +@Composable +fun InteractAcrossProfilesDetailsPreference(app: ApplicationInfo) { + val context = LocalContext.current + val presenter = remember { InteractAcrossProfilesDetailsPresenter(context, app) } + if (!presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false).value) return + + Preference(object : PreferenceModel { + override val title = stringResource(R.string.interact_across_profiles_title) + override val summary = presenter.summaryFlow.collectAsStateWithLifecycle( + initialValue = stringResource(R.string.summary_placeholder), + ) + override val onClick = presenter::startActivity + }) +} + +private class InteractAcrossProfilesDetailsPresenter( + private val context: Context, + private val app: ApplicationInfo, +) { + private val crossProfileApps = context.crossProfileApps + + val isAvailableFlow = flow { + emit(crossProfileApps.canUserAttemptToConfigureInteractAcrossProfiles(app.packageName)) + }.flowOn(Dispatchers.IO) + + val summaryFlow = flow { + emit(InteractAcrossProfilesDetails.getPreferenceSummary(context, app.packageName)) + }.flowOn(Dispatchers.IO) + + fun startActivity() { + AppInfoDashboardFragment.startAppInfoFragment( + InteractAcrossProfilesDetails::class.java, + app, + context, + AppInfoSettingsProvider.METRICS_CATEGORY, + ) + } +} diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreferenceTest.kt new file mode 100644 index 00000000000..aeccb078840 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InteractAcrossProfilesDetailsPreferenceTest.kt @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2022 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.pm.ApplicationInfo +import android.content.pm.CrossProfileApps +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +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.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.settings.applications.appinfo.AppInfoDashboardFragment +import com.android.settings.applications.specialaccess.interactacrossprofiles.InteractAcrossProfilesDetails +import com.android.settingslib.spaprivileged.framework.common.crossProfileApps +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoSession +import org.mockito.Spy +import org.mockito.quality.Strictness +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class InteractAcrossProfilesDetailsPreferenceTest { + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var mockSession: MockitoSession + + @Spy + private val context: Context = ApplicationProvider.getApplicationContext() + + @Mock + private lateinit var crossProfileApps: CrossProfileApps + + @Before + fun setUp() { + mockSession = ExtendedMockito.mockitoSession() + .initMocks(this) + .mockStatic(InteractAcrossProfilesDetails::class.java) + .mockStatic(AppInfoDashboardFragment::class.java) + .strictness(Strictness.LENIENT) + .startMocking() + whenever(context.crossProfileApps).thenReturn(crossProfileApps) + whenever(InteractAcrossProfilesDetails.getPreferenceSummary(context, PACKAGE_NAME)) + .thenReturn("") + } + + @After + fun tearDown() { + mockSession.finishMocking() + } + + private fun mockCanConfig(canConfig: Boolean) { + whenever(crossProfileApps.canUserAttemptToConfigureInteractAcrossProfiles(PACKAGE_NAME)) + .thenReturn(canConfig) + } + + @Test + fun cannotConfig_notDisplayed() { + mockCanConfig(false) + + setContent() + + composeTestRule.onRoot().assertIsNotDisplayed() + } + + @Test + fun canConfig_displayed() { + mockCanConfig(true) + + setContent() + + composeTestRule.onNodeWithText(context.getString(R.string.interact_across_profiles_title)) + .assertIsDisplayed() + .assertIsEnabled() + } + + @Test + fun hasSummary() { + mockCanConfig(true) + whenever(InteractAcrossProfilesDetails.getPreferenceSummary(context, PACKAGE_NAME)) + .thenReturn(SUMMARY) + + setContent() + + composeTestRule.onNodeWithText(SUMMARY).assertIsDisplayed() + } + + @Test + fun whenClick_startActivity() { + mockCanConfig(true) + + setContent() + composeTestRule.onRoot().performClick() + + ExtendedMockito.verify { + AppInfoDashboardFragment.startAppInfoFragment( + InteractAcrossProfilesDetails::class.java, + APP, + context, + AppInfoSettingsProvider.METRICS_CATEGORY, + ) + } + } + + private fun setContent() { + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + InteractAcrossProfilesDetailsPreference(APP) + } + } + } + + private companion object { + const val PACKAGE_NAME = "packageName" + const val UID = 123 + const val SUMMARY = "summary" + + val APP = ApplicationInfo().apply { + packageName = PACKAGE_NAME + uid = UID + } + } +}