diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ca184b18bf4..8791c920067 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -125,6 +125,7 @@ + android.uid.system:1000 + + true + false diff --git a/res/values/strings.xml b/res/values/strings.xml index f7d7e94fbd7..0fd961702ad 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -10486,8 +10486,8 @@ Reboot with MTE System will reboot and allow to experiment with Memory Tagging Extension (MTE). MTE may negatively impact system performance and stability. Will be reset on next subsequent reboot. - Try MTE for a single boot for app development. - MTE is enabled through Advanced memory protection. + Try MTE for a single boot for app development + MTE is enabled through Advanced memory protection Capturing system heap dump @@ -10627,6 +10627,14 @@ Show double-line clock when available Double-line clock + + Buttons + + + None + %1$s + %1$s, %2$s + diff --git a/res/xml/security_lockscreen_settings.xml b/res/xml/security_lockscreen_settings.xml index 3bd84f81ae3..80e8fe69d2a 100644 --- a/res/xml/security_lockscreen_settings.xml +++ b/res/xml/security_lockscreen_settings.xml @@ -69,6 +69,11 @@ android:summary="@string/lockscreen_trivial_controls_summary" settings:controller="com.android.settings.display.ControlsTrivialPrivacyPreferenceController"/> + + { + // TODO(b/258471384): open the buttons destination within wallpaper picker. + final Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER); + final String packageName = + mContext.getString(R.string.config_wallpaper_picker_package); + if (!TextUtils.isEmpty(packageName)) { + intent.setPackage(packageName); + } + mContext.startActivity(intent); + return true; + }); + refreshSummary(preference); + } + } + + @Override + public CharSequence getSummary() { + return CustomizableLockScreenUtils.getQuickAffordanceSummary(mContext); + } +} diff --git a/src/com/android/settings/display/CustomizableLockScreenUtils.java b/src/com/android/settings/display/CustomizableLockScreenUtils.java new file mode 100644 index 00000000000..14601a3655b --- /dev/null +++ b/src/com/android/settings/display/CustomizableLockScreenUtils.java @@ -0,0 +1,152 @@ +/* + * 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.display; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; + +import java.util.ArrayList; +import java.util.List; + +/** Utilities for display settings related to customizable lock screen features. */ +public final class CustomizableLockScreenUtils { + + private static final String TAG = "CustomizableLockScreenUtils"; + private static final Uri BASE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority("com.android.systemui.keyguard.quickaffordance") + .build(); + @VisibleForTesting + static final Uri FLAGS_URI = BASE_URI.buildUpon() + .path("flags") + .build(); + @VisibleForTesting + static final Uri SELECTIONS_URI = BASE_URI.buildUpon() + .path("selections") + .build(); + @VisibleForTesting + static final String NAME = "name"; + @VisibleForTesting + static final String VALUE = "value"; + @VisibleForTesting + static final String ENABLED_FLAG = "is_feature_enabled"; + @VisibleForTesting + static final String AFFORDANCE_NAME = "affordance_name"; + + private CustomizableLockScreenUtils() {} + + /** + * Queries and returns whether the customizable lock screen quick affordances feature is enabled + * on the device. + * + *

This is a slow, blocking call that shouldn't be made on the main thread. + */ + public static boolean isFeatureEnabled(Context context) { + try (Cursor cursor = context.getContentResolver().query( + FLAGS_URI, + null, + null, + null)) { + if (cursor == null) { + Log.w(TAG, "Cursor was null!"); + return false; + } + + final int indexOfNameColumn = cursor.getColumnIndex(NAME); + final int indexOfValueColumn = cursor.getColumnIndex(VALUE); + if (indexOfNameColumn == -1 || indexOfValueColumn == -1) { + Log.w(TAG, "Cursor doesn't contain " + NAME + " or " + VALUE + "!"); + return false; + } + + while (cursor.moveToNext()) { + final String name = cursor.getString(indexOfNameColumn); + final int value = cursor.getInt(indexOfValueColumn); + if (TextUtils.equals(ENABLED_FLAG, name)) { + Log.d(TAG, ENABLED_FLAG + "=" + value); + return value == 1; + } + } + + Log.w(TAG, "Flag with name \"" + ENABLED_FLAG + "\" not found!"); + return false; + } catch (Exception e) { + Log.e(TAG, "Exception while querying quick affordance content provider", e); + return false; + } + } + + /** + * Queries and returns a summary text for the currently-selected lock screen quick affordances. + * + *

This is a slow, blocking call that shouldn't be made on the main thread. + */ + @Nullable + public static CharSequence getQuickAffordanceSummary(Context context) { + try (Cursor cursor = context.getContentResolver().query( + SELECTIONS_URI, + null, + null, + null)) { + if (cursor == null) { + Log.w(TAG, "Cursor was null!"); + return null; + } + + final int columnIndex = cursor.getColumnIndex(AFFORDANCE_NAME); + if (columnIndex == -1) { + Log.w(TAG, "Cursor doesn't contain \"" + AFFORDANCE_NAME + "\" column!"); + return null; + } + + final List affordanceNames = new ArrayList<>(cursor.getCount()); + while (cursor.moveToNext()) { + final String affordanceName = cursor.getString(columnIndex); + if (!TextUtils.isEmpty(affordanceName)) { + affordanceNames.add(affordanceName); + } + } + + // We don't display more than the first two items. + final int usableAffordanceNameCount = Math.min(2, affordanceNames.size()); + final List arguments = new ArrayList<>(usableAffordanceNameCount); + if (!affordanceNames.isEmpty()) { + arguments.add(affordanceNames.get(0)); + } + if (affordanceNames.size() > 1) { + arguments.add(affordanceNames.get(1)); + } + + return context.getResources().getQuantityString( + R.plurals.lockscreen_quick_affordances_summary, + usableAffordanceNameCount, + arguments.toArray()); + } catch (Exception e) { + Log.e(TAG, "Exception while querying quick affordance content provider", e); + return null; + } + } +} diff --git a/src/com/android/settings/display/QRCodeScannerPreferenceController.java b/src/com/android/settings/display/QRCodeScannerPreferenceController.java index 16e594a62d5..cb022a74fa8 100644 --- a/src/com/android/settings/display/QRCodeScannerPreferenceController.java +++ b/src/com/android/settings/display/QRCodeScannerPreferenceController.java @@ -87,6 +87,10 @@ public class QRCodeScannerPreferenceController extends TogglePreferenceControlle @Override public int getAvailabilityStatus() { + if (CustomizableLockScreenUtils.isFeatureEnabled(mContext)) { + return UNSUPPORTED_ON_DEVICE; + } + return isScannerActivityAvailable() ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } diff --git a/src/com/android/settings/display/WalletPrivacyPreferenceController.java b/src/com/android/settings/display/WalletPrivacyPreferenceController.java index 92580f3d569..fe14a40a01a 100644 --- a/src/com/android/settings/display/WalletPrivacyPreferenceController.java +++ b/src/com/android/settings/display/WalletPrivacyPreferenceController.java @@ -62,6 +62,10 @@ public class WalletPrivacyPreferenceController extends TogglePreferenceControlle @Override public int getAvailabilityStatus() { + if (CustomizableLockScreenUtils.isFeatureEnabled(mContext)) { + return UNSUPPORTED_ON_DEVICE; + } + return isEnabled() && isSecure() ? AVAILABLE : DISABLED_DEPENDENT_SETTING; } diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java index d4c00a44cd2..5fd3905649f 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java @@ -43,7 +43,6 @@ import com.android.settings.fuelgauge.batterytip.tips.HighUsageTip; import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip; import com.android.settings.fuelgauge.batterytip.tips.UnrestrictAppTip; -import java.text.NumberFormat; import java.util.List; /** @@ -142,29 +141,6 @@ public class BatteryTipDialogFragment extends InstrumentedDialogFragment impleme .setPositiveButton(R.string.battery_tip_unrestrict_app_dialog_ok, this) .setNegativeButton(R.string.battery_tip_unrestrict_app_dialog_cancel, null) .create(); - case BatteryTip.TipType.BATTERY_DEFENDER: - mMetricsFeatureProvider.action(context, - SettingsEnums.ACTION_TIP_BATTERY_DEFENDER, mMetricsKey); - final double chargeLimitLevel = 0.8f; - final String percentage = - NumberFormat.getPercentInstance().format(chargeLimitLevel); - final String message = context.getString( - R.string.battery_tip_limited_temporarily_dialog_msg, percentage); - final boolean isPluggedIn = isPluggedIn(); - final AlertDialog.Builder dialogBuilder = - new AlertDialog.Builder(context) - .setTitle(R.string.battery_tip_limited_temporarily_title) - .setMessage(message); - if (isPluggedIn) { - dialogBuilder - .setPositiveButton( - R.string.battery_tip_limited_temporarily_dialog_resume_charge, - this) - .setNegativeButton(R.string.okay, null); - } else { - dialogBuilder.setPositiveButton(R.string.okay, null); - } - return dialogBuilder.create(); default: throw new IllegalArgumentException("unknown type " + mBatteryTip.getType()); } diff --git a/src/com/android/settings/fuelgauge/batterytip/actions/BatteryDefenderAction.java b/src/com/android/settings/fuelgauge/batterytip/actions/BatteryDefenderAction.java deleted file mode 100644 index 824b6bee5ca..00000000000 --- a/src/com/android/settings/fuelgauge/batterytip/actions/BatteryDefenderAction.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2020 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.fuelgauge.batterytip.actions; - -import android.content.Intent; - -import com.android.settings.SettingsActivity; -import com.android.settings.overlay.FeatureFactory; - -/** - * Action to open the Support Center article - */ -public class BatteryDefenderAction extends BatteryTipAction { - private SettingsActivity mSettingsActivity; - - public BatteryDefenderAction(SettingsActivity settingsActivity) { - super(settingsActivity.getApplicationContext()); - mSettingsActivity = settingsActivity; - } - - @Override - public void handlePositiveAction(int metricsKey) { - final Intent intent = FeatureFactory.getFactory(mContext) - .getPowerUsageFeatureProvider(mContext).getResumeChargeIntent(); - if (intent != null) { - mContext.sendBroadcast(intent); - } - } -} diff --git a/src/com/android/settings/notification/RedactionInterstitial.java b/src/com/android/settings/notification/RedactionInterstitial.java index f243250a611..d6fdaf81c95 100644 --- a/src/com/android/settings/notification/RedactionInterstitial.java +++ b/src/com/android/settings/notification/RedactionInterstitial.java @@ -189,13 +189,16 @@ public class RedactionInterstitial extends SettingsActivity { } private void loadFromSettings() { + final boolean showUnRedactedDefault = getContext().getResources().getBoolean( + R.bool.default_allow_sensitive_lockscreen_content); final boolean managedProfile = UserManager.get(getContext()).isManagedProfile(mUserId); // Hiding all notifications is device-wide setting, managed profiles can only set // whether their notifications are show in full or redacted. final boolean showNotifications = managedProfile || Settings.Secure.getIntForUser( getContentResolver(), LOCK_SCREEN_SHOW_NOTIFICATIONS, 0, mUserId) != 0; final boolean showUnredacted = Settings.Secure.getIntForUser( - getContentResolver(), LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1, mUserId) != 0; + getContentResolver(), LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, + showUnRedactedDefault ? 1 : 0, mUserId) != 0; int checkedButtonId = R.id.hide_all; if (showNotifications) { diff --git a/tests/robotests/src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceControllerTest.java new file mode 100644 index 00000000000..8597d6490af --- /dev/null +++ b/tests/robotests/src/com/android/settings/display/CustomizableLockScreenQuickAffordancesPreferenceControllerTest.java @@ -0,0 +1,153 @@ +/* + * 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.display; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.MatrixCursor; +import android.text.TextUtils; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.filters.SmallTest; + +import com.android.settings.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@SmallTest +@RunWith(RobolectricTestRunner.class) +public class CustomizableLockScreenQuickAffordancesPreferenceControllerTest { + + private static final String KEY = "key"; + + @Mock private Context mContext; + @Mock private ContentResolver mContentResolver; + + private CustomizableLockScreenQuickAffordancesPreferenceController mUnderTest; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + when(mContext.getResources()) + .thenReturn(ApplicationProvider.getApplicationContext().getResources()); + + mUnderTest = new CustomizableLockScreenQuickAffordancesPreferenceController(mContext, KEY); + } + + @Test + public void getAvailabilityStatus_whenEnabled() { + setEnabled(true); + + assertThat(mUnderTest.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailabilityStatus_whenNotEnabled() { + setEnabled(false); + + assertThat(mUnderTest.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void displayPreference_click() { + setSelectedAffordanceNames("one", "two"); + final PreferenceScreen screen = mock(PreferenceScreen.class); + final Preference preference = mock(Preference.class); + when(screen.findPreference(KEY)).thenReturn(preference); + + mUnderTest.displayPreference(screen); + + final ArgumentCaptor clickCaptor = + ArgumentCaptor.forClass(Preference.OnPreferenceClickListener.class); + verify(preference).setOnPreferenceClickListener(clickCaptor.capture()); + + clickCaptor.getValue().onPreferenceClick(preference); + + final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(preference).setOnPreferenceClickListener(clickCaptor.capture()); + verify(mContext).startActivity(intentCaptor.capture()); + assertThat(intentCaptor.getValue().getPackage()).isEqualTo( + mContext.getString(R.string.config_wallpaper_picker_package)); + assertThat(intentCaptor.getValue().getAction()).isEqualTo(Intent.ACTION_SET_WALLPAPER); + } + + @Test + public void getSummary_whenNoneAreSelected() { + setSelectedAffordanceNames(); + + assertThat(mUnderTest.getSummary()).isNull(); + } + + @Test + public void getSummary_whenOneIsSelected() { + setSelectedAffordanceNames("one"); + + assertThat(TextUtils.equals(mUnderTest.getSummary(), "one")).isTrue(); + } + + @Test + public void getSummary_whenTwoAreSelected() { + setSelectedAffordanceNames("one", "two"); + + assertThat(TextUtils.equals(mUnderTest.getSummary(), "one, two")).isTrue(); + } + + private void setEnabled(boolean isEnabled) { + final MatrixCursor cursor = new MatrixCursor( + new String[] { + CustomizableLockScreenUtils.NAME, + CustomizableLockScreenUtils.VALUE + }); + cursor.addRow(new Object[] { CustomizableLockScreenUtils.ENABLED_FLAG, isEnabled ? 1 : 0 }); + when( + mContentResolver.query( + CustomizableLockScreenUtils.FLAGS_URI, null, null, null)) + .thenReturn(cursor); + } + + private void setSelectedAffordanceNames(String... affordanceNames) { + final MatrixCursor cursor = new MatrixCursor( + new String[] { CustomizableLockScreenUtils.AFFORDANCE_NAME }); + for (final String name : affordanceNames) { + cursor.addRow(new Object[] { name }); + } + + when( + mContentResolver.query( + CustomizableLockScreenUtils.SELECTIONS_URI, null, null, null)) + .thenReturn(cursor); + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragmentTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragmentTest.java index db1159e1bcf..c5d66a60b45 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragmentTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragmentTest.java @@ -244,20 +244,4 @@ public class BatteryTipDialogFragmentTest { assertThat(shadowDialog.getMessage()).isEqualTo( mContext.getText(R.string.battery_tip_dialog_summary_message)); } - - @Test - public void testOnCreateDialog_defenderTip_fireDialog() { - mDialogFragment = BatteryTipDialogFragment.newInstance(mDefenderTip, METRICS_KEY); - - FragmentController.setupFragment(mDialogFragment, FragmentActivity.class, - 0 /* containerViewId */, null /* bundle */); - - final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); - ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog); - - assertThat(shadowDialog.getTitle()).isEqualTo( - mContext.getString(R.string.battery_tip_limited_temporarily_title)); - assertThat(shadowDialog.getMessage()).isEqualTo( - mContext.getString(R.string.battery_tip_limited_temporarily_dialog_msg, "80%")); - } } diff --git a/tests/robotests/src/com/android/settings/notification/RedactionInterstitialTest.java b/tests/robotests/src/com/android/settings/notification/RedactionInterstitialTest.java index 5c6da495235..9d475b84524 100644 --- a/tests/robotests/src/com/android/settings/notification/RedactionInterstitialTest.java +++ b/tests/robotests/src/com/android/settings/notification/RedactionInterstitialTest.java @@ -21,6 +21,7 @@ import android.widget.RadioButton; import com.android.settings.R; import com.android.settings.RestrictedRadioButton; import com.android.settings.notification.RedactionInterstitial.RedactionInterstitialFragment; +import com.android.settings.testutils.shadow.SettingsShadowResources; import com.android.settings.testutils.shadow.ShadowRestrictedLockUtilsInternal; import com.android.settings.testutils.shadow.ShadowUtils; @@ -38,6 +39,7 @@ import org.robolectric.shadows.ShadowUserManager; @Config(shadows = { ShadowUtils.class, ShadowRestrictedLockUtilsInternal.class, + SettingsShadowResources.class, }) public class RedactionInterstitialTest { private RedactionInterstitial mActivity; @@ -134,6 +136,28 @@ public class RedactionInterstitialTest { assertSelectedButton(R.id.redact_sensitive); } + @Test + public void defaultShowSensitiveContent_configDeny() { + final ContentResolver resolver = RuntimeEnvironment.application.getContentResolver(); + Settings.Secure.putIntForUser(resolver, + LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, UserHandle.myUserId()); + setupConfig(false); + setupActivity(); + + assertSelectedButton(R.id.redact_sensitive); + } + + @Test + public void defaultShowSensitiveContent_configAllow() { + final ContentResolver resolver = RuntimeEnvironment.application.getContentResolver(); + Settings.Secure.putIntForUser(resolver, + LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, UserHandle.myUserId()); + setupConfig(true); + setupActivity(); + + assertSelectedButton(R.id.show_all); + } + private void setupActivity() { mActivity = buildActivity(RedactionInterstitial.class, new Intent()).setup().get(); mFragment = (RedactionInterstitialFragment) @@ -142,6 +166,11 @@ public class RedactionInterstitialTest { assertThat(mFragment).isNotNull(); } + private void setupConfig(boolean allowSensitiveContent) { + SettingsShadowResources.overrideResource( + R.bool.default_allow_sensitive_lockscreen_content, allowSensitiveContent); + } + private void setupSettings(int show, int showUnredacted) { final ContentResolver resolver = RuntimeEnvironment.application.getContentResolver(); Settings.Secure.putIntForUser(resolver, diff --git a/tests/spa_unit/Android.bp b/tests/spa_unit/Android.bp index da334116004..0bd561348ef 100644 --- a/tests/spa_unit/Android.bp +++ b/tests/spa_unit/Android.bp @@ -30,13 +30,11 @@ android_test { static_libs: [ "Settings-core", + "SpaLibTestUtils", "androidx.compose.runtime_runtime", - "androidx.compose.ui_ui-test-junit4", - "androidx.compose.ui_ui-test-manifest", "androidx.test.ext.junit", "androidx.test.runner", "mockito-target-extended-minus-junit4", - "truth-prebuilt", ], jni_libs: [ "libdexmakerjvmtiagent", diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppAllServicesPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppAllServicesPreferenceTest.kt index 9846e3f7ff1..bd73d8bcce7 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppAllServicesPreferenceTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppAllServicesPreferenceTest.kt @@ -30,6 +30,7 @@ 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.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot @@ -38,6 +39,8 @@ import androidx.core.os.bundleOf import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.R +import com.android.settingslib.spa.testutils.delay +import com.android.settingslib.spa.testutils.waitUntilExists import com.android.settingslib.spaprivileged.model.app.userHandle import com.android.settingslib.spaprivileged.model.app.userId import com.google.common.truth.Truth.assertThat @@ -140,7 +143,7 @@ class AppAllServicesPreferenceTest { setContent() - composeTestRule.onNodeWithText(SUMMARY).assertIsDisplayed() + composeTestRule.waitUntilExists(hasText(SUMMARY)) } @Test @@ -149,6 +152,7 @@ class AppAllServicesPreferenceTest { setContent() composeTestRule.onRoot().performClick() + composeTestRule.delay() val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) verify(context).startActivityAsUser(intentCaptor.capture(), eq(APP.userHandle)) 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 d71b4eeceb4..0ca4f67f800 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 @@ -31,8 +31,8 @@ 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.testutils.delay import com.android.settingslib.applications.AppUtils +import com.android.settingslib.spa.testutils.delay import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn import org.junit.After diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppDataUsagePreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppDataUsagePreferenceTest.kt index 174f5085864..b6816e65242 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppDataUsagePreferenceTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppDataUsagePreferenceTest.kt @@ -30,7 +30,6 @@ 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.compose.ui.test.printToLog import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.dx.mockito.inline.extended.ExtendedMockito @@ -39,9 +38,9 @@ import com.android.settings.R import com.android.settings.Utils import com.android.settings.applications.appinfo.AppInfoDashboardFragment import com.android.settings.datausage.AppDataUsage -import com.android.settings.testutils.waitUntilExists import com.android.settingslib.net.NetworkCycleDataForUid import com.android.settingslib.net.NetworkCycleDataForUidLoader +import com.android.settingslib.spa.testutils.waitUntilExists import org.junit.After import org.junit.Before import org.junit.Rule @@ -134,9 +133,7 @@ class AppDataUsagePreferenceTest { setContent() - composeTestRule.onRoot().printToLog("AAA") - composeTestRule.onNodeWithText(context.getString(R.string.no_data_usage)) - .assertIsDisplayed() + composeTestRule.waitUntilExists(hasText(context.getString(R.string.no_data_usage))) } @Test diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppDisableButtonTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppDisableButtonTest.kt index f35810ff858..beb6abc592d 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppDisableButtonTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppDisableButtonTest.kt @@ -61,7 +61,7 @@ class AppDisableButtonTest { private lateinit var devicePolicyManager: DevicePolicyManager private val fakeFeatureFactory = FakeFeatureFactory() - private val appFeatureProvider = fakeFeatureFactory.applicationFeatureProvider + private val appFeatureProvider = fakeFeatureFactory.mockApplicationFeatureProvider private lateinit var appDisableButton: AppDisableButton diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt index b66967a96c2..53a18683a7d 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt @@ -38,8 +38,8 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.settings.R import com.android.settings.Utils import com.android.settings.applications.AppStoreUtil -import com.android.settings.testutils.waitUntilExists import com.android.settingslib.applications.AppUtils +import com.android.settingslib.spa.testutils.waitUntilExists import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.model.app.userHandle import org.junit.After diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppLocalePreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppLocalePreferenceTest.kt index 60c4f79ad9e..39524df7c16 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppLocalePreferenceTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppLocalePreferenceTest.kt @@ -24,6 +24,7 @@ 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.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot @@ -36,6 +37,8 @@ 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.testutils.delay +import com.android.settingslib.spa.testutils.waitUntilExists import com.android.settingslib.spaprivileged.model.app.userHandle import com.google.common.truth.Truth.assertThat import org.junit.After @@ -103,15 +106,16 @@ class AppLocalePreferenceTest { composeTestRule.onNodeWithText(context.getString(R.string.app_locale_preference_title)) .assertIsDisplayed() - composeTestRule.onNodeWithText(SUMMARY).assertIsDisplayed() + composeTestRule.waitUntilExists(hasText(SUMMARY)) } @Test - fun whenCanDisplayLocalUi_click_startsActivity() { + fun whenCanDisplayLocalUi_click_startActivity() { doNothing().`when`(context).startActivityAsUser(any(), any()) setContent() composeTestRule.onRoot().performClick() + composeTestRule.delay() val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) verify(context).startActivityAsUser(intentCaptor.capture(), eq(APP.userHandle)) diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppSettingsPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppSettingsPreferenceTest.kt index 1184ee7270a..35811e251c2 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppSettingsPreferenceTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppSettingsPreferenceTest.kt @@ -29,18 +29,14 @@ 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.assertIsNotEnabled -import androidx.compose.ui.test.hasText 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.compose.ui.test.printToLog import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.R -import com.android.settings.testutils.waitUntilExists -import com.android.settingslib.applications.AppUtils +import com.android.settingslib.spa.testutils.delay import com.android.settingslib.spaprivileged.model.app.userHandle import com.android.settingslib.spaprivileged.model.app.userId import com.google.common.truth.Truth.assertThat @@ -49,7 +45,6 @@ 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.eq @@ -135,6 +130,7 @@ class AppSettingsPreferenceTest { setContent() composeTestRule.onRoot().performClick() + composeTestRule.delay() val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) verify(context).startActivityAsUser(intentCaptor.capture(), eq(APP.userHandle)) diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreferenceTest.kt index e3fcdd904ae..b2ff4f2e8a8 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreferenceTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppTimeSpentPreferenceTest.kt @@ -63,7 +63,7 @@ class AppTimeSpentPreferenceTest { private lateinit var packageManager: PackageManager private val fakeFeatureFactory = FakeFeatureFactory() - private val appFeatureProvider = fakeFeatureFactory.applicationFeatureProvider + private val appFeatureProvider = fakeFeatureFactory.mockApplicationFeatureProvider @Before fun setUp() { diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt index 9782817f742..1a78c4974e3 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt @@ -24,10 +24,6 @@ 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.filterToOne -import androidx.compose.ui.test.hasAnyAncestor -import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot @@ -37,7 +33,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.settings.R import com.android.settings.Utils -import com.android.settings.testutils.delay +import com.android.settingslib.spa.testutils.delay +import com.android.settingslib.spa.testutils.onDialogText import org.junit.After import org.junit.Before import org.junit.Rule @@ -141,18 +138,14 @@ class InstantAppDomainsPreferenceTest { composeTestRule.onRoot().performClick() composeTestRule.delay() - assertDialogHasText(context.getString(R.string.app_launch_supported_domain_urls_title)) - assertDialogHasText("abc") - assertDialogHasText("def") + composeTestRule.onDialogText( + context.getString(R.string.app_launch_supported_domain_urls_title) + ).assertIsDisplayed() + composeTestRule.onDialogText("abc").assertIsDisplayed() + composeTestRule.onDialogText("def").assertIsDisplayed() } - private fun assertDialogHasText(text: String) { - composeTestRule.onAllNodes(hasAnyAncestor(isDialog())) - .filterToOne(hasText(text)) - .assertIsDisplayed() - } - - private fun setContent(app:ApplicationInfo = INSTANT_APP) { + private fun setContent(app: ApplicationInfo = INSTANT_APP) { composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { InstantAppDomainsPreference(app) 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 index aeccb078840..f4489c6f5e7 100644 --- 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 @@ -24,6 +24,7 @@ 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.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot @@ -34,6 +35,8 @@ 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.spa.testutils.delay +import com.android.settingslib.spa.testutils.waitUntilExists import com.android.settingslib.spaprivileged.framework.common.crossProfileApps import org.junit.After import org.junit.Before @@ -110,7 +113,7 @@ class InteractAcrossProfilesDetailsPreferenceTest { setContent() - composeTestRule.onNodeWithText(SUMMARY).assertIsDisplayed() + composeTestRule.waitUntilExists(hasText(SUMMARY)) } @Test @@ -119,6 +122,7 @@ class InteractAcrossProfilesDetailsPreferenceTest { setContent() composeTestRule.onRoot().performClick() + composeTestRule.delay() ExtendedMockito.verify { AppInfoDashboardFragment.startAppInfoFragment( diff --git a/tests/spa_unit/src/com/android/settings/testutils/ComposeContentTestRuleExt.kt b/tests/spa_unit/src/com/android/settings/testutils/ComposeContentTestRuleExt.kt deleted file mode 100644 index 82df9cff4dd..00000000000 --- a/tests/spa_unit/src/com/android/settings/testutils/ComposeContentTestRuleExt.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.testutils - -import androidx.compose.ui.test.ComposeTimeoutException -import androidx.compose.ui.test.SemanticsMatcher -import androidx.compose.ui.test.junit4.ComposeContentTestRule - -/** Blocks until the found a semantics node that match the given condition. */ -fun ComposeContentTestRule.waitUntilExists(matcher: SemanticsMatcher) = waitUntil { - onAllNodes(matcher).fetchSemanticsNodes().isNotEmpty() -} - -/** Blocks until the timeout is reached. */ -fun ComposeContentTestRule.delay(timeoutMillis: Long = 1_000) = try { - waitUntil(timeoutMillis) { false } -} catch (_: ComposeTimeoutException) { - // Expected -} diff --git a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt index 054b4150e8e..a544f533dbe 100644 --- a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt +++ b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt @@ -51,7 +51,9 @@ import org.mockito.Mockito.mock class FakeFeatureFactory : FeatureFactory() { - val applicationFeatureProvider: ApplicationFeatureProvider = + private val mockMetricsFeatureProvider: MetricsFeatureProvider = + mock(MetricsFeatureProvider::class.java) + val mockApplicationFeatureProvider: ApplicationFeatureProvider = mock(ApplicationFeatureProvider::class.java) init { @@ -70,9 +72,7 @@ class FakeFeatureFactory : FeatureFactory() { TODO("Not yet implemented") } - override fun getMetricsFeatureProvider(): MetricsFeatureProvider { - TODO("Not yet implemented") - } + override fun getMetricsFeatureProvider(): MetricsFeatureProvider = mockMetricsFeatureProvider override fun getPowerUsageFeatureProvider(context: Context?): PowerUsageFeatureProvider { TODO("Not yet implemented") @@ -96,7 +96,7 @@ class FakeFeatureFactory : FeatureFactory() { TODO("Not yet implemented") } - override fun getApplicationFeatureProvider(context: Context?) = applicationFeatureProvider + override fun getApplicationFeatureProvider(context: Context?) = mockApplicationFeatureProvider override fun getLocaleFeatureProvider(): LocaleFeatureProvider { TODO("Not yet implemented") diff --git a/tests/unit/src/com/android/settings/biometrics2/OWNERS b/tests/unit/src/com/android/settings/biometrics2/OWNERS new file mode 100644 index 00000000000..a257ed861ad --- /dev/null +++ b/tests/unit/src/com/android/settings/biometrics2/OWNERS @@ -0,0 +1 @@ +include /src/com/android/settings/biometrics/OWNERS