From 3eddcc6703dc942b7d640a6b71041e1ce1c881c1 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 25 Oct 2022 20:22:15 +0800 Subject: [PATCH] Add AppLocalePreference for Spa Also let SettingsSpaUnitTests depends on the mockito-target-extended-minus-junit4 to support mock static methods. Bug: 236346018 Test: Unit test & Manual with Settings App Change-Id: Ib7a3022b20e30c8292713f52d29785eb78899c70 --- .../appinfo/AppLocaleDetails.java | 7 +- .../AppLocalePreferenceController.java | 2 +- .../ManageApplications.java | 2 +- .../spa/app/appinfo/AppInfoSettings.kt | 2 +- .../spa/app/appinfo/AppLocalePreference.kt | 88 +++++++++++ tests/spa_unit/Android.bp | 3 +- .../spa/app/appinfo/AppLocalePresenterTest.kt | 139 ++++++++++++++++++ 7 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 src/com/android/settings/spa/app/appinfo/AppLocalePreference.kt create mode 100644 tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppLocalePresenterTest.kt diff --git a/src/com/android/settings/applications/appinfo/AppLocaleDetails.java b/src/com/android/settings/applications/appinfo/AppLocaleDetails.java index 6ada41bec97..a73d658caad 100644 --- a/src/com/android/settings/applications/appinfo/AppLocaleDetails.java +++ b/src/com/android/settings/applications/appinfo/AppLocaleDetails.java @@ -42,7 +42,6 @@ import com.android.settings.applications.AppInfoBase; import com.android.settings.applications.AppLocaleUtil; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.applications.AppUtils; -import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.widget.LayoutPreference; import java.util.Locale; @@ -206,10 +205,10 @@ public class AppLocaleDetails extends SettingsPreferenceFragment { * TODO (b209962418) Do a performance test to low end device. * @return Return the summary to show the current app's language. */ - public static CharSequence getSummary(Context context, AppEntry entry) { - final UserHandle userHandle = UserHandle.getUserHandleForUid(entry.info.uid); + public static CharSequence getSummary(Context context, ApplicationInfo app) { + final UserHandle userHandle = UserHandle.getUserHandleForUid(app.uid); final Context contextAsUser = context.createContextAsUser(userHandle, 0); - Locale appLocale = getAppDefaultLocale(contextAsUser, entry.info.packageName); + Locale appLocale = getAppDefaultLocale(contextAsUser, app.packageName); if (appLocale == null) { return context.getString(R.string.preference_of_system_locale_summary); } else { diff --git a/src/com/android/settings/applications/appinfo/AppLocalePreferenceController.java b/src/com/android/settings/applications/appinfo/AppLocalePreferenceController.java index 6bf94a61200..30fe1c4363a 100644 --- a/src/com/android/settings/applications/appinfo/AppLocalePreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppLocalePreferenceController.java @@ -60,7 +60,7 @@ public class AppLocalePreferenceController extends AppInfoPreferenceControllerBa @Override public CharSequence getSummary() { - return AppLocaleDetails.getSummary(mContext, mParent.getAppEntry()); + return AppLocaleDetails.getSummary(mContext, mParent.getAppEntry().info); } @Override diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index 927619a341a..bb73dd8fc62 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -1707,7 +1707,7 @@ public class ManageApplications extends InstrumentedFragment holder.setSummary(MediaManagementAppsDetails.getSummary(mContext, entry)); break; case LIST_TYPE_APPS_LOCALE: - holder.setSummary(AppLocaleDetails.getSummary(mContext, entry)); + holder.setSummary(AppLocaleDetails.getSummary(mContext, entry.info)); break; case LIST_TYPE_BATTERY_OPTIMIZATION: holder.setSummary(null); diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt index a772dbf36fb..2e3e45f4f8f 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt @@ -96,7 +96,7 @@ private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) { // TODO: data_settings AppTimeSpentPreference(app) // TODO: battery - // TODO: app_language_setting + AppLocalePreference(app) AppOpenByDefaultPreference(app) DefaultAppShortcuts(app) diff --git a/src/com/android/settings/spa/app/appinfo/AppLocalePreference.kt b/src/com/android/settings/spa/app/appinfo/AppLocalePreference.kt new file mode 100644 index 00000000000..68308d72ece --- /dev/null +++ b/src/com/android/settings/spa/app/appinfo/AppLocalePreference.kt @@ -0,0 +1,88 @@ +/* + * 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.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ResolveInfoFlags +import android.net.Uri +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.AppInfoBase +import com.android.settings.applications.AppLocaleUtil +import com.android.settings.applications.appinfo.AppLocaleDetails +import com.android.settings.localepicker.AppLocalePickerActivity +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.model.app.userHandle +import com.android.settingslib.spaprivileged.model.app.userId +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withContext + +@Composable +fun AppLocalePreference(app: ApplicationInfo) { + val context = LocalContext.current + val presenter = remember { AppLocalePresenter(context, app) } + if (!presenter.isAvailableFlow.collectAsStateWithLifecycle(initialValue = false).value) return + + Preference(object : PreferenceModel { + override val title = stringResource(R.string.app_locale_preference_title) + override val summary = presenter.summaryFlow.collectAsStateWithLifecycle( + initialValue = stringResource(R.string.summary_placeholder), + ) + override val onClick = presenter::startActivity + }) +} + +private class AppLocalePresenter( + private val context: Context, + private val app: ApplicationInfo, +) { + private val packageManager = context.packageManager + + val isAvailableFlow = flow { emit(isAvailable()) } + + private suspend fun isAvailable(): Boolean = withContext(Dispatchers.IO) { + val resolveInfos = packageManager.queryIntentActivitiesAsUser( + AppLocaleUtil.LAUNCHER_ENTRY_INTENT, + ResolveInfoFlags.of(PackageManager.GET_META_DATA.toLong()), + app.userId, + ) + AppLocaleUtil.canDisplayLocaleUi(context, app.packageName, resolveInfos) + } + + val summaryFlow = flow { emit(getSummary()) } + + private suspend fun getSummary() = withContext(Dispatchers.IO) { + AppLocaleDetails.getSummary(context, app).toString() + } + + fun startActivity() { + val intent = Intent(context, AppLocalePickerActivity::class.java).apply { + data = Uri.parse("package:" + app.packageName) + putExtra(AppInfoBase.ARG_PACKAGE_UID, app.uid) + } + context.startActivityAsUser(intent, app.userHandle) + } +} diff --git a/tests/spa_unit/Android.bp b/tests/spa_unit/Android.bp index 9126c55a55c..0678168bdb8 100644 --- a/tests/spa_unit/Android.bp +++ b/tests/spa_unit/Android.bp @@ -35,11 +35,12 @@ android_test { "androidx.compose.ui_ui-test-manifest", "androidx.test.ext.junit", "androidx.test.runner", - "mockito-target-inline-minus-junit4", + "mockito-target-extended-minus-junit4", "truth-prebuilt", ], jni_libs: [ "libdexmakerjvmtiagent", + "libstaticjvmtiagent", ], kotlincflags: [ "-Xjvm-default=all", diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppLocalePresenterTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppLocalePresenterTest.kt new file mode 100644 index 00000000000..d330a9200e2 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppLocalePresenterTest.kt @@ -0,0 +1,139 @@ +/* + * 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.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.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.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.mockitoSession +import com.android.settings.R +import com.android.settings.applications.AppInfoBase +import com.android.settings.applications.AppLocaleUtil +import com.android.settings.applications.appinfo.AppLocaleDetails +import com.android.settings.localepicker.AppLocalePickerActivity +import com.android.settingslib.spaprivileged.model.app.userHandle +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.eq +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 AppLocalePresenterTest { + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var mockSession: MockitoSession + + @Spy + private val context: Context = ApplicationProvider.getApplicationContext() + + @Mock + private lateinit var packageManager: PackageManager + + @Before + fun setUp() { + mockSession = mockitoSession() + .initMocks(this) + .mockStatic(AppLocaleUtil::class.java) + .mockStatic(AppLocaleDetails::class.java) + .strictness(Strictness.LENIENT) + .startMocking() + whenever(context.packageManager).thenReturn(packageManager) + whenever(AppLocaleUtil.canDisplayLocaleUi(any(), eq(PACKAGE_NAME), any())).thenReturn(true) + whenever(AppLocaleDetails.getSummary(any(), ArgumentMatchers.eq(APP))).thenReturn(SUMMARY) + } + + @After + fun tearDown() { + mockSession.finishMocking() + } + + @Test + fun whenCanNotDisplayLocalUi_notDisplayed() { + whenever(AppLocaleUtil.canDisplayLocaleUi(any(), eq(PACKAGE_NAME), any())).thenReturn(false) + + setContent() + + composeTestRule.onRoot().assertIsNotDisplayed() + } + + @Test + fun whenCanDisplayLocalUi_displayed() { + setContent() + + composeTestRule.onNodeWithText(context.getString(R.string.app_locale_preference_title)) + .assertIsDisplayed() + composeTestRule.onNodeWithText(SUMMARY).assertIsDisplayed() + } + + @Test + fun whenCanDisplayLocalUi_click_startsActivity() { + doNothing().`when`(context).startActivityAsUser(any(), any()) + + setContent() + composeTestRule.onRoot().performClick() + + val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) + verify(context).startActivityAsUser(intentCaptor.capture(), eq(APP.userHandle)) + val intent = intentCaptor.value + assertThat(intent.component?.className) + .isEqualTo(AppLocalePickerActivity::class.qualifiedName) + assertThat(intent.getIntExtra(AppInfoBase.ARG_PACKAGE_UID, -1)).isEqualTo(UID) + } + + private fun setContent() { + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + AppLocalePreference(APP) + } + } + } + + private companion object { + const val PACKAGE_NAME = "packageName" + const val UID = 123 + val APP = ApplicationInfo().apply { + packageName = PACKAGE_NAME + uid = UID + } + const val SUMMARY = "summary" + } +}