From b8ab650afe7da211c5327153f1ef3336ae7bcf16 Mon Sep 17 00:00:00 2001 From: Diya Bera Date: Tue, 27 Aug 2024 14:10:17 +0000 Subject: [PATCH 01/12] Add mandatory biometric for exported enroll intents Flag: android.hardware.biometrics.flags.mandatory_biometrics Fixes: 358173662 Test: N/A Change-Id: Ic67d42ba84220f4c18cf445ae75d4f7f8dfd1f39 --- src/com/android/settings/Utils.java | 29 +++++++++++++++++-- .../biometrics/BiometricEnrollActivity.java | 16 ++++++++++ .../biometrics/BiometricEnrollBase.java | 2 +- .../BiometricEnrollIntroduction.java | 14 +++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index 3646938e1f8..62a4ce36753 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -1543,19 +1543,42 @@ public final class Utils extends com.android.settingslib.Utils { */ public static void launchBiometricPromptForMandatoryBiometrics(@NonNull Fragment fragment, int requestCode, int userId, boolean hideBackground) { + fragment.startActivityForResult(getIntentForBiometricAuthentication(fragment.getResources(), + userId, hideBackground), requestCode); + } + + /** + * Launch biometric prompt for mandatory biometrics. Call + * {@link #requestBiometricAuthenticationForMandatoryBiometrics(Context, boolean, int)} + * to check if all requirements for mandatory biometrics is satisfied + * before launching biometric prompt. + * + * @param activity corresponding activity of the surface + * @param requestCode for starting the new activity + * @param userId user id for the authentication request + * @param hideBackground if the background activity screen needs to be hidden + */ + public static void launchBiometricPromptForMandatoryBiometrics(@NonNull Activity activity, + int requestCode, int userId, boolean hideBackground) { + activity.startActivityForResult(getIntentForBiometricAuthentication( + activity.getResources(), userId, hideBackground), requestCode); + } + + private static Intent getIntentForBiometricAuthentication(Resources resources, int userId, + boolean hideBackground) { final Intent intent = new Intent(); intent.putExtra(BIOMETRIC_PROMPT_AUTHENTICATORS, BiometricManager.Authenticators.MANDATORY_BIOMETRICS); intent.putExtra(BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT, - fragment.getString(R.string.cancel)); + resources.getString(R.string.cancel)); intent.putExtra(KeyguardManager.EXTRA_DESCRIPTION, - fragment.getString(R.string.mandatory_biometrics_prompt_description)); + resources.getString(R.string.mandatory_biometrics_prompt_description)); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_ALLOW_ANY_USER, true); intent.putExtra(EXTRA_USER_ID, userId); intent.putExtra(BIOMETRIC_PROMPT_HIDE_BACKGROUND, hideBackground); intent.setClassName(SETTINGS_PACKAGE_NAME, ConfirmDeviceCredentialActivity.InternalActivity.class.getName()); - fragment.startActivityForResult(intent, requestCode); + return intent; } private static void disableComponent(PackageManager pm, ComponentName componentName) { diff --git a/src/com/android/settings/biometrics/BiometricEnrollActivity.java b/src/com/android/settings/biometrics/BiometricEnrollActivity.java index c9616f1802c..dbbe30beaa5 100644 --- a/src/com/android/settings/biometrics/BiometricEnrollActivity.java +++ b/src/com/android/settings/biometrics/BiometricEnrollActivity.java @@ -19,6 +19,7 @@ package com.android.settings.biometrics; import static android.provider.Settings.ACTION_BIOMETRIC_ENROLL; import static android.provider.Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED; +import static com.android.settings.biometrics.BiometricEnrollBase.BIOMETRIC_AUTH_REQUEST; import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_CONSENT_DENIED; import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_CONSENT_GRANTED; @@ -51,6 +52,7 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.SetupWizardUtils; +import com.android.settings.Utils; import com.android.settings.core.InstrumentedActivity; import com.android.settings.overlay.FeatureFactory; import com.android.settings.password.ChooseLockGeneric; @@ -442,6 +444,16 @@ public class BiometricEnrollActivity extends InstrumentedActivity { if (!mParentalConsentHelper.launchNext(this, REQUEST_CHOOSE_OPTIONS)) { Log.e(TAG, "Nothing to prompt for consent (no modalities enabled)!"); finish(); + } else { + final Utils.BiometricStatus biometricStatus = + Utils.requestBiometricAuthenticationForMandatoryBiometrics(this, + false /* biometricsAuthenticationRequested */, mUserId); + if (biometricStatus == Utils.BiometricStatus.OK) { + Utils.launchBiometricPromptForMandatoryBiometrics(this, + BIOMETRIC_AUTH_REQUEST, mUserId, true /* hideBackground */); + } else if (biometricStatus != Utils.BiometricStatus.NOT_ACTIVE) { + finish(); + } } } else { Log.d(TAG, "Unknown result for set/choose lock: " + resultCode); @@ -473,6 +485,10 @@ public class BiometricEnrollActivity extends InstrumentedActivity { finish(); } break; + case BIOMETRIC_AUTH_REQUEST: + if (resultCode != RESULT_OK) { + finish(); + } default: Log.w(TAG, "Unknown consenting requestCode: " + requestCode + ", finishing"); finish(); diff --git a/src/com/android/settings/biometrics/BiometricEnrollBase.java b/src/com/android/settings/biometrics/BiometricEnrollBase.java index 37ada236343..3285a9537a6 100644 --- a/src/com/android/settings/biometrics/BiometricEnrollBase.java +++ b/src/com/android/settings/biometrics/BiometricEnrollBase.java @@ -117,7 +117,6 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity { public static final int LEARN_MORE_REQUEST = 3; public static final int CONFIRM_REQUEST = 4; public static final int ENROLL_REQUEST = 5; - public static final int BIOMETRIC_AUTH_REQUEST = 6; /** * Request code when starting another biometric enrollment from within a biometric flow. For @@ -125,6 +124,7 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity { */ public static final int ENROLL_NEXT_BIOMETRIC_REQUEST = 6; public static final int REQUEST_POSTURE_GUIDANCE = 7; + public static final int BIOMETRIC_AUTH_REQUEST = 8; protected boolean mLaunchedConfirmLock; protected boolean mLaunchedPostureGuidance; diff --git a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java index 5ebe9c7e515..6f9b94a7fba 100644 --- a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java +++ b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java @@ -36,6 +36,7 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.SetupWizardUtils; +import com.android.settings.Utils; import com.android.settings.password.ChooseLockGeneric; import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settings.password.SetupSkipDialog; @@ -417,6 +418,15 @@ public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase getNextButton().setEnabled(true); })); } + final Utils.BiometricStatus biometricStatus = + Utils.requestBiometricAuthenticationForMandatoryBiometrics(this, + false /* biometricsAuthenticationRequested */, mUserId); + if (biometricStatus == Utils.BiometricStatus.OK) { + Utils.launchBiometricPromptForMandatoryBiometrics(this, + BIOMETRIC_AUTH_REQUEST, mUserId, true /* hideBackground */); + } else if (biometricStatus != Utils.BiometricStatus.NOT_ACTIVE) { + finish(); + } } else { setResult(resultCode, data); finish(); @@ -445,6 +455,10 @@ public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase setResult(resultCode, data); finish(); } + } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { + if (resultCode != RESULT_OK) { + finish(); + } } super.onActivityResult(requestCode, resultCode, data); } From 23601a670d4ff41a9f4a3ce076ac0377c648f87b Mon Sep 17 00:00:00 2001 From: Yiyi Shen Date: Tue, 27 Aug 2024 13:40:00 +0800 Subject: [PATCH 02/12] Register callbacks on background thread to avoid ANR Fix: 360798156 Test: atest Flag: EXEMPT small fix Change-Id: Ia40bbe33952fa7ea2b951c04cf47b4a05f641e26 --- .../FastPairDevicePreferenceController.java | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceController.java b/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceController.java index a1c6d184814..2df7d11788a 100644 --- a/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceController.java +++ b/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceController.java @@ -36,6 +36,7 @@ import com.android.settings.connecteddevice.DevicePreferenceCallback; import com.android.settings.core.BasePreferenceController; import com.android.settings.flags.Flags; import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.utils.ThreadUtils; import java.util.ArrayList; import java.util.Collections; @@ -90,28 +91,12 @@ public class FastPairDevicePreferenceController extends BasePreferenceController @Override public void onStart(@NonNull LifecycleOwner owner) { - if (mFastPairDeviceUpdater != null) { - mFastPairDeviceUpdater.setPreferenceContext(mContext); - mFastPairDeviceUpdater.registerCallback(); - } else { - if (DEBUG) { - Log.d(TAG, "Callback register: Fast Pair device updater is null. Ignore."); - } - } - mContext.registerReceiver(mReceiver, mIntentFilter, Context.RECEIVER_EXPORTED_UNAUDITED); + var unused = ThreadUtils.postOnBackgroundThread(() -> registerCallbacks()); } @Override public void onStop(@NonNull LifecycleOwner owner) { - if (mFastPairDeviceUpdater != null) { - mFastPairDeviceUpdater.setPreferenceContext(null); - mFastPairDeviceUpdater.unregisterCallback(); - } else { - if (DEBUG) { - Log.d(TAG, "Callback unregister: Fast Pair device updater is null. Ignore."); - } - } - mContext.unregisterReceiver(mReceiver); + var unused = ThreadUtils.postOnBackgroundThread(() -> unregisterCallbacks()); } @Override @@ -208,4 +193,28 @@ public class FastPairDevicePreferenceController extends BasePreferenceController mSeeAllPreference.setVisible(false); } } + + private void registerCallbacks() { + if (mFastPairDeviceUpdater != null) { + mFastPairDeviceUpdater.setPreferenceContext(mContext); + mFastPairDeviceUpdater.registerCallback(); + } else { + if (DEBUG) { + Log.d(TAG, "Callback register: Fast Pair device updater is null. Ignore."); + } + } + mContext.registerReceiver(mReceiver, mIntentFilter, Context.RECEIVER_EXPORTED_UNAUDITED); + } + + private void unregisterCallbacks() { + if (mFastPairDeviceUpdater != null) { + mFastPairDeviceUpdater.setPreferenceContext(null); + mFastPairDeviceUpdater.unregisterCallback(); + } else { + if (DEBUG) { + Log.d(TAG, "Callback unregister: Fast Pair device updater is null. Ignore."); + } + } + mContext.unregisterReceiver(mReceiver); + } } From dc68fd425381357b04b987861fcad5a1ddcaf4a3 Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Wed, 28 Aug 2024 14:23:53 +0800 Subject: [PATCH 03/12] Clean up settings subsequent pairing aconfig flag BUG: 362649019 Test: atest FastPairDeviceGroupControllerTest Flag: EXEMPT clean up Change-Id: Ia49b0e4e9e5b9af7fe33b2182344f7a43786f111 --- ..._connecteddevice_flag_declarations.aconfig | 9 +-- .../FastPairDeviceGroupController.java | 15 ++--- .../FastPairDevicePreferenceController.java | 14 ++--- .../ConnectedDeviceDashboardFragmentTest.java | 2 - .../FastPairDeviceGroupControllerTest.java | 59 ------------------- ...astPairDevicePreferenceControllerTest.java | 51 ---------------- 6 files changed, 10 insertions(+), 140 deletions(-) diff --git a/aconfig/settings_connecteddevice_flag_declarations.aconfig b/aconfig/settings_connecteddevice_flag_declarations.aconfig index b2550965371..ee0c233614f 100644 --- a/aconfig/settings_connecteddevice_flag_declarations.aconfig +++ b/aconfig/settings_connecteddevice_flag_declarations.aconfig @@ -1,13 +1,6 @@ package: "com.android.settings.flags" container: "system_ext" -flag { - name: "enable_subsequent_pair_settings_integration" - namespace: "pixel_cross_device_control" - description: "Gates whether to enable subsequent pair Settings integration." - bug: "299405720" -} - flag { name: "rotation_connected_display_setting" namespace: "display_manager" @@ -47,4 +40,4 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -} \ No newline at end of file +} diff --git a/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupController.java b/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupController.java index 52ad91c4f70..6b77b4795e8 100644 --- a/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupController.java +++ b/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupController.java @@ -35,7 +35,6 @@ import androidx.preference.PreferenceScreen; import com.android.settings.connecteddevice.DevicePreferenceCallback; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.flags.Flags; import com.android.settings.overlay.FeatureFactory; /** @@ -66,15 +65,11 @@ public class FastPairDeviceGroupController extends BasePreferenceController public FastPairDeviceGroupController(Context context) { super(context, KEY); - if (Flags.enableSubsequentPairSettingsIntegration()) { - FastPairFeatureProvider fastPairFeatureProvider = - FeatureFactory.getFeatureFactory().getFastPairFeatureProvider(); - mFastPairDeviceUpdater = - fastPairFeatureProvider.getFastPairDeviceUpdater(context, this); - } else { - Log.d(TAG, "Flag disabled. Ignored."); - mFastPairDeviceUpdater = null; - } + FastPairFeatureProvider fastPairFeatureProvider = + FeatureFactory.getFeatureFactory().getFastPairFeatureProvider(); + mFastPairDeviceUpdater = + fastPairFeatureProvider.getFastPairDeviceUpdater(context, this); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); } diff --git a/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceController.java b/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceController.java index a1c6d184814..80e9f24624a 100644 --- a/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceController.java +++ b/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceController.java @@ -34,7 +34,6 @@ import androidx.preference.PreferenceScreen; import com.android.settings.connecteddevice.DevicePreferenceCallback; import com.android.settings.core.BasePreferenceController; -import com.android.settings.flags.Flags; import com.android.settings.overlay.FeatureFactory; import java.util.ArrayList; @@ -75,15 +74,10 @@ public class FastPairDevicePreferenceController extends BasePreferenceController public FastPairDevicePreferenceController(Context context, String preferenceKey) { super(context, preferenceKey); - if (Flags.enableSubsequentPairSettingsIntegration()) { - FastPairFeatureProvider fastPairFeatureProvider = - FeatureFactory.getFeatureFactory().getFastPairFeatureProvider(); - mFastPairDeviceUpdater = - fastPairFeatureProvider.getFastPairDeviceUpdater(context, this); - } else { - Log.d(TAG, "Flag disabled. Ignore."); - mFastPairDeviceUpdater = null; - } + FastPairFeatureProvider fastPairFeatureProvider = + FeatureFactory.getFeatureFactory().getFastPairFeatureProvider(); + mFastPairDeviceUpdater = + fastPairFeatureProvider.getFastPairDeviceUpdater(context, this); mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragmentTest.java index 33292af8cb5..ecf6d003cef 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragmentTest.java @@ -36,7 +36,6 @@ import com.android.settings.R; import com.android.settings.connecteddevice.fastpair.FastPairDeviceUpdater; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerListHelper; -import com.android.settings.flags.Flags; import com.android.settings.slices.SlicePreferenceController; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; @@ -94,7 +93,6 @@ public class ConnectedDeviceDashboardFragmentTest { mContext = spy(RuntimeEnvironment.application); mFragment = new ConnectedDeviceDashboardFragment(); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION); mSetFlagsRule.enableFlags(com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING); mFeatureFactory = FakeFeatureFactory.setupForTest(); when(mFeatureFactory diff --git a/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupControllerTest.java index 25e927b14e2..2b140ea4e1e 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupControllerTest.java @@ -33,8 +33,6 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Looper; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -45,7 +43,6 @@ import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.flags.Flags; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.widget.GearPreference; @@ -107,7 +104,6 @@ public class FastPairDeviceGroupControllerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) public void onStart_flagOn_registerCallback() { // register the callback in onStart() mFastPairDeviceGroupController.onStart(mLifecycleOwner); @@ -120,7 +116,6 @@ public class FastPairDeviceGroupControllerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) public void onStop_flagOn_unregisterCallback() { // register broadcast first mContext.registerReceiver( @@ -133,51 +128,6 @@ public class FastPairDeviceGroupControllerTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) - public void onStart_flagOff_registerCallback() { - // register the callback in onStart() - mFastPairDeviceGroupController.onStart(mLifecycleOwner); - assertThat(mFastPairDeviceUpdater).isNull(); - verify(mContext) - .registerReceiver( - mFastPairDeviceGroupController.mReceiver, - mFastPairDeviceGroupController.mIntentFilter, - Context.RECEIVER_EXPORTED); - } - - @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) - public void onStop_flagOff_unregisterCallback() { - // register broadcast first - mContext.registerReceiver( - mFastPairDeviceGroupController.mReceiver, null, Context.RECEIVER_EXPORTED); - - // unregister the callback in onStop() - mFastPairDeviceGroupController.onStop(mLifecycleOwner); - assertThat(mFastPairDeviceUpdater).isNull(); - verify(mContext).unregisterReceiver(mFastPairDeviceGroupController.mReceiver); - } - - @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) - public void getAvailabilityStatus_noFastPairFeature_returnUnSupported() { - doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); - - assertThat(mFastPairDeviceGroupController.getAvailabilityStatus()) - .isEqualTo(UNSUPPORTED_ON_DEVICE); - } - - @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) - public void getAvailabilityStatus_noBluetoothFastPairFeature_returnUnSupported() { - doReturn(false).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); - - assertThat(mFastPairDeviceGroupController.getAvailabilityStatus()) - .isEqualTo(UNSUPPORTED_ON_DEVICE); - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) public void getAvailabilityStatus_noBluetoothFeature_returnUnSupported() { doReturn(false).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); @@ -186,7 +136,6 @@ public class FastPairDeviceGroupControllerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) public void getAvailabilityStatus_withBluetoothFastPairFeature_returnSupported() { doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); @@ -231,14 +180,6 @@ public class FastPairDeviceGroupControllerTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) - public void displayPreference_notAvailable_doNothing() { - mFastPairDeviceGroupController.displayPreference(mScreen); - assertThat(mPreferenceGroup.isVisible()).isFalse(); - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) public void displayPreference_isAvailable_fetchFastPairDevices() { doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceControllerTest.java index be2ed564811..8a84e2399f2 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceControllerTest.java @@ -32,8 +32,6 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Looper; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -44,7 +42,6 @@ import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceManager; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.flags.Flags; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.widget.GearPreference; @@ -108,7 +105,6 @@ public class FastPairDevicePreferenceControllerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) public void onStart_flagOn_registerCallback() { // register the callback in onStart() mFastPairDevicePrefController.onStart(mLifecycleOwner); @@ -121,20 +117,6 @@ public class FastPairDevicePreferenceControllerTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) - public void onStart_flagOff_registerCallback() { - // register the callback in onStart() - mFastPairDevicePrefController.onStart(mLifecycleOwner); - assertThat(mFastPairDeviceUpdater).isNull(); - verify(mContext) - .registerReceiver( - mFastPairDevicePrefController.mReceiver, - mFastPairDevicePrefController.mIntentFilter, - Context.RECEIVER_EXPORTED_UNAUDITED); - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) public void onStop_flagOn_unregisterCallback() { // register broadcast first mContext.registerReceiver( @@ -147,20 +129,6 @@ public class FastPairDevicePreferenceControllerTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) - public void onStop_flagOff_unregisterCallback() { - // register broadcast first - mContext.registerReceiver( - mFastPairDevicePrefController.mReceiver, null, Context.RECEIVER_EXPORTED_UNAUDITED); - - // unregister the callback in onStop() - mFastPairDevicePrefController.onStop(mLifecycleOwner); - assertThat(mFastPairDeviceUpdater).isNull(); - verify(mContext).unregisterReceiver(mFastPairDevicePrefController.mReceiver); - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) public void getAvailabilityStatus_noBluetoothFeature_returnUnsupported() { doReturn(false).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); @@ -169,25 +137,6 @@ public class FastPairDevicePreferenceControllerTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) - public void getAvailabilityStatus_noFastPairFeature_returnUnsupported() { - doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); - - assertThat(mFastPairDevicePrefController.getAvailabilityStatus()) - .isEqualTo(UNSUPPORTED_ON_DEVICE); - } - - @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) - public void getAvailabilityStatus_noBluetoothFastPairFeature_returnUnsupported() { - doReturn(false).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); - - assertThat(mFastPairDevicePrefController.getAvailabilityStatus()) - .isEqualTo(UNSUPPORTED_ON_DEVICE); - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) public void getAvailabilityStatus_bothBluetoothFastPairFeature_returnSupported() { doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); From 34cf0cbea1c521ef8fa5c7a0d31f1de1a84508f5 Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Mon, 26 Aug 2024 11:21:49 +0800 Subject: [PATCH 04/12] Show a dialog if bluetooth key is missing when reconnecting BUG: 360031750 Test: atest BluetoothKeyMissingDialogTest Flag: com.android.settings.flags.enable_bluetooth_key_missing_dialog Change-Id: Iab75675b4cd8fec7ab55516cd0f057a9c7d92f7e --- AndroidManifest.xml | 20 +++ .../settings_bluetooth_declarations.aconfig | 10 ++ res/layout/bluetooth_key_missing.xml | 58 +++++++ res/values/strings.xml | 9 + .../bluetooth/BluetoothKeyMissingDialog.java | 47 +++++ .../BluetoothKeyMissingDialogFragment.java | 94 ++++++++++ .../BluetoothKeyMissingReceiver.java | 122 +++++++++++++ .../BluetoothKeyMissingDialogTest.java | 76 +++++++++ .../BluetoothKeyMissingReceiverTest.java | 160 ++++++++++++++++++ 9 files changed, 596 insertions(+) create mode 100644 res/layout/bluetooth_key_missing.xml create mode 100644 src/com/android/settings/bluetooth/BluetoothKeyMissingDialog.java create mode 100644 src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java create mode 100644 src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogTest.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiverTest.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 7b79611ca65..13aafc9dbb7 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3230,6 +3230,19 @@ + + + + + + + + + + + + + diff --git a/aconfig/settings_bluetooth_declarations.aconfig b/aconfig/settings_bluetooth_declarations.aconfig index 3d14288fcd2..0c423b5a1b7 100644 --- a/aconfig/settings_bluetooth_declarations.aconfig +++ b/aconfig/settings_bluetooth_declarations.aconfig @@ -34,3 +34,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_bluetooth_key_missing_dialog" + namespace: "cross_device_experiences" + description: "Show a dialog if the bluetooth key is missing when reconnecting" + bug: "360031750" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/res/layout/bluetooth_key_missing.xml b/res/layout/bluetooth_key_missing.xml new file mode 100644 index 00000000000..b9f8d866bd3 --- /dev/null +++ b/res/layout/bluetooth_key_missing.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 0fcb0d68502..17a9a08f48d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1860,6 +1860,15 @@ Change + + %1$s not connected + + For your security, forget this device, then pair it again + + Forget device + + Cancel + Device details diff --git a/src/com/android/settings/bluetooth/BluetoothKeyMissingDialog.java b/src/com/android/settings/bluetooth/BluetoothKeyMissingDialog.java new file mode 100644 index 00000000000..46975f77726 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothKeyMissingDialog.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 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.bluetooth; + +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; + +import android.bluetooth.BluetoothDevice; +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; + +/** A dialog to ask the user to forget a bluetooth device when the key is missing. */ +public class BluetoothKeyMissingDialog extends FragmentActivity { + public static final String FRAGMENT_TAG = "BtKeyMissingFrg"; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); + Intent intent = getIntent(); + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (device == null) { + finish(); + return; + } + BluetoothKeyMissingDialogFragment fragment = new BluetoothKeyMissingDialogFragment(device); + fragment.show(getSupportFragmentManager(), FRAGMENT_TAG); + closeSystemDialogs(); + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java b/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java new file mode 100644 index 00000000000..a8e3aae175a --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 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.bluetooth; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothDevice; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * A dialogFragment used by {@link BluetoothKeyMissingDialog} to create a dialog for the + * bluetooth device. + */ +public class BluetoothKeyMissingDialogFragment extends InstrumentedDialogFragment + implements OnClickListener { + + private static final String TAG = "BTKeyMissingDialogFragment"; + + private BluetoothDevice mBluetoothDevice; + + public BluetoothKeyMissingDialogFragment(@NonNull BluetoothDevice bluetoothDevice) { + mBluetoothDevice = bluetoothDevice; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + View view = getActivity().getLayoutInflater().inflate(R.layout.bluetooth_key_missing, null); + TextView keyMissingTitle = view.findViewById(R.id.bluetooth_key_missing_title); + keyMissingTitle.setText( + getString(R.string.bluetooth_key_missing_title, mBluetoothDevice.getName())); + builder.setView(view); + builder.setPositiveButton(getString(R.string.bluetooth_key_missing_forget), this); + builder.setNegativeButton(getString(R.string.bluetooth_key_missing_cancel), this); + AlertDialog dialog = builder.create(); + dialog.setCanceledOnTouchOutside(false); + return dialog; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (!getActivity().isFinishing()) { + getActivity().finish(); + } + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + Log.i( + TAG, + "Positive button clicked, remove bond for " + + mBluetoothDevice.getAnonymizedAddress()); + mBluetoothDevice.removeBond(); + } else if (which == DialogInterface.BUTTON_NEGATIVE) { + Log.i(TAG, "Negative button clicked for " + mBluetoothDevice.getAnonymizedAddress()); + } + if (!getActivity().isFinishing()) { + getActivity().finish(); + } + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.BLUETOOTH_KEY_MISSING_DIALOG_FRAGMENT; + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java b/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java new file mode 100644 index 00000000000..d7a5343d694 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024 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.bluetooth; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.PowerManager; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; + +import androidx.core.app.NotificationCompat; + +import com.android.settings.R; +import com.android.settings.flags.Flags; + +/** + * BluetoothKeyMissingReceiver is a receiver for Bluetooth key missing error when reconnecting to a + * bonded bluetooth device. + */ +public final class BluetoothKeyMissingReceiver extends BroadcastReceiver { + private static final String TAG = "BtKeyMissingReceiver"; + private static final String CHANNEL_ID = "bluetooth_notification_channel"; + private static final int NOTIFICATION_ID = android.R.drawable.stat_sys_data_bluetooth; + + @Override + public void onReceive(Context context, Intent intent) { + if (!Flags.enableBluetoothKeyMissingDialog()) { + return; + } + String action = intent.getAction(); + if (action == null) { + return; + } + + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + PowerManager powerManager = context.getSystemService(PowerManager.class); + if (TextUtils.equals(action, BluetoothDevice.ACTION_KEY_MISSING)) { + Log.d(TAG, "Receive ACTION_KEY_MISSING"); + if (shouldShowDialog(context, device, powerManager)) { + Intent pairingIntent = getKeyMissingDialogIntent(context, device); + Log.d(TAG, "Show key missing dialog:" + device); + context.startActivityAsUser(pairingIntent, UserHandle.CURRENT); + } else { + Log.d(TAG, "Show key missing notification: " + device); + showNotification(context, device); + } + } + } + + private Intent getKeyMissingDialogIntent(Context context, BluetoothDevice device) { + Intent pairingIntent = new Intent(); + pairingIntent.setClass(context, BluetoothKeyMissingDialog.class); + pairingIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + pairingIntent.setAction(BluetoothDevice.ACTION_KEY_MISSING); + pairingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return pairingIntent; + } + + private boolean shouldShowDialog( + Context context, BluetoothDevice device, PowerManager powerManager) { + return LocalBluetoothPreferences.shouldShowDialogInForeground(context, device) + && powerManager.isInteractive(); + } + + private void showNotification(Context context, BluetoothDevice bluetoothDevice) { + NotificationManager nm = context.getSystemService(NotificationManager.class); + NotificationChannel notificationChannel = + new NotificationChannel( + CHANNEL_ID, + context.getString(R.string.bluetooth), + NotificationManager.IMPORTANCE_HIGH); + nm.createNotificationChannel(notificationChannel); + + PendingIntent pairIntent = + PendingIntent.getActivity( + context, + 0, + getKeyMissingDialogIntent(context, bluetoothDevice), + PendingIntent.FLAG_ONE_SHOT + | PendingIntent.FLAG_UPDATE_CURRENT + | PendingIntent.FLAG_IMMUTABLE); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, + CHANNEL_ID) + .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) + .setTicker(context.getString(R.string.bluetooth_notif_ticker)) + .setLocalOnly(true); + builder.setContentTitle( + context.getString( + R.string.bluetooth_key_missing_title, bluetoothDevice.getName())) + .setContentText(context.getString(R.string.bluetooth_key_missing_message)) + .setContentIntent(pairIntent) + .setAutoCancel(true) + .setDefaults(Notification.DEFAULT_SOUND) + .setColor( + context.getColor( + com.android.internal.R.color.system_notification_accent_color)); + + nm.notify(NOTIFICATION_ID, builder.build()); + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogTest.java new file mode 100644 index 00000000000..a47101e7b79 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 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.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; + +import android.bluetooth.BluetoothDevice; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentActivity; + +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowAlertDialogCompat.class) +public class BluetoothKeyMissingDialogTest { + @Mock private BluetoothDevice mBluetoothDevice; + + private BluetoothKeyMissingDialogFragment mFragment = null; + private FragmentActivity mActivity = null; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mActivity = Robolectric.setupActivity(FragmentActivity.class); + mFragment = new BluetoothKeyMissingDialogFragment(mBluetoothDevice); + mActivity + .getSupportFragmentManager() + .beginTransaction() + .add(mFragment, null) + .commit(); + shadowMainLooper().idle(); + } + + @Test + public void clickForgetDevice_removeBond() { + mFragment.onClick(mFragment.getDialog(), AlertDialog.BUTTON_POSITIVE); + + verify(mBluetoothDevice).removeBond(); + assertThat(mActivity.isFinishing()).isTrue(); + } + + @Test + public void clickCancel_notRemoveBond() { + mFragment.onClick(mFragment.getDialog(), AlertDialog.BUTTON_NEGATIVE); + + verify(mBluetoothDevice, never()).removeBond(); + assertThat(mActivity.isFinishing()).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiverTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiverTest.java new file mode 100644 index 00000000000..c764ed6cd97 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiverTest.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2024 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.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.app.NotificationManager; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import com.android.settings.flags.Flags; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +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.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowApplication; + +import java.util.List; +import java.util.stream.Collectors; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class, ShadowBluetoothUtils.class}) +public class BluetoothKeyMissingReceiverTest { + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + private ShadowApplication mShadowApplication; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + @Mock private LocalBluetoothManager mLocalBtManager; + @Mock private NotificationManager mNm; + @Mock private BluetoothDevice mBluetoothDevice; + + @Before + public void setUp() { + mContext = spy(RuntimeEnvironment.getApplication()); + mShadowApplication = Shadow.extract(mContext); + mShadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm); + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setEnabled(true); + ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; + } + + @After + public void tearDown() { + ShadowBluetoothUtils.reset(); + } + + @Test + public void broadcastReceiver_isRegistered() { + List registeredReceivers = + mShadowApplication.getRegisteredReceivers(); + + int matchedCount = + registeredReceivers.stream() + .filter( + receiver -> + BluetoothKeyMissingReceiver.class + .getSimpleName() + .equals( + receiver.broadcastReceiver + .getClass() + .getSimpleName())) + .collect(Collectors.toList()) + .size(); + assertThat(matchedCount).isEqualTo(1); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_BLUETOOTH_KEY_MISSING_DIALOG) + public void broadcastReceiver_receiveKeyMissingIntentFlagOff_doNothing() { + Intent intent = spy(new Intent(BluetoothDevice.ACTION_KEY_MISSING)); + when(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)).thenReturn(mBluetoothDevice); + BluetoothKeyMissingReceiver bluetoothKeyMissingReceiver = getReceiver(intent); + bluetoothKeyMissingReceiver.onReceive(mContext, intent); + + verifyNoInteractions(mNm); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_BLUETOOTH_KEY_MISSING_DIALOG) + public void broadcastReceiver_background_showNotification() { + Intent intent = spy(new Intent(BluetoothDevice.ACTION_KEY_MISSING)); + when(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)).thenReturn(mBluetoothDevice); + BluetoothKeyMissingReceiver bluetoothKeyMissingReceiver = getReceiver(intent); + bluetoothKeyMissingReceiver.onReceive(mContext, intent); + + verify(mNm).notify(eq(android.R.drawable.stat_sys_data_bluetooth), any(Notification.class)); + verify(mContext, never()).startActivityAsUser(any(), any()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_BLUETOOTH_KEY_MISSING_DIALOG) + public void broadcastReceiver_foreground_receiveKeyMissingIntent_showDialog() { + when(mLocalBtManager.isForegroundActivity()).thenReturn(true); + Intent intent = spy(new Intent(BluetoothDevice.ACTION_KEY_MISSING)); + when(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)).thenReturn(mBluetoothDevice); + BluetoothKeyMissingReceiver bluetoothKeyMissingReceiver = getReceiver(intent); + bluetoothKeyMissingReceiver.onReceive(mContext, intent); + + verifyNoInteractions(mNm); + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mContext).startActivityAsUser(captor.capture(), eq(UserHandle.CURRENT)); + assertThat(captor.getValue().getComponent().getClassName()) + .isEqualTo(BluetoothKeyMissingDialog.class.getName()); + } + + private BluetoothKeyMissingReceiver getReceiver(Intent intent) { + assertThat(mShadowApplication.hasReceiverForIntent(intent)).isTrue(); + List receiversForIntent = + mShadowApplication.getReceiversForIntent(intent); + assertThat(receiversForIntent).hasSize(1); + BroadcastReceiver broadcastReceiver = receiversForIntent.get(0); + assertThat(broadcastReceiver).isInstanceOf(BluetoothKeyMissingReceiver.class); + return (BluetoothKeyMissingReceiver) broadcastReceiver; + } +} From 468e15f49a95d634b1be1419600e836c3b1f3d18 Mon Sep 17 00:00:00 2001 From: Chris Antol Date: Thu, 1 Aug 2024 09:01:49 +0000 Subject: [PATCH 05/12] Supported updated suggestion cards in AOSP Settings Flag: com.android.settings.flags.updated_suggestion_card_aosp Bug: 323258154 Test: Enable flag and trigger suggestion cards to show Change-Id: Iee47d05d8d75c10ba073ae3541f108bc37b4c09b --- aconfig/settings_flag_declarations.aconfig | 7 + res/drawable/suggestion_icon_background.xml | 23 ++ res/layout/suggestion_tile.xml | 96 +++++++ res/values/dimens.xml | 1 + res/values/styles.xml | 18 ++ .../SuggestionFeatureProvider.java | 17 +- .../SuggestionFeatureProviderImpl.java | 11 +- .../suggestions/SuggestionFragment.kt | 238 ++++++++++++++++++ .../homepage/SettingsHomepageActivity.java | 5 +- .../SuggestionFeatureProviderImplTest.java | 26 ++ .../SettingsHomepageActivityTest.java | 2 +- 11 files changed, 438 insertions(+), 6 deletions(-) create mode 100644 res/drawable/suggestion_icon_background.xml create mode 100644 res/layout/suggestion_tile.xml create mode 100644 src/com/android/settings/dashboard/suggestions/SuggestionFragment.kt diff --git a/aconfig/settings_flag_declarations.aconfig b/aconfig/settings_flag_declarations.aconfig index 0007bbffb75..983d4b3e6d9 100644 --- a/aconfig/settings_flag_declarations.aconfig +++ b/aconfig/settings_flag_declarations.aconfig @@ -56,3 +56,10 @@ flag { description: "This flag controls the About phone > Legal information page migration" bug: "323791114" } + +flag { + name: "updated_suggestion_card_aosp" + namespace: "android_settings" + description: "Use updated suggestion card(s) in AOSP Settings" + bug: "323258154" +} diff --git a/res/drawable/suggestion_icon_background.xml b/res/drawable/suggestion_icon_background.xml new file mode 100644 index 00000000000..a6dee4836cf --- /dev/null +++ b/res/drawable/suggestion_icon_background.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/res/layout/suggestion_tile.xml b/res/layout/suggestion_tile.xml new file mode 100644 index 00000000000..fcb0d4f78a1 --- /dev/null +++ b/res/layout/suggestion_tile.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 689262fa3e8..2a9b4334e29 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -297,6 +297,7 @@ 4dp 14dp 16dp + 18dp 16dp @*android:dimen/config_dialogCornerRadius 12dp diff --git a/res/values/styles.xml b/res/values/styles.xml index c02378e3f7c..70d3670cebb 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -1017,4 +1017,22 @@ @null + + + + + + diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java index fe97759e225..6c4d1ad0255 100644 --- a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java +++ b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; /** Interface should be implemented if you have added new suggestions */ @@ -46,6 +47,20 @@ public interface SuggestionFeatureProvider { /** * Returns the class of {@link Fragment} that supports contextual suggestion. + * + * @deprecated - use {@link SuggestionFeatureProvider#getSuggestionFragment()} instead. */ - Class getContextualSuggestionFragment(); + @Deprecated + @Nullable + default Class getContextualSuggestionFragment() { + return null; + } + + /** + * Returns the class of {@link Fragment} that provides the UI for Suggestions. + */ + @Nullable + default Class getSuggestionFragment() { + return getContextualSuggestionFragment(); + } } diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java index 066901015d4..5e8e3740904 100644 --- a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java +++ b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java @@ -22,12 +22,14 @@ import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import com.android.settings.Settings.NightDisplaySuggestionActivity; import com.android.settings.biometrics.fingerprint.FingerprintEnrollSuggestionActivity; import com.android.settings.biometrics.fingerprint.FingerprintSuggestionActivity; import com.android.settings.display.NightDisplayPreferenceController; +import com.android.settings.flags.Flags; import com.android.settings.notification.zen.ZenOnboardingActivity; import com.android.settings.notification.zen.ZenSuggestionActivity; import com.android.settings.password.ScreenLockSuggestionActivity; @@ -81,8 +83,13 @@ public class SuggestionFeatureProviderImpl implements SuggestionFeatureProvider return context.getSharedPreferences(SHARED_PREF_FILENAME, Context.MODE_PRIVATE); } + @Nullable @Override - public Class getContextualSuggestionFragment() { - return null; + public Class getSuggestionFragment() { + if (Flags.updatedSuggestionCardAosp()) { + return SuggestionFragment.class; + } else { + return null; + } } } diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionFragment.kt b/src/com/android/settings/dashboard/suggestions/SuggestionFragment.kt new file mode 100644 index 00000000000..760ddc9e505 --- /dev/null +++ b/src/com/android/settings/dashboard/suggestions/SuggestionFragment.kt @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2024 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.dashboard.suggestions + +import android.app.ActivityOptions +import android.app.PendingIntent +import android.app.settings.SettingsEnums +import android.content.Context +import android.os.Bundle +import android.os.SystemClock +import android.service.settings.suggestions.Suggestion +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import com.android.settings.core.InstrumentedFragment +import com.android.settings.homepage.SettingsHomepageActivity +import com.android.settings.homepage.SplitLayoutListener +import com.android.settings.overlay.FeatureFactory +import com.android.settings.R +import com.android.settingslib.suggestions.SuggestionController +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +private const val SUGGESTIONS = "suggestions" +private const val TAG = "ContextualSuggestFrag" +private const val FLAG_IS_DISMISSIBLE = 1 shl 2 + +/** + * Fragment to control display and interaction logic for [Suggestion]s + */ +class SuggestionFragment : InstrumentedFragment(), + SplitLayoutListener, SuggestionController.ServiceConnectionListener { + + private val scope = CoroutineScope(Job() + Dispatchers.Main) + private lateinit var suggestionController: SuggestionController + private lateinit var suggestionTile: View + private var icon: ImageView? = null + private var iconFrame: View? = null + private var title: TextView? = null + private var summary: TextView? = null + private var dismiss: ImageView? = null + private var iconVisible = true + private var startTime: Long = 0 + private var suggestionsRestored = false + private var splitLayoutSupported = false + + override fun onAttach(context: Context) { + super.onAttach(context) + val component = FeatureFactory.featureFactory + .suggestionFeatureProvider + .suggestionServiceComponent + suggestionController = SuggestionController(context, component, this) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + suggestionTile = inflater.inflate(R.layout.suggestion_tile, container, true) + icon = suggestionTile.findViewById(android.R.id.icon) + iconFrame = suggestionTile.findViewById(android.R.id.icon_frame) + title = suggestionTile.findViewById(android.R.id.title) + summary = suggestionTile.findViewById(android.R.id.summary) + dismiss = suggestionTile.findViewById(android.R.id.closeButton) + if (!iconVisible) { + onSplitLayoutChanged(false) + } + // Restore the suggestion and skip reloading + if (savedInstanceState != null) { + Log.d(TAG, "Restoring suggestions") + savedInstanceState.getParcelableArrayList( + SUGGESTIONS, + Suggestion::class.java + )?.let { suggestions -> + suggestionsRestored = true + startTime = SystemClock.uptimeMillis() + updateState(suggestions) + } + } + + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putParcelableArrayList(SUGGESTIONS, currentSuggestions) + super.onSaveInstanceState(outState) + } + + override fun onStart() { + super.onStart() + suggestionController.start() + } + + override fun onStop() { + suggestionController.stop() + super.onStop() + } + + override fun getMetricsCategory(): Int { + return SettingsEnums.SETTINGS_HOMEPAGE + } + + override fun setSplitLayoutSupported(supported: Boolean) { + splitLayoutSupported = supported + } + + override fun onSplitLayoutChanged(isRegularLayout: Boolean) { + iconVisible = isRegularLayout + if (splitLayoutSupported) { + iconFrame?.visibility = if (iconVisible) View.VISIBLE else View.GONE + } + } + + override fun onServiceConnected() { + loadSuggestions() + } + + override fun onServiceDisconnected() { + // no-op + } + + private fun loadSuggestions() { + if (suggestionsRestored) { + // Skip first suggestion loading when restored + suggestionsRestored = false + return + } + + startTime = SystemClock.uptimeMillis() + scope.launch(Dispatchers.IO) { + Log.d(TAG, "Start loading suggestions") + val suggestions = suggestionController.suggestions + Log.d(TAG, "Loaded suggestions: ${suggestions?.size}") + withContext(Dispatchers.Main) { + updateState(suggestions) + } + } + } + + private fun updateState(suggestions: List?) { + currentSuggestions.clear() + if (suggestions.isNullOrEmpty()) { + Log.d(TAG, "Remove suggestions") + showSuggestionTile(false) + return + } + currentSuggestions.addAll(suggestions) + + // Only take top suggestion; we assume this is the highest rank. + val suggestion = suggestions.first() + icon?.setImageIcon(suggestion.icon) + suggestion.title?.let { + title?.text = it + } ?: run { + Log.d(TAG, "No suggestion title, removing") + showSuggestionTile(false) + return + } + val suggestionSummary = suggestion.summary + if (suggestionSummary.isNullOrEmpty()) { + summary?.visibility = View.GONE + } else { + summary?.visibility = View.VISIBLE + summary?.text = suggestionSummary + } + if (suggestion.flags and FLAG_IS_DISMISSIBLE != 0) { + dismiss?.let { dismissView -> + dismissView.visibility = View.VISIBLE + dismissView.setOnClickListener { + scope.launch(Dispatchers.IO) { + suggestionController.dismissSuggestions(suggestion) + } + if (suggestions.size > 1) { + dismissView.visibility = View.GONE + updateState(suggestions.subList(1, suggestions.size)) + } else { + currentSuggestions.clear() + suggestionTile.visibility = View.GONE + } + } + } + } + suggestionTile.setOnClickListener { + // Notify service that suggestion is being launched. Note that the service does not + // actually start the suggestion on our behalf, instead simply logging metrics. + scope.launch(Dispatchers.IO) { + suggestionController.launchSuggestion(suggestion) + } + currentSuggestions.clear() + try { + val options = ActivityOptions.makeBasic() + .setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ) + suggestion.pendingIntent.send(options.toBundle()) + } catch (e: PendingIntent.CanceledException) { + Log.e(TAG, "Failed to start suggestion ${suggestion.title}", e) + } + } + showSuggestionTile(true) + } + + private fun showSuggestionTile(show: Boolean) { + val totalTime = SystemClock.uptimeMillis() - startTime + Log.d(TAG, "Total loading time: $totalTime ms") + mMetricsFeatureProvider.action( + context, + SettingsEnums.ACTION_CONTEXTUAL_HOME_SHOW, + totalTime.toInt() + ) + (activity as? SettingsHomepageActivity)?.showHomepageWithSuggestion(show) + } + + private companion object { + val currentSuggestions = arrayListOf() + } +} \ No newline at end of file diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 7a2f52a8674..4b9f6a8da9d 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -276,7 +276,8 @@ public class SettingsHomepageActivity extends FragmentActivity implements final boolean scrollNeeded = mIsEmbeddingActivityEnabled && !TextUtils.equals(getString(DEFAULT_HIGHLIGHT_MENU_KEY), highlightMenuKey); showSuggestionFragment(scrollNeeded); - if (FeatureFlagUtils.isEnabled(this, FeatureFlags.CONTEXTUAL_HOME)) { + if (!Flags.updatedSuggestionCardAosp() + && FeatureFlagUtils.isEnabled(this, FeatureFlags.CONTEXTUAL_HOME)) { showFragment(() -> new ContextualCardsFragment(), R.id.contextual_cards_content); ((FrameLayout) findViewById(R.id.main_content)) .getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING); @@ -477,7 +478,7 @@ public class SettingsHomepageActivity extends FragmentActivity implements private void showSuggestionFragment(boolean scrollNeeded) { final Class fragmentClass = FeatureFactory.getFeatureFactory() - .getSuggestionFeatureProvider().getContextualSuggestionFragment(); + .getSuggestionFeatureProvider().getSuggestionFragment(); if (fragmentClass == null) { return; } diff --git a/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImplTest.java index 0c8af12db29..6d2193cb760 100644 --- a/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImplTest.java +++ b/tests/robotests/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImplTest.java @@ -25,12 +25,19 @@ import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.hardware.fingerprint.FingerprintManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.service.settings.suggestions.Suggestion; +import androidx.fragment.app.Fragment; + +import com.android.settings.flags.Flags; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowSecureSettings; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -43,6 +50,9 @@ import org.robolectric.annotation.Config; @Config(shadows = ShadowSecureSettings.class) public class SuggestionFeatureProviderImplTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private Context mContext; @Mock @@ -94,4 +104,20 @@ public class SuggestionFeatureProviderImplTest { when(mActivityManager.isLowRamDevice()).thenReturn(false); assertThat(mProvider.isSuggestionEnabled(mContext)).isTrue(); } + + @DisableFlags(Flags.FLAG_UPDATED_SUGGESTION_CARD_AOSP) + @Test + public void getSuggestionFragment_withFlagDisabled_shouldReturnNull() { + Class fragment = mProvider.getSuggestionFragment(); + + assertThat(fragment).isNull(); + } + + @EnableFlags(Flags.FLAG_UPDATED_SUGGESTION_CARD_AOSP) + @Test + public void getSuggestionFragment_withFlagEnabled_shouldReturnFragment() { + Class fragment = mProvider.getSuggestionFragment(); + + assertThat(fragment).isEqualTo(SuggestionFragment.class); + } } diff --git a/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java b/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java index 31ac9446612..2cf93e4fb3b 100644 --- a/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java +++ b/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java @@ -444,7 +444,7 @@ public class SettingsHomepageActivityTest { public static class ShadowSuggestionFeatureProviderImpl { @Implementation - public Class getContextualSuggestionFragment() { + public Class getSuggestionFragment() { return Fragment.class; } } From 3ccbb0ffa51cfe9e24294c44eb14ffdd2a6fa0a4 Mon Sep 17 00:00:00 2001 From: Yuri Lin Date: Tue, 20 Aug 2024 16:58:37 -0400 Subject: [PATCH 06/12] Add modes to Settings homepage. Also moves the modes entrypoints out of the sub-pages it was previously in (notifications and sound settings). Bug: 353700470 Test: manual by opening the homepage Flag: android.app.modes_ui Change-Id: I53124bfed25c43fc2dabaf9c43ce0513bbfc7377 --- AndroidManifest.xml | 2 +- res/values/menu_keys.xml | 1 + res/xml/configure_notification_settings.xml | 9 --------- res/xml/modes_list_settings.xml | 3 ++- res/xml/sound_settings.xml | 8 -------- res/xml/top_level_settings.xml | 10 ++++++++++ res/xml/top_level_settings_v2.xml | 10 ++++++++++ .../modes/ZenModesLinkPreferenceController.java | 10 +++++++++- 8 files changed, 33 insertions(+), 20 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 7b79611ca65..a418dacf3bb 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1321,7 +1321,7 @@ + android:value="@string/menu_key_priority_modes" /> diff --git a/res/values/menu_keys.xml b/res/values/menu_keys.xml index 27e9639122a..36fdb818c6c 100755 --- a/res/values/menu_keys.xml +++ b/res/values/menu_keys.xml @@ -24,6 +24,7 @@ top_level_battery top_level_storage top_level_sound + top_level_priority_modes top_level_display top_level_wallpaper top_level_accessibility diff --git a/res/xml/configure_notification_settings.xml b/res/xml/configure_notification_settings.xml index 1e5b241ea82..b673a0839b0 100644 --- a/res/xml/configure_notification_settings.xml +++ b/res/xml/configure_notification_settings.xml @@ -135,15 +135,6 @@ settings:controller="com.android.settings.notification.zen.ZenModePreferenceController" /> - - + android:title="@string/zen_modes_list_title" + android:key="modes_list_settings"> diff --git a/res/xml/sound_settings.xml b/res/xml/sound_settings.xml index 393009ec920..15312c8b067 100644 --- a/res/xml/sound_settings.xml +++ b/res/xml/sound_settings.xml @@ -105,14 +105,6 @@ settings:keywords="@string/keywords_sounds_and_notifications_interruptions" settings:controller="com.android.settings.notification.zen.ZenModePreferenceController"/> - - + + + + Date: Thu, 22 Aug 2024 16:49:37 +0800 Subject: [PATCH 07/12] Create EuiccRepository Which also avoid calling from main thread. Bug: 356684993 Flag: EXEMPT bug fix Test: manual - on SIMs Test: atest EuiccRepositoryTest Change-Id: I0b11b0bd1e8a4b5754781e888fd220fa3080a212 --- .../network/MobileNetworkListFragment.kt | 4 +- .../MobileNetworkSummaryController.java | 6 +- .../network/telephony/MobileNetworkUtils.java | 76 ----------- .../telephony/euicc/EuiccRepository.kt | 129 ++++++++++++++++++ .../settings/spa/network/SimsSection.kt | 17 ++- .../telephony/euicc/EuiccRepositoryTest.kt | 117 ++++++++++++++++ 6 files changed, 262 insertions(+), 87 deletions(-) create mode 100644 src/com/android/settings/network/telephony/euicc/EuiccRepository.kt create mode 100644 tests/spa_unit/src/com/android/settings/network/telephony/euicc/EuiccRepositoryTest.kt diff --git a/src/com/android/settings/network/MobileNetworkListFragment.kt b/src/com/android/settings/network/MobileNetworkListFragment.kt index eb0d16cef09..bb88330dcfb 100644 --- a/src/com/android/settings/network/MobileNetworkListFragment.kt +++ b/src/com/android/settings/network/MobileNetworkListFragment.kt @@ -27,7 +27,7 @@ import com.android.settings.R import com.android.settings.SettingsPreferenceFragment import com.android.settings.dashboard.DashboardFragment import com.android.settings.flags.Flags -import com.android.settings.network.telephony.MobileNetworkUtils +import com.android.settings.network.telephony.euicc.EuiccRepository import com.android.settings.search.BaseSearchIndexProvider import com.android.settings.spa.SpaActivity.Companion.startSpaActivity import com.android.settings.spa.network.NetworkCellularGroupProvider @@ -58,7 +58,7 @@ class MobileNetworkListFragment : DashboardFragment() { listView.itemAnimator = null findPreference(KEY_ADD_SIM)!!.isVisible = - MobileNetworkUtils.showEuiccSettings(context) + EuiccRepository(requireContext()).showEuiccSettings() } override fun getPreferenceScreenResId() = R.xml.network_provider_sims_list diff --git a/src/com/android/settings/network/MobileNetworkSummaryController.java b/src/com/android/settings/network/MobileNetworkSummaryController.java index 4627a253eca..9bf6915a527 100644 --- a/src/com/android/settings/network/MobileNetworkSummaryController.java +++ b/src/com/android/settings/network/MobileNetworkSummaryController.java @@ -35,7 +35,7 @@ import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.network.telephony.MobileNetworkUtils; +import com.android.settings.network.telephony.euicc.EuiccRepository; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.RestrictedPreference; import com.android.settingslib.Utils; @@ -118,7 +118,7 @@ public class MobileNetworkSummaryController extends AbstractPreferenceController if ((mSubInfoEntityList == null || mSubInfoEntityList.isEmpty()) || ( mUiccInfoEntityList == null || mUiccInfoEntityList.isEmpty()) || ( mMobileNetworkInfoEntityList == null || mMobileNetworkInfoEntityList.isEmpty())) { - if (MobileNetworkUtils.showEuiccSettingsDetecting(mContext)) { + if (new EuiccRepository(mContext).showEuiccSettings()) { return mContext.getResources().getString( R.string.mobile_network_summary_add_a_network); } @@ -168,7 +168,7 @@ public class MobileNetworkSummaryController extends AbstractPreferenceController || (mUiccInfoEntityList == null || mUiccInfoEntityList.isEmpty()) || (mMobileNetworkInfoEntityList == null || mMobileNetworkInfoEntityList.isEmpty()))) { - if (MobileNetworkUtils.showEuiccSettingsDetecting(mContext)) { + if (new EuiccRepository(mContext).showEuiccSettings()) { mPreference.setOnPreferenceClickListener((Preference pref) -> { logPreferenceClick(pref); startAddSimFlow(); diff --git a/src/com/android/settings/network/telephony/MobileNetworkUtils.java b/src/com/android/settings/network/telephony/MobileNetworkUtils.java index 517f66ad60a..235418ebb55 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkUtils.java +++ b/src/com/android/settings/network/telephony/MobileNetworkUtils.java @@ -32,7 +32,6 @@ import static com.android.settings.network.telephony.TelephonyConstants.Telephon import static com.android.settings.network.telephony.TelephonyConstants.TelephonyManagerConstants.NETWORK_MODE_NR_LTE_GSM_WCDMA; import android.app.KeyguardManager; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -51,8 +50,6 @@ import android.os.CancellationSignal; import android.os.Handler; import android.os.Looper; import android.os.PersistableBundle; -import android.os.SystemClock; -import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -89,32 +86,17 @@ import com.android.settings.network.ims.WifiCallingQueryImsState; import com.android.settings.network.telephony.TelephonyConstants.TelephonyManagerConstants; import com.android.settings.network.telephony.wificalling.WifiCallingRepository; import com.android.settingslib.core.instrumentation.Instrumentable; -import com.android.settingslib.development.DevelopmentSettingsEnabler; import com.android.settingslib.graph.SignalDrawable; import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity; -import com.android.settingslib.utils.ThreadUtils; -import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; public class MobileNetworkUtils { private static final String TAG = "MobileNetworkUtils"; - // CID of the device. - private static final String KEY_CID = "ro.boot.cid"; - // CIDs of devices which should not show anything related to eSIM. - private static final String KEY_ESIM_CID_IGNORE = "ro.setupwizard.esim_cid_ignore"; - // System Property which is used to decide whether the default eSIM UI will be shown, - // the default value is false. - private static final String KEY_ENABLE_ESIM_UI_BY_DEFAULT = - "esim.enable_esim_system_ui_by_default"; private static final String LEGACY_ACTION_CONFIGURE_PHONE_ACCOUNT = "android.telecom.action.CONNECTION_SERVICE_CONFIGURE"; private static final String RTL_MARK = "\u200F"; @@ -282,64 +264,6 @@ public class MobileNetworkUtils { return intent; } - /** - * Whether to show the entry point to eUICC settings. - * - *

We show the entry point on any device which supports eUICC as long as either the eUICC - * was ever provisioned (that is, at least one profile was ever downloaded onto it), or if - * the user has enabled development mode. - */ - public static boolean showEuiccSettings(Context context) { - if (!SubscriptionUtil.isSimHardwareVisible(context)) { - return false; - } - long timeForAccess = SystemClock.elapsedRealtime(); - try { - Boolean isShow = ((Future) ThreadUtils.postOnBackgroundThread(() -> { - try { - return showEuiccSettingsDetecting(context); - } catch (Exception threadException) { - Log.w(TAG, "Accessing Euicc failure", threadException); - } - return Boolean.FALSE; - })).get(3, TimeUnit.SECONDS); - return ((isShow != null) && isShow.booleanValue()); - } catch (ExecutionException | InterruptedException | TimeoutException exception) { - timeForAccess = SystemClock.elapsedRealtime() - timeForAccess; - Log.w(TAG, "Accessing Euicc takes too long: +" + timeForAccess + "ms"); - } - return false; - } - - // The same as #showEuiccSettings(Context context) - public static Boolean showEuiccSettingsDetecting(Context context) { - final EuiccManager euiccManager = - (EuiccManager) context.getSystemService(EuiccManager.class); - if (euiccManager == null || !euiccManager.isEnabled()) { - Log.w(TAG, "EuiccManager is not enabled."); - return false; - } - - final ContentResolver cr = context.getContentResolver(); - final boolean esimIgnoredDevice = - Arrays.asList(TextUtils.split(SystemProperties.get(KEY_ESIM_CID_IGNORE, ""), ",")) - .contains(SystemProperties.get(KEY_CID)); - final boolean enabledEsimUiByDefault = - SystemProperties.getBoolean(KEY_ENABLE_ESIM_UI_BY_DEFAULT, true); - final boolean euiccProvisioned = - Settings.Global.getInt(cr, Settings.Global.EUICC_PROVISIONED, 0) != 0; - final boolean inDeveloperMode = - DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context); - Log.i(TAG, - String.format("showEuiccSettings: esimIgnoredDevice: %b, enabledEsimUiByDefault: " - + "%b, euiccProvisioned: %b, inDeveloperMode: %b.", - esimIgnoredDevice, enabledEsimUiByDefault, euiccProvisioned, inDeveloperMode)); - return (euiccProvisioned - || (!esimIgnoredDevice && inDeveloperMode) - || (!esimIgnoredDevice && enabledEsimUiByDefault - && isCurrentCountrySupported(context))); - } - /** * Return {@code true} if mobile data is enabled */ diff --git a/src/com/android/settings/network/telephony/euicc/EuiccRepository.kt b/src/com/android/settings/network/telephony/euicc/EuiccRepository.kt new file mode 100644 index 00000000000..74b531351b8 --- /dev/null +++ b/src/com/android/settings/network/telephony/euicc/EuiccRepository.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2024 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.network.telephony.euicc + +import android.content.Context +import android.os.SystemProperties +import android.provider.Settings +import android.telephony.TelephonyManager +import android.telephony.euicc.EuiccManager +import android.util.Log +import com.android.settings.network.SubscriptionUtil +import com.android.settingslib.development.DevelopmentSettingsEnabler +import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBoolean +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +class EuiccRepository +@JvmOverloads +constructor( + private val context: Context, + private val isEuiccProvisioned: () -> Boolean = { + val euiccProvisioned by context.settingsGlobalBoolean(Settings.Global.EUICC_PROVISIONED) + euiccProvisioned + }, + private val isDevelopmentSettingsEnabled: () -> Boolean = { + DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context) + }, +) { + + private val euiccManager = context.getSystemService(EuiccManager::class.java) + private val telephonyManager = context.getSystemService(TelephonyManager::class.java) + + fun showEuiccSettingsFlow() = + flow { emit(showEuiccSettings()) } + .distinctUntilChanged() + .conflate() + .flowOn(Dispatchers.Default) + + /** + * Whether to show the entry point to eUICC settings. + * + * We show the entry point on any device which supports eUICC as long as either the eUICC was + * ever provisioned (that is, at least one profile was ever downloaded onto it), or if the user + * has enabled development mode. + */ + fun showEuiccSettings(): Boolean { + if (!SubscriptionUtil.isSimHardwareVisible(context)) return false + if (euiccManager == null || !euiccManager.isEnabled) { + Log.w(TAG, "EuiccManager is not enabled.") + return false + } + if (isEuiccProvisioned()) { + Log.i(TAG, "showEuiccSettings: euicc provisioned") + return true + } + val ignoredCids = + SystemProperties.get(KEY_ESIM_CID_IGNORE).split(',').filter { it.isNotEmpty() } + val cid = SystemProperties.get(KEY_CID) + if (cid in ignoredCids) { + Log.i(TAG, "showEuiccSettings: cid ignored") + return false + } + if (isDevelopmentSettingsEnabled()) { + Log.i(TAG, "showEuiccSettings: development settings enabled") + return true + } + val enabledEsimUiByDefault = + SystemProperties.getBoolean(KEY_ENABLE_ESIM_UI_BY_DEFAULT, true) + Log.i(TAG, "showEuiccSettings: enabledEsimUiByDefault=$enabledEsimUiByDefault") + return enabledEsimUiByDefault && isCurrentCountrySupported() + } + + /** + * Loop through all the device logical slots to check whether the user's current country + * supports eSIM. + */ + private fun isCurrentCountrySupported(): Boolean { + val euiccManager = euiccManager ?: return false + val telephonyManager = telephonyManager ?: return false + val visitedCountrySet = mutableSetOf() + for (slotIndex in 0 until telephonyManager.getActiveModemCount()) { + val countryCode = telephonyManager.getNetworkCountryIso(slotIndex) + if ( + countryCode.isNotEmpty() && + visitedCountrySet.add(countryCode) && + euiccManager.isSupportedCountry(countryCode) + ) { + Log.i(TAG, "isCurrentCountrySupported: $countryCode is supported") + return true + } + } + Log.i(TAG, "isCurrentCountrySupported: no country is supported") + return false + } + + companion object { + private const val TAG = "EuiccRepository" + + /** CID of the device. */ + private const val KEY_CID: String = "ro.boot.cid" + + /** CIDs of devices which should not show anything related to eSIM. */ + private const val KEY_ESIM_CID_IGNORE: String = "ro.setupwizard.esim_cid_ignore" + + /** + * System Property which is used to decide whether the default eSIM UI will be shown, the + * default value is false. + */ + private const val KEY_ENABLE_ESIM_UI_BY_DEFAULT: String = + "esim.enable_esim_system_ui_by_default" + } +} diff --git a/src/com/android/settings/spa/network/SimsSection.kt b/src/com/android/settings/spa/network/SimsSection.kt index 842656e28bd..276d121c24f 100644 --- a/src/com/android/settings/spa/network/SimsSection.kt +++ b/src/com/android/settings/spa/network/SimsSection.kt @@ -40,6 +40,7 @@ import com.android.settings.network.SubscriptionUtil import com.android.settings.network.telephony.MobileNetworkUtils import com.android.settings.network.telephony.SubscriptionActivationRepository import com.android.settings.network.telephony.SubscriptionRepository +import com.android.settings.network.telephony.euicc.EuiccRepository import com.android.settings.network.telephony.phoneNumberFlow import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel @@ -120,13 +121,17 @@ fun phoneNumber(subInfo: SubscriptionInfo): State { @Composable private fun AddSim() { val context = LocalContext.current - if (remember { MobileNetworkUtils.showEuiccSettings(context) }) { + val isShow by + remember { EuiccRepository(context).showEuiccSettingsFlow() } + .collectAsStateWithLifecycle(initialValue = false) + if (isShow) { RestrictedPreference( - model = object : PreferenceModel { - override val title = stringResource(id = R.string.mobile_network_list_add_more) - override val icon = @Composable { SettingsIcon(Icons.Outlined.Add) } - override val onClick = { startAddSimFlow(context) } - }, + model = + object : PreferenceModel { + override val title = stringResource(id = R.string.mobile_network_list_add_more) + override val icon = @Composable { SettingsIcon(Icons.Outlined.Add) } + override val onClick = { startAddSimFlow(context) } + }, restrictions = Restrictions(keys = listOf(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)), ) } diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/euicc/EuiccRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/euicc/EuiccRepositoryTest.kt new file mode 100644 index 00000000000..24e6fd6ac16 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/euicc/EuiccRepositoryTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 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.network.telephony.euicc + +import android.content.Context +import android.telephony.TelephonyManager +import android.telephony.euicc.EuiccManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class EuiccRepositoryTest { + + private val mockEuiccManager = mock { on { isEnabled } doReturn true } + + private val mockTelephonyManager = + mock { + on { activeModemCount } doReturn 1 + on { getNetworkCountryIso(any()) } doReturn COUNTRY_CODE + } + + private val context: Context = + spy(ApplicationProvider.getApplicationContext()) { + on { getSystemService(EuiccManager::class.java) } doReturn mockEuiccManager + on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager + } + + private val resources = + spy(context.resources) { on { getBoolean(R.bool.config_show_sim_info) } doReturn true } + + private var euiccProvisioned = false + + private val repository = + EuiccRepository( + context, + isEuiccProvisioned = { euiccProvisioned }, + isDevelopmentSettingsEnabled = { false }, + ) + + @Before + fun setUp() { + context.stub { on { resources } doReturn resources } + } + + @Test + fun showEuiccSettings_noSim_returnFalse() { + resources.stub { on { getBoolean(R.bool.config_show_sim_info) } doReturn false } + + val showEuiccSettings = repository.showEuiccSettings() + + assertThat(showEuiccSettings).isFalse() + } + + @Test + fun showEuiccSettings_euiccDisabled_returnFalse() { + mockEuiccManager.stub { on { isEnabled } doReturn false } + + val showEuiccSettings = repository.showEuiccSettings() + + assertThat(showEuiccSettings).isFalse() + } + + @Test + fun showEuiccSettings_euiccProvisioned_returnTrue() { + euiccProvisioned = true + + val showEuiccSettings = repository.showEuiccSettings() + + assertThat(showEuiccSettings).isTrue() + } + + @Test + fun showEuiccSettings_countryNotSupported_returnFalse() { + mockEuiccManager.stub { on { isSupportedCountry(COUNTRY_CODE) } doReturn false } + + val showEuiccSettings = repository.showEuiccSettings() + + assertThat(showEuiccSettings).isFalse() + } + + @Test + fun showEuiccSettings_countrySupported_returnTrue() { + mockEuiccManager.stub { on { isSupportedCountry(COUNTRY_CODE) } doReturn true } + + val showEuiccSettings = repository.showEuiccSettings() + + assertThat(showEuiccSettings).isTrue() + } + + private companion object { + const val COUNTRY_CODE = "us" + } +} From f218f7622102d7708270fcb9fa2ce5df849d686a Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 20 Aug 2024 15:44:22 +0800 Subject: [PATCH 08/12] Create VideoCallingRepository Also support settings search for video calling. Fix: 233327342 Flag: EXEMPT bug fix Test: manual - on mobile settings Test: manual - search video calling Test: unit test Change-Id: Ic4a25849f77f1759fa157931d428daa9e6854ff2 --- res/xml/mobile_network_settings.xml | 3 +- .../settings/network/ims/VtQueryImsState.java | 37 +-- .../telephony/CarrierConfigRepository.kt | 13 +- .../MobileNetworkSettingsSearchIndex.kt | 2 + .../VideoCallingPreferenceController.java | 234 ------------------ .../VideoCallingPreferenceController.kt | 147 +++++++++++ .../telephony/VideoCallingRepository.kt | 65 +++++ .../network/ims/MockVolteQueryImsState.java | 105 -------- .../network/ims/MockVtQueryImsState.java | 104 -------- .../VideoCallingPreferenceControllerTest.java | 160 ------------ .../VideoCallingPreferenceControllerTest.kt | 125 ++++++++++ .../telephony/VideoCallingRepositoryTest.kt | 166 +++++++++++++ .../network/ims/MockVtQueryImsState.java | 104 -------- 13 files changed, 522 insertions(+), 743 deletions(-) delete mode 100644 src/com/android/settings/network/telephony/VideoCallingPreferenceController.java create mode 100644 src/com/android/settings/network/telephony/VideoCallingPreferenceController.kt create mode 100644 src/com/android/settings/network/telephony/VideoCallingRepository.kt delete mode 100644 tests/robotests/src/com/android/settings/network/ims/MockVolteQueryImsState.java delete mode 100644 tests/robotests/src/com/android/settings/network/ims/MockVtQueryImsState.java delete mode 100644 tests/robotests/src/com/android/settings/network/telephony/VideoCallingPreferenceControllerTest.java create mode 100644 tests/spa_unit/src/com/android/settings/network/telephony/VideoCallingPreferenceControllerTest.kt create mode 100644 tests/spa_unit/src/com/android/settings/network/telephony/VideoCallingRepositoryTest.kt delete mode 100644 tests/unit/src/com/android/settings/network/ims/MockVtQueryImsState.java diff --git a/res/xml/mobile_network_settings.xml b/res/xml/mobile_network_settings.xml index 4f16e12f394..fc99ae279a0 100644 --- a/res/xml/mobile_network_settings.xml +++ b/res/xml/mobile_network_settings.xml @@ -221,10 +221,11 @@ + diff --git a/src/com/android/settings/network/ims/VtQueryImsState.java b/src/com/android/settings/network/ims/VtQueryImsState.java index 5c48ff00380..7351b83b18f 100644 --- a/src/com/android/settings/network/ims/VtQueryImsState.java +++ b/src/com/android/settings/network/ims/VtQueryImsState.java @@ -18,24 +18,17 @@ package com.android.settings.network.ims; import android.content.Context; import android.telecom.TelecomManager; -import android.telephony.AccessNetworkConstants; import android.telephony.SubscriptionManager; -import android.telephony.ims.ImsException; -import android.telephony.ims.feature.MmTelFeature; -import android.telephony.ims.stub.ImsRegistrationImplBase; -import android.util.Log; import androidx.annotation.VisibleForTesting; /** * Controller class for querying VT status */ -public class VtQueryImsState extends ImsQueryController { +public class VtQueryImsState { - private static final String LOG_TAG = "VtQueryImsState"; - - private Context mContext; - private int mSubId; + private final Context mContext; + private final int mSubId; /** * Constructor @@ -44,9 +37,6 @@ public class VtQueryImsState extends ImsQueryController { * @param subId subscription's id */ public VtQueryImsState(Context context, int subId) { - super(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO, - ImsRegistrationImplBase.REGISTRATION_TECH_LTE, - AccessNetworkConstants.TRANSPORT_TYPE_WWAN); mContext = context; mSubId = subId; } @@ -62,24 +52,6 @@ public class VtQueryImsState extends ImsQueryController { return (new ImsQueryVtUserSetting(subId)).query(); } - /** - * Check whether Video Call can be perform or not on this subscription - * - * @return true when Video Call can be performed, otherwise false - */ - public boolean isReadyToVideoCall() { - if (!isProvisionedOnDevice(mSubId)) { - return false; - } - - try { - return isEnabledByPlatform(mSubId) && isServiceStateReady(mSubId); - } catch (InterruptedException | IllegalArgumentException | ImsException exception) { - Log.w(LOG_TAG, "fail to get Vt ready. subId=" + mSubId, exception); - } - return false; - } - /** * Get allowance status for user to alter configuration * @@ -89,8 +61,7 @@ public class VtQueryImsState extends ImsQueryController { if (!SubscriptionManager.isValidSubscriptionId(mSubId)) { return false; } - return ((!isTtyEnabled(mContext)) - || (isTtyOnVolteEnabled(mSubId))); + return !isTtyEnabled(mContext) || new ImsQueryTtyOnVolteStat(mSubId).query(); } @VisibleForTesting diff --git a/src/com/android/settings/network/telephony/CarrierConfigRepository.kt b/src/com/android/settings/network/telephony/CarrierConfigRepository.kt index 3f5c06efb93..88525402e34 100644 --- a/src/com/android/settings/network/telephony/CarrierConfigRepository.kt +++ b/src/com/android/settings/network/telephony/CarrierConfigRepository.kt @@ -50,7 +50,7 @@ class CarrierConfigRepository(private val context: Context) { private val keysToRetrieve = mutableMapOf() override fun getBoolean(key: String): Boolean { - check(key.endsWith("_bool")) { "Boolean key should ends with _bool" } + checkBooleanKey(key) val value = cache[key] return if (value == null) { keysToRetrieve += key to KeyType.BOOLEAN @@ -186,9 +186,18 @@ class CarrierConfigRepository(private val context: Context) { ListenerRegistered.getAndSet(false) } + private val BooleanKeysWhichNotFollowingsNamingConventions = + listOf(CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS) + + private fun checkBooleanKey(key: String) { + check(key.endsWith("_bool") || key in BooleanKeysWhichNotFollowingsNamingConventions) { + "Boolean key should ends with _bool" + } + } + @VisibleForTesting fun setBooleanForTest(subId: Int, key: String, value: Boolean) { - check(key.endsWith("_bool")) { "Boolean key should ends with _bool" } + checkBooleanKey(key) getPerSubCache(subId)[key] = BooleanConfigValue(value) } diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettingsSearchIndex.kt b/src/com/android/settings/network/telephony/MobileNetworkSettingsSearchIndex.kt index c63e7d23bf1..4f31e0f71c7 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSettingsSearchIndex.kt +++ b/src/com/android/settings/network/telephony/MobileNetworkSettingsSearchIndex.kt @@ -26,6 +26,7 @@ import com.android.settings.network.telephony.DataUsagePreferenceController.Comp import com.android.settings.network.telephony.MmsMessagePreferenceController.Companion.MmsMessageSearchItem import com.android.settings.network.telephony.NrAdvancedCallingPreferenceController.Companion.NrAdvancedCallingSearchItem import com.android.settings.network.telephony.RoamingPreferenceController.Companion.RoamingSearchItem +import com.android.settings.network.telephony.VideoCallingPreferenceController.Companion.VideoCallingSearchItem import com.android.settings.network.telephony.WifiCallingPreferenceController.Companion.WifiCallingSearchItem import com.android.settings.spa.SpaSearchLanding.BundleValue import com.android.settings.spa.SpaSearchLanding.SpaSearchLandingFragment @@ -122,6 +123,7 @@ class MobileNetworkSettingsSearchIndex( NrAdvancedCallingSearchItem(context), PreferredNetworkModeSearchItem(context), RoamingSearchItem(context), + VideoCallingSearchItem(context), WifiCallingSearchItem(context), ) } diff --git a/src/com/android/settings/network/telephony/VideoCallingPreferenceController.java b/src/com/android/settings/network/telephony/VideoCallingPreferenceController.java deleted file mode 100644 index f803efdc1b0..00000000000 --- a/src/com/android/settings/network/telephony/VideoCallingPreferenceController.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright (C) 2018 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.network.telephony; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.PersistableBundle; -import android.telephony.CarrierConfigManager; -import android.telephony.SubscriptionManager; -import android.telephony.TelephonyCallback; -import android.telephony.TelephonyManager; -import android.telephony.ims.ImsMmTelManager; -import android.util.Log; - -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import androidx.preference.TwoStatePreference; - -import com.android.internal.telephony.flags.Flags; -import com.android.settings.network.CarrierConfigCache; -import com.android.settings.network.MobileDataEnabledListener; -import com.android.settings.network.ims.VolteQueryImsState; -import com.android.settings.network.ims.VtQueryImsState; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnStart; -import com.android.settingslib.core.lifecycle.events.OnStop; - -/** - * Preference controller for "Video Calling" - */ -public class VideoCallingPreferenceController extends TelephonyTogglePreferenceController implements - LifecycleObserver, OnStart, OnStop, - MobileDataEnabledListener.Client, - Enhanced4gBasePreferenceController.On4gLteUpdateListener { - - private static final String TAG = "VideoCallingPreference"; - - private Preference mPreference; - private PhoneTelephonyCallback mTelephonyCallback; - @VisibleForTesting - Integer mCallState; - private MobileDataEnabledListener mDataContentObserver; - private CallingPreferenceCategoryController mCallingPreferenceCategoryController; - - public VideoCallingPreferenceController(Context context, String key) { - super(context, key); - mDataContentObserver = new MobileDataEnabledListener(context, this); - mTelephonyCallback = new PhoneTelephonyCallback(); - } - - @Override - public int getAvailabilityStatus(int subId) { - return SubscriptionManager.isValidSubscriptionId(subId) - && isVideoCallEnabled(subId) - ? AVAILABLE - : CONDITIONALLY_UNAVAILABLE; - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - mPreference = screen.findPreference(getPreferenceKey()); - } - - @Override - public void onStart() { - mTelephonyCallback.register(mContext, mSubId); - mDataContentObserver.start(mSubId); - } - - @Override - public void onStop() { - mTelephonyCallback.unregister(); - mDataContentObserver.stop(); - } - - @Override - public void updateState(Preference preference) { - super.updateState(preference); - if ((mCallState == null) || (preference == null)) { - Log.d(TAG, "Skip update under mCallState=" + mCallState); - return; - } - final TwoStatePreference switchPreference = (TwoStatePreference) preference; - final boolean videoCallEnabled = isVideoCallEnabled(mSubId); - switchPreference.setVisible(videoCallEnabled); - mCallingPreferenceCategoryController - .updateChildVisible(getPreferenceKey(), videoCallEnabled); - if (videoCallEnabled) { - final boolean videoCallEditable = queryVoLteState(mSubId).isEnabledByUser() - && queryImsState(mSubId).isAllowUserControl(); - preference.setEnabled(videoCallEditable - && mCallState == TelephonyManager.CALL_STATE_IDLE); - switchPreference.setChecked(videoCallEditable && isChecked()); - } - } - - @Override - public boolean setChecked(boolean isChecked) { - if (!SubscriptionManager.isValidSubscriptionId(mSubId)) { - return false; - } - final ImsMmTelManager imsMmTelManager = ImsMmTelManager.createForSubscriptionId(mSubId); - if (imsMmTelManager == null) { - return false; - } - try { - imsMmTelManager.setVtSettingEnabled(isChecked); - return true; - } catch (IllegalArgumentException exception) { - Log.w(TAG, "Unable to set VT status " + isChecked + ". subId=" + mSubId, - exception); - } - return false; - } - - @Override - public boolean isChecked() { - return queryImsState(mSubId).isEnabledByUser(); - } - - @VisibleForTesting - protected boolean isImsSupported() { - return mContext.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_TELEPHONY_IMS); - } - - /** - * Init instance of VideoCallingPreferenceController. - */ - public VideoCallingPreferenceController init( - int subId, CallingPreferenceCategoryController callingPreferenceCategoryController) { - mSubId = subId; - mCallingPreferenceCategoryController = callingPreferenceCategoryController; - - return this; - } - - @VisibleForTesting - boolean isVideoCallEnabled(int subId) { - if (!SubscriptionManager.isValidSubscriptionId(subId)) { - return false; - } - - final PersistableBundle carrierConfig = - CarrierConfigCache.getInstance(mContext).getConfigForSubId(subId); - if (carrierConfig == null) { - return false; - } - - if (!carrierConfig.getBoolean( - CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS) - && (!mContext.getSystemService(TelephonyManager.class) - .createForSubscriptionId(subId).isDataEnabled())) { - return false; - } - - return isImsSupported() && queryImsState(subId).isReadyToVideoCall(); - } - - @Override - public void on4gLteUpdated() { - updateState(mPreference); - } - - private class PhoneTelephonyCallback extends TelephonyCallback implements - TelephonyCallback.CallStateListener { - - private TelephonyManager mTelephonyManager; - - @Override - public void onCallStateChanged(int state) { - mCallState = state; - updateState(mPreference); - } - - public void register(Context context, int subId) { - mTelephonyManager = context.getSystemService(TelephonyManager.class); - if (SubscriptionManager.isValidSubscriptionId(subId)) { - mTelephonyManager = mTelephonyManager.createForSubscriptionId(subId); - } - // assign current call state so that it helps to show correct preference state even - // before first onCallStateChanged() by initial registration. - if (Flags.enforceTelephonyFeatureMappingForPublicApis()) { - try { - mCallState = mTelephonyManager.getCallState(subId); - } catch (UnsupportedOperationException e) { - // Device doesn't support FEATURE_TELEPHONY_CALLING - mCallState = TelephonyManager.CALL_STATE_IDLE; - } - } else { - mCallState = mTelephonyManager.getCallState(subId); - } - mTelephonyManager.registerTelephonyCallback(context.getMainExecutor(), this); - } - - public void unregister() { - mCallState = null; - mTelephonyManager.unregisterTelephonyCallback(this); - } - } - - /** - * Implementation of MobileDataEnabledListener.Client - */ - public void onMobileDataEnabledChange() { - updateState(mPreference); - } - - @VisibleForTesting - VtQueryImsState queryImsState(int subId) { - return new VtQueryImsState(mContext, subId); - } - - @VisibleForTesting - VolteQueryImsState queryVoLteState(int subId) { - return new VolteQueryImsState(mContext, subId); - } -} diff --git a/src/com/android/settings/network/telephony/VideoCallingPreferenceController.kt b/src/com/android/settings/network/telephony/VideoCallingPreferenceController.kt new file mode 100644 index 00000000000..e6b3f315425 --- /dev/null +++ b/src/com/android/settings/network/telephony/VideoCallingPreferenceController.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2024 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.network.telephony + +import android.content.Context +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.telephony.ims.ImsManager +import android.util.Log +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LifecycleOwner +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import androidx.preference.TwoStatePreference +import com.android.settings.R +import com.android.settings.core.TogglePreferenceController +import com.android.settings.network.ims.VolteQueryImsState +import com.android.settings.network.ims.VtQueryImsState +import com.android.settings.network.telephony.Enhanced4gBasePreferenceController.On4gLteUpdateListener +import com.android.settings.network.telephony.MobileNetworkSettingsSearchIndex.MobileNetworkSettingsSearchItem +import com.android.settings.network.telephony.MobileNetworkSettingsSearchIndex.MobileNetworkSettingsSearchResult +import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking + +/** Preference controller for "Video Calling" */ +class VideoCallingPreferenceController +@JvmOverloads +constructor( + context: Context, + key: String, + private val callStateRepository: CallStateRepository = CallStateRepository(context), +) : TogglePreferenceController(context, key), On4gLteUpdateListener { + + private var subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID + private var preference: TwoStatePreference? = null + private var callingPreferenceCategoryController: CallingPreferenceCategoryController? = null + private val repository = VideoCallingRepository(context) + + private var videoCallEditable = false + private var isInCall = false + + /** Init instance of VideoCallingPreferenceController. */ + fun init( + subId: Int, + callingPreferenceCategoryController: CallingPreferenceCategoryController?, + ): VideoCallingPreferenceController { + this.subId = subId + this.callingPreferenceCategoryController = callingPreferenceCategoryController + + return this + } + + // Availability is controlled in onViewCreated() and VideoCallingSearchItem. + override fun getAvailabilityStatus() = AVAILABLE + + override fun displayPreference(screen: PreferenceScreen) { + super.displayPreference(screen) + preference = screen.findPreference(preferenceKey) + } + + override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { + repository.isVideoCallReadyFlow(subId).collectLatestWithLifecycle(viewLifecycleOwner) { + isReady -> + preference?.isVisible = isReady + callingPreferenceCategoryController?.updateChildVisible(preferenceKey, isReady) + } + callStateRepository.callStateFlow(subId).collectLatestWithLifecycle(viewLifecycleOwner) { + callState -> + isInCall = callState != TelephonyManager.CALL_STATE_IDLE + updatePreference() + } + } + + override fun updateState(preference: Preference) { + super.updateState(preference) + videoCallEditable = + queryVoLteState(subId).isEnabledByUser && queryImsState(subId).isAllowUserControl + updatePreference() + } + + private fun updatePreference() { + preference?.isEnabled = videoCallEditable && !isInCall + preference?.isChecked = videoCallEditable && isChecked + } + + override fun getSliceHighlightMenuRes() = NO_RES + + override fun setChecked(isChecked: Boolean): Boolean { + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + return false + } + val imsMmTelManager = ImsManager(mContext).getImsMmTelManager(subId) + try { + imsMmTelManager.isVtSettingEnabled = isChecked + return true + } catch (exception: IllegalArgumentException) { + Log.w(TAG, "[$subId] Unable to set VT status $isChecked", exception) + } + return false + } + + override fun isChecked(): Boolean = queryImsState(subId).isEnabledByUser + + override fun on4gLteUpdated() { + preference?.let { updateState(it) } + } + + @VisibleForTesting fun queryImsState(subId: Int) = VtQueryImsState(mContext, subId) + + @VisibleForTesting fun queryVoLteState(subId: Int) = VolteQueryImsState(mContext, subId) + + companion object { + private const val TAG = "VideoCallingPreferenceController" + + class VideoCallingSearchItem(private val context: Context) : + MobileNetworkSettingsSearchItem { + private val repository = VideoCallingRepository(context) + + private fun isAvailable(subId: Int): Boolean = runBlocking { + repository.isVideoCallReadyFlow(subId).first() + } + + override fun getSearchResult(subId: Int): MobileNetworkSettingsSearchResult? { + if (!isAvailable(subId)) return null + return MobileNetworkSettingsSearchResult( + key = "video_calling_key", + title = context.getString(R.string.video_calling_settings_title), + ) + } + } + } +} diff --git a/src/com/android/settings/network/telephony/VideoCallingRepository.kt b/src/com/android/settings/network/telephony/VideoCallingRepository.kt new file mode 100644 index 00000000000..634eb285cdd --- /dev/null +++ b/src/com/android/settings/network/telephony/VideoCallingRepository.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 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.network.telephony + +import android.content.Context +import android.telephony.AccessNetworkConstants +import android.telephony.CarrierConfigManager +import android.telephony.SubscriptionManager +import android.telephony.ims.feature.MmTelFeature +import android.telephony.ims.stub.ImsRegistrationImplBase +import com.android.settings.network.telephony.ims.ImsFeatureRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf + +@OptIn(ExperimentalCoroutinesApi::class) +class VideoCallingRepository( + context: Context, + private val mobileDataRepository: MobileDataRepository = MobileDataRepository(context), + private val imsFeatureRepositoryFactory: (Int) -> ImsFeatureRepository = { subId -> + ImsFeatureRepository(context, subId) + }, +) { + private val carrierConfigRepository = CarrierConfigRepository(context) + + fun isVideoCallReadyFlow(subId: Int): Flow { + if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false) + + return isPreconditionMeetFlow(subId).flatMapLatest { isPreconditionMeet -> + if (isPreconditionMeet) { + imsFeatureRepositoryFactory(subId) + .isReadyFlow( + capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO, + tech = ImsRegistrationImplBase.REGISTRATION_TECH_LTE, + transportType = AccessNetworkConstants.TRANSPORT_TYPE_WWAN, + ) + } else { + flowOf(false) + } + } + } + + private fun isPreconditionMeetFlow(subId: Int): Flow = + if (carrierConfigRepository.getBoolean( + subId, CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS)) { + flowOf(true) + } else { + mobileDataRepository.isMobileDataEnabledFlow(subId) + } +} diff --git a/tests/robotests/src/com/android/settings/network/ims/MockVolteQueryImsState.java b/tests/robotests/src/com/android/settings/network/ims/MockVolteQueryImsState.java deleted file mode 100644 index 515ab5b659b..00000000000 --- a/tests/robotests/src/com/android/settings/network/ims/MockVolteQueryImsState.java +++ /dev/null @@ -1,105 +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.network.ims; - -import android.content.Context; -import android.telephony.ims.ImsException; - -/** - * Controller class for mock VoLte status - */ -public class MockVolteQueryImsState extends VolteQueryImsState { - - private Boolean mIsTtyOnVolteEnabled; - private Boolean mIsSupported; - private Boolean mIsProvisionedOnDevice; - private Boolean mIsServiceStateReady; - private Boolean mIsEnabledByUser; - - /** - * Constructor - * - * @param context {@link Context} - * @param subId subscription's id - */ - public MockVolteQueryImsState(Context context, int subId) { - super(context, subId); - } - - public void setIsTtyOnVolteEnabled(boolean enabled) { - mIsTtyOnVolteEnabled = enabled; - } - - @Override - boolean isTtyOnVolteEnabled(int subId) { - if (mIsTtyOnVolteEnabled != null) { - return mIsTtyOnVolteEnabled; - } - return super.isTtyOnVolteEnabled(subId); - } - - public void setEnabledByPlatform(boolean isSupported) { - mIsSupported = isSupported; - } - - @Override - boolean isEnabledByPlatform(int subId) throws InterruptedException, ImsException, - IllegalArgumentException { - if (mIsSupported != null) { - return mIsSupported; - } - return super.isEnabledByPlatform(subId); - } - - public void setIsProvisionedOnDevice(boolean isProvisioned) { - mIsProvisionedOnDevice = isProvisioned; - } - - @Override - boolean isProvisionedOnDevice(int subId) { - if (mIsProvisionedOnDevice != null) { - return mIsProvisionedOnDevice; - } - return super.isProvisionedOnDevice(subId); - } - - public void setServiceStateReady(boolean isReady) { - mIsServiceStateReady = isReady; - } - - @Override - boolean isServiceStateReady(int subId) throws InterruptedException, ImsException, - IllegalArgumentException { - if (mIsServiceStateReady != null) { - return mIsServiceStateReady; - } - return super.isServiceStateReady(subId); - } - - public void setIsEnabledByUser(boolean enabled) { - mIsEnabledByUser = enabled; - } - - @Override - boolean isEnabledByUser(int subId) { - if (mIsEnabledByUser != null) { - return mIsEnabledByUser; - } - return super.isEnabledByUser(subId); - } - -} diff --git a/tests/robotests/src/com/android/settings/network/ims/MockVtQueryImsState.java b/tests/robotests/src/com/android/settings/network/ims/MockVtQueryImsState.java deleted file mode 100644 index 0949f1c0262..00000000000 --- a/tests/robotests/src/com/android/settings/network/ims/MockVtQueryImsState.java +++ /dev/null @@ -1,104 +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.network.ims; - -import android.content.Context; -import android.telephony.ims.ImsException; - -/** - * Controller class for mock VT status - */ -public class MockVtQueryImsState extends VtQueryImsState { - - private Boolean mIsTtyOnVolteEnabled; - private Boolean mIsEnabledOnPlatform; - private Boolean mIsProvisionedOnDevice; - private Boolean mIsEnabledByUser; - private Boolean mIsServiceStateReady; - - /** - * Constructor - * - * @param context {@link Context} - * @param subId subscription's id - */ - public MockVtQueryImsState(Context context, int subId) { - super(context, subId); - } - - public void setIsTtyOnVolteEnabled(boolean enabled) { - mIsTtyOnVolteEnabled = enabled; - } - - @Override - boolean isTtyOnVolteEnabled(int subId) { - if (mIsTtyOnVolteEnabled != null) { - return mIsTtyOnVolteEnabled; - } - return super.isTtyOnVolteEnabled(subId); - } - - public void setIsEnabledByPlatform(boolean isEnabled) { - mIsEnabledOnPlatform = isEnabled; - } - - @Override - boolean isEnabledByPlatform(int subId) throws InterruptedException, ImsException, - IllegalArgumentException { - if (mIsEnabledOnPlatform != null) { - return mIsEnabledOnPlatform; - } - return super.isEnabledByPlatform(subId); - } - - public void setIsProvisionedOnDevice(boolean isProvisioned) { - mIsProvisionedOnDevice = isProvisioned; - } - - @Override - boolean isProvisionedOnDevice(int subId) { - if (mIsProvisionedOnDevice != null) { - return mIsProvisionedOnDevice; - } - return super.isProvisionedOnDevice(subId); - } - - public void setServiceStateReady(boolean isReady) { - mIsServiceStateReady = isReady; - } - - @Override - boolean isServiceStateReady(int subId) throws InterruptedException, ImsException, - IllegalArgumentException { - if (mIsServiceStateReady != null) { - return mIsServiceStateReady; - } - return super.isServiceStateReady(subId); - } - - public void setIsEnabledByUser(boolean enabled) { - mIsEnabledByUser = enabled; - } - - @Override - boolean isEnabledByUser(int subId) { - if (mIsEnabledByUser != null) { - return mIsEnabledByUser; - } - return super.isEnabledByUser(subId); - } -} diff --git a/tests/robotests/src/com/android/settings/network/telephony/VideoCallingPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/telephony/VideoCallingPreferenceControllerTest.java deleted file mode 100644 index da8958dff2e..00000000000 --- a/tests/robotests/src/com/android/settings/network/telephony/VideoCallingPreferenceControllerTest.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (C) 2018 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.network.telephony; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; - -import android.content.Context; -import android.os.PersistableBundle; -import android.telephony.CarrierConfigManager; -import android.telephony.TelephonyManager; -import android.telephony.ims.ProvisioningManager; - -import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreference; - -import com.android.settings.network.CarrierConfigCache; -import com.android.settings.network.ims.MockVolteQueryImsState; -import com.android.settings.network.ims.MockVtQueryImsState; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; - -@RunWith(RobolectricTestRunner.class) -public class VideoCallingPreferenceControllerTest { - private static final int SUB_ID = 2; - - @Mock - private TelephonyManager mTelephonyManager; - @Mock - private ProvisioningManager mProvisioningManager; - @Mock - private CarrierConfigCache mCarrierConfigCache; - @Mock - private PreferenceScreen mPreferenceScreen; - - private MockVtQueryImsState mQueryImsState; - private MockVolteQueryImsState mQueryVoLteState; - - private VideoCallingPreferenceController mController; - private PersistableBundle mCarrierConfig; - private SwitchPreference mPreference; - private Context mContext; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - - mContext = spy(RuntimeEnvironment.application); - doReturn(mTelephonyManager).when(mContext).getSystemService(TelephonyManager.class); - CarrierConfigCache.setTestInstance(mContext, mCarrierConfigCache); - doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(SUB_ID); - - mCarrierConfig = new PersistableBundle(); - mCarrierConfig.putBoolean( - CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS, true); - doReturn(mCarrierConfig).when(mCarrierConfigCache).getConfigForSubId(SUB_ID); - - mQueryImsState = new MockVtQueryImsState(mContext, SUB_ID); - mQueryImsState.setIsEnabledByUser(true); - - mQueryVoLteState = new MockVolteQueryImsState(mContext, SUB_ID); - mQueryVoLteState.setIsEnabledByUser(true); - - mPreference = new SwitchPreference(mContext); - mController = spy(new VideoCallingPreferenceController(mContext, "wifi_calling")); - mController.init( - SUB_ID, new CallingPreferenceCategoryController(mContext, "calling_category")); - doReturn(mQueryImsState).when(mController).queryImsState(anyInt()); - doReturn(mQueryVoLteState).when(mController).queryVoLteState(anyInt()); - doReturn(true).when(mController).isImsSupported(); - mPreference.setKey(mController.getPreferenceKey()); - - mQueryImsState.setIsEnabledByPlatform(true); - mQueryImsState.setIsProvisionedOnDevice(true); - mQueryImsState.setServiceStateReady(true); - doReturn(true).when(mTelephonyManager).isDataEnabled(); - - mController.mCallState = TelephonyManager.CALL_STATE_IDLE; - } - - @Test - public void isVideoCallEnabled_allFlagsOn_returnTrue() { - assertThat(mController.isVideoCallEnabled(SUB_ID)).isTrue(); - } - - @Test - public void isVideoCallEnabled_disabledByPlatform_returnFalse() { - mQueryImsState.setIsProvisionedOnDevice(false); - mQueryImsState.setIsEnabledByPlatform(false); - - assertThat(mController.isVideoCallEnabled(SUB_ID)).isFalse(); - } - - @Test - public void isVideoCallEnabled_dataDisabled_returnFalse() { - mCarrierConfig.putBoolean( - CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS, false); - doReturn(false).when(mTelephonyManager).isDataEnabled(); - - assertThat(mController.isVideoCallEnabled(SUB_ID)).isFalse(); - } - - @Test - public void updateState_4gLteOff_disabled() { - mQueryImsState.setIsEnabledByUser(false); - mQueryVoLteState.setIsEnabledByUser(false); - - mController.updateState(mPreference); - - assertThat(mPreference.isEnabled()).isFalse(); - assertThat(mPreference.isChecked()).isFalse(); - } - - @Test - public void updateState_4gLteOnWithoutCall_checked() { - mQueryImsState.setIsEnabledByUser(true); - mQueryVoLteState.setIsEnabledByUser(true); - mQueryImsState.setIsTtyOnVolteEnabled(true); - mController.mCallState = TelephonyManager.CALL_STATE_IDLE; - - mController.updateState(mPreference); - - assertThat(mPreference.isEnabled()).isTrue(); - assertThat(mPreference.isChecked()).isTrue(); - } - - - @Test - public void displayPreference_notAvailable_setPreferenceInvisible() { - mQueryImsState.setIsEnabledByPlatform(false); - - mController.displayPreference(mPreferenceScreen); - - assertThat(mPreferenceScreen.isVisible()).isFalse(); - } - -} diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/VideoCallingPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/VideoCallingPreferenceControllerTest.kt new file mode 100644 index 00000000000..4babfaae301 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/VideoCallingPreferenceControllerTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2024 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.network.telephony + +import android.content.Context +import android.telephony.TelephonyManager +import androidx.lifecycle.testing.TestLifecycleOwner +import androidx.preference.PreferenceManager +import androidx.preference.SwitchPreferenceCompat +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.network.ims.VolteQueryImsState +import com.android.settings.network.ims.VtQueryImsState +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class VideoCallingPreferenceControllerTest { + + private val mockVtQueryImsState = mock {} + + private var mockQueryVoLteState = mock {} + + private val context: Context = ApplicationProvider.getApplicationContext() + + private val mockCallStateRepository = mock {} + + private var controller = + spy( + VideoCallingPreferenceController( + context = context, + key = TEST_KEY, + callStateRepository = mockCallStateRepository, + ) + ) { + on { queryImsState(SUB_ID) } doReturn mockVtQueryImsState + on { queryVoLteState(SUB_ID) } doReturn mockQueryVoLteState + } + + private val preference = SwitchPreferenceCompat(context).apply { key = TEST_KEY } + private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context) + + @Before + fun setUp() { + controller.init(SUB_ID, CallingPreferenceCategoryController(context, "calling_category")) + preferenceScreen.addPreference(preference) + controller.displayPreference(preferenceScreen) + } + + @Test + fun updateState_4gLteOff_disabledAndUnchecked() { + mockQueryVoLteState.stub { on { isEnabledByUser } doReturn false } + + controller.updateState(preference) + + assertThat(preference.isEnabled).isFalse() + assertThat(preference.isChecked).isFalse() + } + + @Test + fun updateState_4gLteOnWithoutCall_enabledAndChecked() = runBlocking { + mockVtQueryImsState.stub { + on { isEnabledByUser } doReturn true + on { isAllowUserControl } doReturn true + } + mockQueryVoLteState.stub { on { isEnabledByUser } doReturn true } + mockCallStateRepository.stub { + on { callStateFlow(SUB_ID) } doReturn flowOf(TelephonyManager.CALL_STATE_IDLE) + } + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + controller.updateState(preference) + + assertThat(preference.isEnabled).isTrue() + assertThat(preference.isChecked).isTrue() + } + + @Test + fun updateState_4gLteOnWithCall_disabledAndChecked() = runBlocking { + mockVtQueryImsState.stub { + on { isEnabledByUser } doReturn true + on { isAllowUserControl } doReturn true + } + mockQueryVoLteState.stub { on { isEnabledByUser } doReturn true } + mockCallStateRepository.stub { + on { callStateFlow(SUB_ID) } doReturn flowOf(TelephonyManager.CALL_STATE_RINGING) + } + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + controller.updateState(preference) + + assertThat(preference.isEnabled).isFalse() + assertThat(preference.isChecked).isTrue() + } + + private companion object { + const val TEST_KEY = "test_key" + const val SUB_ID = 10 + } +} diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/VideoCallingRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/VideoCallingRepositoryTest.kt new file mode 100644 index 00000000000..063e1918a89 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/VideoCallingRepositoryTest.kt @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2024 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.network.telephony + +import android.content.Context +import android.telephony.AccessNetworkConstants +import android.telephony.CarrierConfigManager +import android.telephony.SubscriptionManager +import android.telephony.ims.feature.MmTelFeature +import android.telephony.ims.stub.ImsRegistrationImplBase +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.network.telephony.ims.ImsFeatureRepository +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class VideoCallingRepositoryTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + private val mockMobileDataRepository = mock() + private val mockImsFeatureRepository = mock() + + private val repository = + VideoCallingRepository( + context = context, + mobileDataRepository = mockMobileDataRepository, + imsFeatureRepositoryFactory = { mockImsFeatureRepository }, + ) + + @Before + fun setUp() { + CarrierConfigRepository.resetForTest() + } + + @Test + fun isVideoCallReadyFlow_invalidSubId() = runBlocking { + val isVideoCallReady = + repository + .isVideoCallReadyFlow(subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID) + .firstWithTimeoutOrNull() + + assertThat(isVideoCallReady).isFalse() + } + + @Test + fun isVideoCallReadyFlow_ignoreDataEnabledChangedAndIsReady_returnTrue() = runBlocking { + CarrierConfigRepository.setBooleanForTest( + subId = SUB_ID, + key = CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS, + value = true, + ) + mockImsFeatureRepository.stub { + on { + isReadyFlow( + capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO, + tech = ImsRegistrationImplBase.REGISTRATION_TECH_LTE, + transportType = AccessNetworkConstants.TRANSPORT_TYPE_WWAN, + ) + } doReturn flowOf(true) + } + + val isVideoCallReady = repository.isVideoCallReadyFlow(SUB_ID).firstWithTimeoutOrNull() + + assertThat(isVideoCallReady).isTrue() + } + + @Test + fun isVideoCallReadyFlow_ignoreDataEnabledChangedAndNotReady_returnFalse() = runBlocking { + CarrierConfigRepository.setBooleanForTest( + subId = SUB_ID, + key = CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS, + value = true, + ) + mockImsFeatureRepository.stub { + on { + isReadyFlow( + capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO, + tech = ImsRegistrationImplBase.REGISTRATION_TECH_LTE, + transportType = AccessNetworkConstants.TRANSPORT_TYPE_WWAN, + ) + } doReturn flowOf(false) + } + + val isVideoCallReady = repository.isVideoCallReadyFlow(SUB_ID).firstWithTimeoutOrNull() + + assertThat(isVideoCallReady).isFalse() + } + + @Test + fun isVideoCallReadyFlow_mobileDataEnabledAndIsReady_returnTrue() = runBlocking { + CarrierConfigRepository.setBooleanForTest( + subId = SUB_ID, + key = CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS, + value = false, + ) + mockMobileDataRepository.stub { + on { isMobileDataEnabledFlow(SUB_ID) } doReturn flowOf(true) + } + mockImsFeatureRepository.stub { + on { + isReadyFlow( + capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO, + tech = ImsRegistrationImplBase.REGISTRATION_TECH_LTE, + transportType = AccessNetworkConstants.TRANSPORT_TYPE_WWAN, + ) + } doReturn flowOf(true) + } + + val isVideoCallReady = repository.isVideoCallReadyFlow(SUB_ID).firstWithTimeoutOrNull() + + assertThat(isVideoCallReady).isTrue() + } + + @Test + fun isVideoCallReadyFlow_ignoreDataEnabledChangedAndIsReady_returnFalse() = runBlocking { + CarrierConfigRepository.setBooleanForTest( + subId = SUB_ID, + key = CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS, + value = false, + ) + mockMobileDataRepository.stub { + on { isMobileDataEnabledFlow(SUB_ID) } doReturn flowOf(false) + } + mockImsFeatureRepository.stub { + on { + isReadyFlow( + capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO, + tech = ImsRegistrationImplBase.REGISTRATION_TECH_LTE, + transportType = AccessNetworkConstants.TRANSPORT_TYPE_WWAN, + ) + } doReturn flowOf(true) + } + + val isVideoCallReady = repository.isVideoCallReadyFlow(SUB_ID).firstWithTimeoutOrNull() + + assertThat(isVideoCallReady).isFalse() + } + + private companion object { + const val SUB_ID = 10 + } +} diff --git a/tests/unit/src/com/android/settings/network/ims/MockVtQueryImsState.java b/tests/unit/src/com/android/settings/network/ims/MockVtQueryImsState.java deleted file mode 100644 index 0949f1c0262..00000000000 --- a/tests/unit/src/com/android/settings/network/ims/MockVtQueryImsState.java +++ /dev/null @@ -1,104 +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.network.ims; - -import android.content.Context; -import android.telephony.ims.ImsException; - -/** - * Controller class for mock VT status - */ -public class MockVtQueryImsState extends VtQueryImsState { - - private Boolean mIsTtyOnVolteEnabled; - private Boolean mIsEnabledOnPlatform; - private Boolean mIsProvisionedOnDevice; - private Boolean mIsEnabledByUser; - private Boolean mIsServiceStateReady; - - /** - * Constructor - * - * @param context {@link Context} - * @param subId subscription's id - */ - public MockVtQueryImsState(Context context, int subId) { - super(context, subId); - } - - public void setIsTtyOnVolteEnabled(boolean enabled) { - mIsTtyOnVolteEnabled = enabled; - } - - @Override - boolean isTtyOnVolteEnabled(int subId) { - if (mIsTtyOnVolteEnabled != null) { - return mIsTtyOnVolteEnabled; - } - return super.isTtyOnVolteEnabled(subId); - } - - public void setIsEnabledByPlatform(boolean isEnabled) { - mIsEnabledOnPlatform = isEnabled; - } - - @Override - boolean isEnabledByPlatform(int subId) throws InterruptedException, ImsException, - IllegalArgumentException { - if (mIsEnabledOnPlatform != null) { - return mIsEnabledOnPlatform; - } - return super.isEnabledByPlatform(subId); - } - - public void setIsProvisionedOnDevice(boolean isProvisioned) { - mIsProvisionedOnDevice = isProvisioned; - } - - @Override - boolean isProvisionedOnDevice(int subId) { - if (mIsProvisionedOnDevice != null) { - return mIsProvisionedOnDevice; - } - return super.isProvisionedOnDevice(subId); - } - - public void setServiceStateReady(boolean isReady) { - mIsServiceStateReady = isReady; - } - - @Override - boolean isServiceStateReady(int subId) throws InterruptedException, ImsException, - IllegalArgumentException { - if (mIsServiceStateReady != null) { - return mIsServiceStateReady; - } - return super.isServiceStateReady(subId); - } - - public void setIsEnabledByUser(boolean enabled) { - mIsEnabledByUser = enabled; - } - - @Override - boolean isEnabledByUser(int subId) { - if (mIsEnabledByUser != null) { - return mIsEnabledByUser; - } - return super.isEnabledByUser(subId); - } -} From 840495eb5cf810f6520661a4ca5fbf616547d8f5 Mon Sep 17 00:00:00 2001 From: Yiyi Shen Date: Wed, 28 Aug 2024 18:05:59 +0800 Subject: [PATCH 09/12] [Audiosharing] Fix text truncated Fix: 359814312 Test: manual Flag: com.android.settingslib.flags.enable_le_audio_sharing Change-Id: Ia278d30019efad4a035d02f7ea3812e84dfdba8f --- res/layout/dialog_custom_title_audio_sharing.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/layout/dialog_custom_title_audio_sharing.xml b/res/layout/dialog_custom_title_audio_sharing.xml index 86e00108e9a..692cbc79a51 100644 --- a/res/layout/dialog_custom_title_audio_sharing.xml +++ b/res/layout/dialog_custom_title_audio_sharing.xml @@ -35,7 +35,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" - android:maxLines="2" + android:maxLines="3" + android:ellipsize="end" android:paddingTop="14dp" android:textAlignment="center" android:textSize="24sp" /> From 1d92232b848a2b486dcc22988dd94a887821f69a Mon Sep 17 00:00:00 2001 From: Weng Su Date: Thu, 29 Aug 2024 17:08:25 +0800 Subject: [PATCH 10/12] Add WIFI_LEVEL_UNREACHABLE logs - Add WIFI_LEVEL_UNREACHABLE logs in Wi-Fi details settings Bug: 362269742 Flag: EXEMPT add logs only Test: Manual testing Change-Id: I9a2b01df06417bafa85d300f1abe41797ec08143 --- .../settings/wifi/details2/WifiDetailPreferenceController2.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java index 9992cc047c2..927d2ee93e6 100644 --- a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java +++ b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java @@ -556,6 +556,7 @@ public class WifiDetailPreferenceController2 extends AbstractPreferenceControlle return mContext.getDrawable(getHotspotIconResource(deviceType)); } if (mWifiEntry.getLevel() == WifiEntry.WIFI_LEVEL_UNREACHABLE) { + Log.w(TAG, "WiFi level is WIFI_LEVEL_UNREACHABLE(-1)"); return mContext.getDrawable(R.drawable.empty_icon); } return mIconInjector.getIcon(wifiEntry.shouldShowXLevelIcon(), wifiEntry.getLevel()); From 29c7cc0cb54f1a0a5c3f60c3674a756e930d3174 Mon Sep 17 00:00:00 2001 From: "Chaitanya Cheemala (xWF)" Date: Thu, 29 Aug 2024 10:25:04 +0000 Subject: [PATCH 11/12] Revert "Show a dialog if bluetooth key is missing when reconnecting" Revert submission 28985385-bt_key_missing_dialog Reason for revert: Likely culprit for b/362901443 - verifying through ABTD before revert submission. This is part of the standard investigation process, and does not mean your CL will be reverted. Reverted changes: /q/submissionid:28985385-bt_key_missing_dialog Change-Id: I2a3ab8f0dbd2c81eda49d11abc6d586f59f149e9 --- AndroidManifest.xml | 20 --- .../settings_bluetooth_declarations.aconfig | 10 -- res/layout/bluetooth_key_missing.xml | 58 ------- res/values/strings.xml | 9 - .../bluetooth/BluetoothKeyMissingDialog.java | 47 ----- .../BluetoothKeyMissingDialogFragment.java | 94 ---------- .../BluetoothKeyMissingReceiver.java | 122 ------------- .../BluetoothKeyMissingDialogTest.java | 76 --------- .../BluetoothKeyMissingReceiverTest.java | 160 ------------------ 9 files changed, 596 deletions(-) delete mode 100644 res/layout/bluetooth_key_missing.xml delete mode 100644 src/com/android/settings/bluetooth/BluetoothKeyMissingDialog.java delete mode 100644 src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java delete mode 100644 src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java delete mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogTest.java delete mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiverTest.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 13aafc9dbb7..7b79611ca65 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3230,19 +3230,6 @@ - - - - - - - - - - - - - diff --git a/aconfig/settings_bluetooth_declarations.aconfig b/aconfig/settings_bluetooth_declarations.aconfig index 0c423b5a1b7..3d14288fcd2 100644 --- a/aconfig/settings_bluetooth_declarations.aconfig +++ b/aconfig/settings_bluetooth_declarations.aconfig @@ -34,13 +34,3 @@ flag { purpose: PURPOSE_BUGFIX } } - -flag { - name: "enable_bluetooth_key_missing_dialog" - namespace: "cross_device_experiences" - description: "Show a dialog if the bluetooth key is missing when reconnecting" - bug: "360031750" - metadata { - purpose: PURPOSE_BUGFIX - } -} diff --git a/res/layout/bluetooth_key_missing.xml b/res/layout/bluetooth_key_missing.xml deleted file mode 100644 index b9f8d866bd3..00000000000 --- a/res/layout/bluetooth_key_missing.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 17a9a08f48d..0fcb0d68502 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1860,15 +1860,6 @@ Change - - %1$s not connected - - For your security, forget this device, then pair it again - - Forget device - - Cancel - Device details diff --git a/src/com/android/settings/bluetooth/BluetoothKeyMissingDialog.java b/src/com/android/settings/bluetooth/BluetoothKeyMissingDialog.java deleted file mode 100644 index 46975f77726..00000000000 --- a/src/com/android/settings/bluetooth/BluetoothKeyMissingDialog.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2024 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.bluetooth; - -import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; - -import android.bluetooth.BluetoothDevice; -import android.content.Intent; -import android.os.Bundle; - -import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentActivity; - -/** A dialog to ask the user to forget a bluetooth device when the key is missing. */ -public class BluetoothKeyMissingDialog extends FragmentActivity { - public static final String FRAGMENT_TAG = "BtKeyMissingFrg"; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); - Intent intent = getIntent(); - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - if (device == null) { - finish(); - return; - } - BluetoothKeyMissingDialogFragment fragment = new BluetoothKeyMissingDialogFragment(device); - fragment.show(getSupportFragmentManager(), FRAGMENT_TAG); - closeSystemDialogs(); - } -} diff --git a/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java b/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java deleted file mode 100644 index a8e3aae175a..00000000000 --- a/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogFragment.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2024 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.bluetooth; - -import android.app.Dialog; -import android.app.settings.SettingsEnums; -import android.bluetooth.BluetoothDevice; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.os.Bundle; -import android.util.Log; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; - -import com.android.settings.R; -import com.android.settings.core.instrumentation.InstrumentedDialogFragment; - -/** - * A dialogFragment used by {@link BluetoothKeyMissingDialog} to create a dialog for the - * bluetooth device. - */ -public class BluetoothKeyMissingDialogFragment extends InstrumentedDialogFragment - implements OnClickListener { - - private static final String TAG = "BTKeyMissingDialogFragment"; - - private BluetoothDevice mBluetoothDevice; - - public BluetoothKeyMissingDialogFragment(@NonNull BluetoothDevice bluetoothDevice) { - mBluetoothDevice = bluetoothDevice; - } - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - View view = getActivity().getLayoutInflater().inflate(R.layout.bluetooth_key_missing, null); - TextView keyMissingTitle = view.findViewById(R.id.bluetooth_key_missing_title); - keyMissingTitle.setText( - getString(R.string.bluetooth_key_missing_title, mBluetoothDevice.getName())); - builder.setView(view); - builder.setPositiveButton(getString(R.string.bluetooth_key_missing_forget), this); - builder.setNegativeButton(getString(R.string.bluetooth_key_missing_cancel), this); - AlertDialog dialog = builder.create(); - dialog.setCanceledOnTouchOutside(false); - return dialog; - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (!getActivity().isFinishing()) { - getActivity().finish(); - } - } - - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == DialogInterface.BUTTON_POSITIVE) { - Log.i( - TAG, - "Positive button clicked, remove bond for " - + mBluetoothDevice.getAnonymizedAddress()); - mBluetoothDevice.removeBond(); - } else if (which == DialogInterface.BUTTON_NEGATIVE) { - Log.i(TAG, "Negative button clicked for " + mBluetoothDevice.getAnonymizedAddress()); - } - if (!getActivity().isFinishing()) { - getActivity().finish(); - } - } - - @Override - public int getMetricsCategory() { - return SettingsEnums.BLUETOOTH_KEY_MISSING_DIALOG_FRAGMENT; - } -} diff --git a/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java b/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java deleted file mode 100644 index d7a5343d694..00000000000 --- a/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiver.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2024 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.bluetooth; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.PowerManager; -import android.os.UserHandle; -import android.text.TextUtils; -import android.util.Log; - -import androidx.core.app.NotificationCompat; - -import com.android.settings.R; -import com.android.settings.flags.Flags; - -/** - * BluetoothKeyMissingReceiver is a receiver for Bluetooth key missing error when reconnecting to a - * bonded bluetooth device. - */ -public final class BluetoothKeyMissingReceiver extends BroadcastReceiver { - private static final String TAG = "BtKeyMissingReceiver"; - private static final String CHANNEL_ID = "bluetooth_notification_channel"; - private static final int NOTIFICATION_ID = android.R.drawable.stat_sys_data_bluetooth; - - @Override - public void onReceive(Context context, Intent intent) { - if (!Flags.enableBluetoothKeyMissingDialog()) { - return; - } - String action = intent.getAction(); - if (action == null) { - return; - } - - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - PowerManager powerManager = context.getSystemService(PowerManager.class); - if (TextUtils.equals(action, BluetoothDevice.ACTION_KEY_MISSING)) { - Log.d(TAG, "Receive ACTION_KEY_MISSING"); - if (shouldShowDialog(context, device, powerManager)) { - Intent pairingIntent = getKeyMissingDialogIntent(context, device); - Log.d(TAG, "Show key missing dialog:" + device); - context.startActivityAsUser(pairingIntent, UserHandle.CURRENT); - } else { - Log.d(TAG, "Show key missing notification: " + device); - showNotification(context, device); - } - } - } - - private Intent getKeyMissingDialogIntent(Context context, BluetoothDevice device) { - Intent pairingIntent = new Intent(); - pairingIntent.setClass(context, BluetoothKeyMissingDialog.class); - pairingIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); - pairingIntent.setAction(BluetoothDevice.ACTION_KEY_MISSING); - pairingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - return pairingIntent; - } - - private boolean shouldShowDialog( - Context context, BluetoothDevice device, PowerManager powerManager) { - return LocalBluetoothPreferences.shouldShowDialogInForeground(context, device) - && powerManager.isInteractive(); - } - - private void showNotification(Context context, BluetoothDevice bluetoothDevice) { - NotificationManager nm = context.getSystemService(NotificationManager.class); - NotificationChannel notificationChannel = - new NotificationChannel( - CHANNEL_ID, - context.getString(R.string.bluetooth), - NotificationManager.IMPORTANCE_HIGH); - nm.createNotificationChannel(notificationChannel); - - PendingIntent pairIntent = - PendingIntent.getActivity( - context, - 0, - getKeyMissingDialogIntent(context, bluetoothDevice), - PendingIntent.FLAG_ONE_SHOT - | PendingIntent.FLAG_UPDATE_CURRENT - | PendingIntent.FLAG_IMMUTABLE); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, - CHANNEL_ID) - .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) - .setTicker(context.getString(R.string.bluetooth_notif_ticker)) - .setLocalOnly(true); - builder.setContentTitle( - context.getString( - R.string.bluetooth_key_missing_title, bluetoothDevice.getName())) - .setContentText(context.getString(R.string.bluetooth_key_missing_message)) - .setContentIntent(pairIntent) - .setAutoCancel(true) - .setDefaults(Notification.DEFAULT_SOUND) - .setColor( - context.getColor( - com.android.internal.R.color.system_notification_accent_color)); - - nm.notify(NOTIFICATION_ID, builder.build()); - } -} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogTest.java deleted file mode 100644 index a47101e7b79..00000000000 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingDialogTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2024 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.bluetooth; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; - -import android.bluetooth.BluetoothDevice; - -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.FragmentActivity; - -import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -@RunWith(RobolectricTestRunner.class) -@Config(shadows = ShadowAlertDialogCompat.class) -public class BluetoothKeyMissingDialogTest { - @Mock private BluetoothDevice mBluetoothDevice; - - private BluetoothKeyMissingDialogFragment mFragment = null; - private FragmentActivity mActivity = null; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mActivity = Robolectric.setupActivity(FragmentActivity.class); - mFragment = new BluetoothKeyMissingDialogFragment(mBluetoothDevice); - mActivity - .getSupportFragmentManager() - .beginTransaction() - .add(mFragment, null) - .commit(); - shadowMainLooper().idle(); - } - - @Test - public void clickForgetDevice_removeBond() { - mFragment.onClick(mFragment.getDialog(), AlertDialog.BUTTON_POSITIVE); - - verify(mBluetoothDevice).removeBond(); - assertThat(mActivity.isFinishing()).isTrue(); - } - - @Test - public void clickCancel_notRemoveBond() { - mFragment.onClick(mFragment.getDialog(), AlertDialog.BUTTON_NEGATIVE); - - verify(mBluetoothDevice, never()).removeBond(); - assertThat(mActivity.isFinishing()).isTrue(); - } -} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiverTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiverTest.java deleted file mode 100644 index c764ed6cd97..00000000000 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothKeyMissingReceiverTest.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (C) 2024 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.bluetooth; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -import android.app.Notification; -import android.app.NotificationManager; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.UserHandle; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; - -import com.android.settings.flags.Flags; -import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; -import com.android.settings.testutils.shadow.ShadowBluetoothUtils; -import com.android.settingslib.bluetooth.LocalBluetoothManager; - -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.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; -import org.robolectric.shadow.api.Shadow; -import org.robolectric.shadows.ShadowApplication; - -import java.util.List; -import java.util.stream.Collectors; - -@RunWith(RobolectricTestRunner.class) -@Config(shadows = {ShadowBluetoothAdapter.class, ShadowBluetoothUtils.class}) -public class BluetoothKeyMissingReceiverTest { - @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); - @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - - private Context mContext; - private ShadowApplication mShadowApplication; - private ShadowBluetoothAdapter mShadowBluetoothAdapter; - @Mock private LocalBluetoothManager mLocalBtManager; - @Mock private NotificationManager mNm; - @Mock private BluetoothDevice mBluetoothDevice; - - @Before - public void setUp() { - mContext = spy(RuntimeEnvironment.getApplication()); - mShadowApplication = Shadow.extract(mContext); - mShadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm); - mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); - mShadowBluetoothAdapter.setEnabled(true); - ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; - } - - @After - public void tearDown() { - ShadowBluetoothUtils.reset(); - } - - @Test - public void broadcastReceiver_isRegistered() { - List registeredReceivers = - mShadowApplication.getRegisteredReceivers(); - - int matchedCount = - registeredReceivers.stream() - .filter( - receiver -> - BluetoothKeyMissingReceiver.class - .getSimpleName() - .equals( - receiver.broadcastReceiver - .getClass() - .getSimpleName())) - .collect(Collectors.toList()) - .size(); - assertThat(matchedCount).isEqualTo(1); - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_BLUETOOTH_KEY_MISSING_DIALOG) - public void broadcastReceiver_receiveKeyMissingIntentFlagOff_doNothing() { - Intent intent = spy(new Intent(BluetoothDevice.ACTION_KEY_MISSING)); - when(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)).thenReturn(mBluetoothDevice); - BluetoothKeyMissingReceiver bluetoothKeyMissingReceiver = getReceiver(intent); - bluetoothKeyMissingReceiver.onReceive(mContext, intent); - - verifyNoInteractions(mNm); - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_BLUETOOTH_KEY_MISSING_DIALOG) - public void broadcastReceiver_background_showNotification() { - Intent intent = spy(new Intent(BluetoothDevice.ACTION_KEY_MISSING)); - when(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)).thenReturn(mBluetoothDevice); - BluetoothKeyMissingReceiver bluetoothKeyMissingReceiver = getReceiver(intent); - bluetoothKeyMissingReceiver.onReceive(mContext, intent); - - verify(mNm).notify(eq(android.R.drawable.stat_sys_data_bluetooth), any(Notification.class)); - verify(mContext, never()).startActivityAsUser(any(), any()); - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_BLUETOOTH_KEY_MISSING_DIALOG) - public void broadcastReceiver_foreground_receiveKeyMissingIntent_showDialog() { - when(mLocalBtManager.isForegroundActivity()).thenReturn(true); - Intent intent = spy(new Intent(BluetoothDevice.ACTION_KEY_MISSING)); - when(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)).thenReturn(mBluetoothDevice); - BluetoothKeyMissingReceiver bluetoothKeyMissingReceiver = getReceiver(intent); - bluetoothKeyMissingReceiver.onReceive(mContext, intent); - - verifyNoInteractions(mNm); - ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); - verify(mContext).startActivityAsUser(captor.capture(), eq(UserHandle.CURRENT)); - assertThat(captor.getValue().getComponent().getClassName()) - .isEqualTo(BluetoothKeyMissingDialog.class.getName()); - } - - private BluetoothKeyMissingReceiver getReceiver(Intent intent) { - assertThat(mShadowApplication.hasReceiverForIntent(intent)).isTrue(); - List receiversForIntent = - mShadowApplication.getReceiversForIntent(intent); - assertThat(receiversForIntent).hasSize(1); - BroadcastReceiver broadcastReceiver = receiversForIntent.get(0); - assertThat(broadcastReceiver).isInstanceOf(BluetoothKeyMissingReceiver.class); - return (BluetoothKeyMissingReceiver) broadcastReceiver; - } -} From 4f8e95fb571cd41e84e199016c3447eeeeeb6e77 Mon Sep 17 00:00:00 2001 From: Diya Bera Date: Thu, 29 Aug 2024 14:48:13 +0000 Subject: [PATCH 12/12] Update identity check string for biometric prompt string Flag: android.hardware.biometrics.Flag.mandatory_biometrics Fixes: 362799712 Test: Manual Change-Id: Ie04a805c56d8867e2e039e4904f6f4dfc78cb0e5 --- res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 7b7dd196e91..56d13a22209 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -937,7 +937,7 @@ Face, fingerprints, and %s added - Identity Check is on + Identity Check is on and requires a biometric Remote Authenticator Unlock