From 5732773031864b6109f946dcc73347c8df96daf6 Mon Sep 17 00:00:00 2001 From: Yiyi Shen Date: Thu, 6 Feb 2025 15:32:05 +0800 Subject: [PATCH 1/8] [Audiosharing] Set temp bond metadata for just bonded lea buds in sharing Test: atest Flag: com.android.settingslib.flags.enable_temporary_bond_devices_ui Bug: 392004799 Change-Id: I99e9955d00362125b7cbf54e2013c99dd2f9b457 --- .../BluetoothDevicePairingDetailBase.java | 119 ++++++---- src/com/android/settings/bluetooth/Utils.java | 31 +++ .../BluetoothDevicePairingDetailBaseTest.java | 211 +++++++++++++++--- .../android/settings/bluetooth/UtilsTest.java | 110 ++++++++- 4 files changed, 396 insertions(+), 75 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java index 33eba1074c4..86f0314716f 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java @@ -46,6 +46,7 @@ import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HearingAidStatsLogUtils; +import com.android.settingslib.flags.Flags; import com.android.settingslib.utils.ThreadUtils; import com.google.common.collect.ImmutableList; @@ -62,6 +63,7 @@ import java.util.concurrent.TimeUnit; public abstract class BluetoothDevicePairingDetailBase extends DeviceListPreferenceFragment { private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(15); private static final int AUTO_DISMISS_MESSAGE_ID = 1001; + private static final int AUTO_FINISH_MESSAGE_ID = 1002; private static final ImmutableList AUDIO_SHARING_PROFILES = ImmutableList.of( BluetoothProfile.LE_AUDIO, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, BluetoothProfile.VOLUME_CONTROL); @@ -77,7 +79,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere @Nullable ProgressDialogFragment mProgressDialog = null; @VisibleForTesting - boolean mShouldTriggerAudioSharingShareThenPairFlow = false; + boolean mShouldTriggerShareThenPairFlow = false; private CopyOnWriteArrayList mDevicesWithMetadataChangedListener = new CopyOnWriteArrayList<>(); @@ -89,7 +91,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere // onDeviceBondStateChanged(BOND_BONDED), BluetoothDevicePreference's summary has already // change from "Pairing..." to empty since it listens to metadata changes happens earlier. // - // In share then pair flow, we have to wait on this page till the device is connected. + // In pairing flow during audio sharing, we have to wait on this page till the device is + // connected to check the device type and handle extra logic for audio sharing. // The BluetoothDevicePreference summary will be blank for seconds between "Pairing..." and // "Connecting..." To help users better understand the process, we listen to metadata change // as well and show a progress dialog with "Connecting to ...." once BluetoothDevice.getState() @@ -100,10 +103,11 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere public void onMetadataChanged(@NonNull BluetoothDevice device, int key, @Nullable byte[] value) { Log.d(getLogTag(), "onMetadataChanged device = " + device + ", key = " + key); - if (mShouldTriggerAudioSharingShareThenPairFlow && mProgressDialog == null + if ((mShouldTriggerShareThenPairFlow || shouldSetTempBondMetadata()) + && mProgressDialog == null && device.getBondState() == BluetoothDevice.BOND_BONDED && mSelectedList.contains(device)) { - triggerAudioSharingShareThenPairFlow(device); + handleDeviceBondedInAudioSharing(device); // Once device is bonded, remove the listener removeOnMetadataChangedListener(device); } @@ -133,7 +137,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere return; } updateBluetooth(); - mShouldTriggerAudioSharingShareThenPairFlow = shouldTriggerAudioSharingShareThenPairFlow(); + mShouldTriggerShareThenPairFlow = shouldTriggerShareThenPairFlow(); } @Override @@ -177,11 +181,12 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere @Override public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { + boolean shouldSetTempBond = shouldSetTempBondMetadata(); if (bondState == BluetoothDevice.BOND_BONDED) { - if (cachedDevice != null && mShouldTriggerAudioSharingShareThenPairFlow) { + if (cachedDevice != null && (mShouldTriggerShareThenPairFlow || shouldSetTempBond)) { BluetoothDevice device = cachedDevice.getDevice(); if (device != null && mSelectedList.contains(device)) { - triggerAudioSharingShareThenPairFlow(device); + handleDeviceBondedInAudioSharing(device); removeOnMetadataChangedListener(device); return; } @@ -190,7 +195,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere finish(); return; } else if (bondState == BluetoothDevice.BOND_BONDING) { - if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) { + if ((mShouldTriggerShareThenPairFlow || shouldSetTempBond) && cachedDevice != null) { BluetoothDevice device = cachedDevice.getDevice(); if (device != null && mSelectedList.contains(device)) { addOnMetadataChangedListener(device); @@ -203,7 +208,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere pageId); HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice); } else if (bondState == BluetoothDevice.BOND_NONE) { - if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) { + if ((mShouldTriggerShareThenPairFlow || shouldSetTempBond) && cachedDevice != null) { BluetoothDevice device = cachedDevice.getDevice(); if (device != null && mSelectedList.contains(device)) { removeOnMetadataChangedListener(device); @@ -233,21 +238,29 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere final BluetoothDevice device = cachedDevice.getDevice(); if (device != null && mSelectedList.contains(device)) { - if (BluetoothUtils.isAudioSharingUIAvailable(getContext())) { - if (mShouldTriggerAudioSharingShareThenPairFlow - && state == BluetoothAdapter.STATE_CONNECTED - && device.equals(mJustBonded) - && AUDIO_SHARING_PROFILES.contains(bluetoothProfile) - && isReadyForAudioSharing(cachedDevice, bluetoothProfile)) { - Log.d(getLogTag(), - "onProfileConnectionStateChanged, ready for audio sharing"); - dismissConnectingDialog(); - mHandler.removeMessages(AUTO_DISMISS_MESSAGE_ID); - finishFragmentWithResultForAudioSharing(device); + var unused = ThreadUtils.postOnBackgroundThread(() -> { + if (BluetoothUtils.isAudioSharingUIAvailable(getContext())) { + if ((mShouldTriggerShareThenPairFlow || shouldSetTempBondMetadata()) + && state == BluetoothAdapter.STATE_CONNECTED + && device.equals(mJustBonded) + && AUDIO_SHARING_PROFILES.contains(bluetoothProfile) + && isReadyForAudioSharing(cachedDevice, bluetoothProfile)) { + Log.d(getLogTag(), "onProfileConnectionStateChanged, lea eligible"); + dismissConnectingDialog(); + BluetoothUtils.setTemporaryBondMetadata(device); + if (mShouldTriggerShareThenPairFlow) { + mHandler.removeMessages(AUTO_DISMISS_MESSAGE_ID); + postOnMainThread(() -> + finishFragmentWithResultForAudioSharing(device)); + } else { + mHandler.removeMessages(AUTO_FINISH_MESSAGE_ID); + postOnMainThread(() -> finish()); + } + } + } else { + postOnMainThread(() -> finish()); } - } else { - finish(); - } + }); } else { onDeviceDeleted(cachedDevice); } @@ -314,7 +327,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere } @VisibleForTesting - boolean shouldTriggerAudioSharingShareThenPairFlow() { + boolean shouldTriggerShareThenPairFlow() { if (BluetoothUtils.isAudioSharingUIAvailable(getContext())) { Activity activity = getActivity(); Intent intent = activity == null ? null : activity.getIntent(); @@ -328,6 +341,16 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere return false; } + private boolean shouldSetTempBondMetadata() { + return Flags.enableTemporaryBondDevicesUi() + && BluetoothUtils.isAudioSharingUIAvailable(getContext()) + && BluetoothUtils.isBroadcasting(mLocalManager) + && mLocalManager != null + && mLocalManager.getCachedDeviceManager() != null + && mLocalManager.getProfileManager().getLeAudioBroadcastAssistantProfile() != null + && !Utils.shouldBlockPairingInAudioSharing(mLocalManager); + } + private boolean isReadyForAudioSharing(@NonNull CachedBluetoothDevice cachedDevice, int justConnectedProfile) { for (int profile : AUDIO_SHARING_PROFILES) { @@ -382,11 +405,10 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere }); } - private void triggerAudioSharingShareThenPairFlow( - @NonNull BluetoothDevice device) { + private void handleDeviceBondedInAudioSharing(@Nullable BluetoothDevice device) { var unused = ThreadUtils.postOnBackgroundThread(() -> { if (mJustBonded != null) { - Log.d(getLogTag(), "Skip triggerAudioSharingShareThenPairFlow, already done"); + Log.d(getLogTag(), "Skip handleDeviceBondedInAudioSharing, already done"); return; } mJustBonded = device; @@ -395,17 +417,38 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere String deviceName = TextUtils.isEmpty(aliasName) ? device.getAddress() : aliasName; showConnectingDialog(deviceName); - // Wait for AUTO_DISMISS_TIME_THRESHOLD_MS and check if the paired device supports audio - // sharing. - if (!mHandler.hasMessages(AUTO_DISMISS_MESSAGE_ID)) { - mHandler.postDelayed(() -> - postOnMainThread( - () -> { - Log.d(getLogTag(), "Show incompatible dialog when timeout"); - dismissConnectingDialog(); - AudioSharingIncompatibleDialogFragment.show(this, deviceName, - () -> finish()); - }), AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); + if (mShouldTriggerShareThenPairFlow) { + // For share then pair flow, we have strong signal that users wish to pair new + // device to join sharing. + // So we wait for AUTO_DISMISS_TIME_THRESHOLD_MS, if we find that the bonded device + // is lea in onProfileConnectionStateChanged, we finish the activity, set the device + // as temp bond and auto add source; otherwise, show dialog to notify that the + // device is incompatible for audio sharing. + if (!mHandler.hasMessages(AUTO_DISMISS_MESSAGE_ID)) { + mHandler.postDelayed(() -> + postOnMainThread( + () -> { + Log.d(getLogTag(), + "Show incompatible dialog when timeout"); + dismissConnectingDialog(); + AudioSharingIncompatibleDialogFragment.show(this, + deviceName, + () -> finish()); + }), AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); + } + } else { + // For other pairing request during audio sharing with sinks < 2, we wait for + // AUTO_DISMISS_TIME_THRESHOLD_MS, if we find that the bonded device is lea in + // onProfileConnectionStateChanged, we finish the activity and set the device as + // temp bond; otherwise, we just finish the activity. + if (!mHandler.hasMessages(AUTO_FINISH_MESSAGE_ID)) { + mHandler.postDelayed(() -> + postOnMainThread( + () -> { + Log.d(getLogTag(), "Finish activity when timeout"); + finish(); + }), AUTO_FINISH_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); + } } }); } diff --git a/src/com/android/settings/bluetooth/Utils.java b/src/com/android/settings/bluetooth/Utils.java index 6404f31a5f2..ea76fafc3a5 100644 --- a/src/com/android/settings/bluetooth/Utils.java +++ b/src/com/android/settings/bluetooth/Utils.java @@ -33,6 +33,7 @@ import android.provider.Settings; import android.util.Log; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; @@ -42,17 +43,21 @@ import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.BluetoothUtils.ErrorListener; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothManager.BluetoothManagerCallback; import com.android.settingslib.utils.ThreadUtils; import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; +import java.util.stream.Collectors; /** * Utils is a helper class that contains constants for various @@ -293,4 +298,30 @@ public final class Utils { ThreadUtils.postOnMainThread(runnable); }); } + + /** + * Check if need to block pairing during audio sharing + * + * @param localBtManager {@link LocalBluetoothManager} + * @return if need to block pairing during audio sharing + */ + public static boolean shouldBlockPairingInAudioSharing( + @NonNull LocalBluetoothManager localBtManager) { + if (!BluetoothUtils.isBroadcasting(localBtManager)) return false; + LocalBluetoothLeBroadcastAssistant assistant = + localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); + CachedBluetoothDeviceManager deviceManager = localBtManager.getCachedDeviceManager(); + List connectedDevices = + assistant == null ? ImmutableList.of() : assistant.getAllConnectedDevices(); + // Block the pairing if there is ongoing audio sharing session and + // a) there is already one temp bond sink connected + // or b) there are already two sinks joining the audio sharing + return assistant != null && deviceManager != null + && (connectedDevices.stream().anyMatch(BluetoothUtils::isTemporaryBondDevice) + || connectedDevices.stream().filter( + d -> BluetoothUtils.hasActiveLocalBroadcastSourceForBtDevice(d, + localBtManager)) + .map(d -> BluetoothUtils.getGroupId(deviceManager.findDevice(d))).collect( + Collectors.toSet()).size() >= 2); + } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java index bd6ac4bf2c0..38a607a76a1 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java @@ -18,6 +18,7 @@ package com.android.settings.bluetooth; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING; +import static com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS; import static com.google.common.truth.Truth.assertThat; @@ -36,6 +37,7 @@ import static org.robolectric.Shadows.shadowOf; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothStatusCodes; import android.content.Context; @@ -44,6 +46,8 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Looper; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.util.Pair; @@ -62,9 +66,15 @@ import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.testutils.shadow.ShadowFragment; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.flags.Flags; +import com.google.common.collect.ImmutableList; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -112,6 +122,14 @@ public class BluetoothDevicePairingDetailBaseTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) private LocalBluetoothManager mLocalManager; @Mock + private CachedBluetoothDeviceManager mDeviceManager; + @Mock + private LocalBluetoothProfileManager mProfileManager; + @Mock + private LocalBluetoothLeBroadcast mBroadcast; + @Mock + private LocalBluetoothLeBroadcastAssistant mAssistant; + @Mock private CachedBluetoothDevice mCachedBluetoothDevice; @Mock private Drawable mDrawable; @@ -197,11 +215,13 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test - public void onDeviceBondStateChanged_bonded_pairAndJoinSharingDisabled_finish() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + @DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI) + public void onDeviceBondStateChanged_bonded_notPairInSharing_finish() { when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); - setUpFragmentWithPairAndJoinSharingIntent(false); + setUpFragmentWithShareThenPairIntent(false); + setUpAudioSharingStates(/* enabled = */ false, /* needSetTempBondMetadata = */ false); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); verify(mFragment).finish(); @@ -209,12 +229,14 @@ public class BluetoothDevicePairingDetailBaseTest { @Test @Config(shadows = ShadowDialogFragment.class) - public void onDeviceBondStateChanged_bonded_pairAndJoinSharingEnabled_handle() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + @DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI) + public void onDeviceBondStateChanged_bonded_shareThenPair_handle() { ShadowDialogFragment.reset(); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); - setUpFragmentWithPairAndJoinSharingIntent(true); + setUpFragmentWithShareThenPairIntent(true); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ false); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); shadowOf(Looper.getMainLooper()).idle(); @@ -231,11 +253,37 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test - public void onDeviceBondStateChanged_bonding_pairAndJoinSharingDisabled_doNothing() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @Config(shadows = ShadowDialogFragment.class) + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void onDeviceBondStateChanged_bonded_pairAfterShare_handle() { + ShadowDialogFragment.reset(); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); - setUpFragmentWithPairAndJoinSharingIntent(false); + setUpFragmentWithShareThenPairIntent(false); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); + shadowOf(Looper.getMainLooper()).idle(); + + ProgressDialogFragment progressDialog = mFragment.mProgressDialog; + assertThat(progressDialog).isNotNull(); + assertThat(progressDialog.getMessage()).isEqualTo( + mContext.getString(R.string.progress_dialog_connect_device_content, + TEST_DEVICE_ADDRESS)); + assertThat( + ShadowDialogFragment.isIsShowing(ProgressDialogFragment.class.getName())).isTrue(); + verify(mFragment, never()).finish(); + + ShadowDialogFragment.reset(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + @DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI) + public void onDeviceBondStateChanged_bonding_notPairInSharing_doNothing() { + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithShareThenPairIntent(false); + setUpAudioSharingStates(/* enabled = */ false, /* needSetTempBondMetadata = */ false); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); verify(mBluetoothAdapter, never()).addOnMetadataChangedListener(any(BluetoothDevice.class), @@ -243,11 +291,13 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test - public void onDeviceBondStateChanged_bonding_pairAndJoinSharingEnabled_addListener() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + @DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI) + public void onDeviceBondStateChanged_bonding_shareThenPair_addListener() { when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); - setUpFragmentWithPairAndJoinSharingIntent(true); + setUpFragmentWithShareThenPairIntent(true); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ false); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); verify(mBluetoothAdapter).addOnMetadataChangedListener(eq(mBluetoothDevice), @@ -256,8 +306,22 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test - public void onDeviceBondStateChanged_unbonded_pairAndJoinSharingDisabled_doNothing() { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void onDeviceBondStateChanged_bonding_pairAfterShare_addListener() { + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithShareThenPairIntent(false); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + + verify(mBluetoothAdapter).addOnMetadataChangedListener(eq(mBluetoothDevice), + any(Executor.class), + any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + + @Test + @DisableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void onDeviceBondStateChanged_unbonded_notPairInSharing_doNothing() { when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_NONE); @@ -267,11 +331,13 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test - public void onDeviceBondStateChanged_unbonded_pairAndJoinSharingEnabled_removeListener() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + @DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI) + public void onDeviceBondStateChanged_unbonded_shareThenPair_removeListener() { when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); - setUpFragmentWithPairAndJoinSharingIntent(true); + setUpFragmentWithShareThenPairIntent(true); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ false); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_NONE); @@ -280,8 +346,23 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test - public void onProfileConnectionStateChanged_deviceInSelectedListAndConnected_finish() { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void onDeviceBondStateChanged_unbonded_pairAfterShare_removeListener() { + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithShareThenPairIntent(false); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_NONE); + + verify(mBluetoothAdapter).removeOnMetadataChangedListener(eq(mBluetoothDevice), + any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + + @Test + @DisableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void + onProfileConnectionStateChanged_deviceInSelectedListAndConnected_notInSharing_finish() { final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(mBluetoothDevice); mFragment.mSelectedList.add(device); @@ -291,19 +372,22 @@ public class BluetoothDevicePairingDetailBaseTest { mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP); + shadowOf(Looper.getMainLooper()).idle(); verify(mFragment).finish(); } @Test @Config(shadows = ShadowDialogFragment.class) + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void - onProfileConnectionStateChanged_deviceInSelectedListAndConnected_pairAndJoinSharing() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + onProfileConnectionStateChanged_inSelectedListAndConnected_shareThenPair_handle() { ShadowDialogFragment.reset(); - when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); - mFragment.mSelectedList.add(mBluetoothDevice); - setUpFragmentWithPairAndJoinSharingIntent(true); + BluetoothDevice device = spy(mBluetoothDevice); + when(mCachedBluetoothDevice.getDevice()).thenReturn(device); + mFragment.mSelectedList.add(device); + setUpFragmentWithShareThenPairIntent(true); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ false); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); shadowOf(Looper.getMainLooper()).idle(); @@ -316,6 +400,7 @@ public class BluetoothDevicePairingDetailBaseTest { BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); shadowOf(Looper.getMainLooper()).idle(); + verify(device).setMetadata(eq(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS), any()); ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); verify(mFragment.getActivity()).setResult(eq(Activity.RESULT_OK), captor.capture()); Intent intent = captor.getValue(); @@ -325,15 +410,44 @@ public class BluetoothDevicePairingDetailBaseTest { BluetoothDevice.class) : null; assertThat(btDevice).isNotNull(); - assertThat(btDevice).isEqualTo(mBluetoothDevice); + assertThat(btDevice).isEqualTo(device); verify(mFragment).finish(); ShadowDialogFragment.reset(); } @Test + @Config(shadows = ShadowDialogFragment.class) + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void + onProfileConnectionStateChanged_inSelectedListAndConnected_pairAfterShare_handle() { + ShadowDialogFragment.reset(); + BluetoothDevice device = spy(mBluetoothDevice); + when(mCachedBluetoothDevice.getDevice()).thenReturn(device); + mFragment.mSelectedList.add(device); + setUpFragmentWithShareThenPairIntent(false); + setUpAudioSharingStates(/* enabled = */ true, /* needSetTempBondMetadata = */ true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); + shadowOf(Looper.getMainLooper()).idle(); + + when(mCachedBluetoothDevice.isConnected()).thenReturn(true); + when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); + when(mCachedBluetoothDevice.isConnectedLeAudioBroadcastAssistantDevice()).thenReturn(true); + when(mCachedBluetoothDevice.isConnectedVolumeControlDevice()).thenReturn(true); + + mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + shadowOf(Looper.getMainLooper()).idle(); + + verify(device).setMetadata(eq(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS), any()); + verify(mFragment).finish(); + + ShadowDialogFragment.reset(); + } + + @Test + @DisableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void onProfileConnectionStateChanged_deviceNotInSelectedList_doNothing() { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(device); @@ -347,8 +461,8 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test + @DisableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void onProfileConnectionStateChanged_deviceDisconnected_doNothing() { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(mBluetoothDevice); mFragment.mSelectedList.add(device); @@ -363,8 +477,8 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test + @DisableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void onProfileConnectionStateChanged_deviceInPreferenceMapAndConnected_removed() { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevicePreference preference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, true, BluetoothDevicePreference.SortType.TYPE_FIFO); @@ -381,8 +495,8 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test + @DisableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void onProfileConnectionStateChanged_deviceNotInPreferenceMap_doNothing() { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); final BluetoothDevicePreference preference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, @@ -404,9 +518,9 @@ public class BluetoothDevicePairingDetailBaseTest { // not crash } - private void setUpFragmentWithPairAndJoinSharingIntent(boolean enablePairAndJoinSharing) { + private void setUpFragmentWithShareThenPairIntent(boolean enableShareThenPair) { Bundle args = new Bundle(); - args.putBoolean(EXTRA_PAIR_AND_JOIN_SHARING, enablePairAndJoinSharing); + args.putBoolean(EXTRA_PAIR_AND_JOIN_SHARING, enableShareThenPair); Intent intent = new Intent(); intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); FragmentActivity activity = spy(Robolectric.setupActivity(FragmentActivity.class)); @@ -420,8 +534,39 @@ public class BluetoothDevicePairingDetailBaseTest { Lifecycle lifecycle = mock(Lifecycle.class); when(lifecycle.getCurrentState()).thenReturn(Lifecycle.State.RESUMED); doReturn(lifecycle).when(mFragment).getLifecycle(); - mFragment.mShouldTriggerAudioSharingShareThenPairFlow = - mFragment.shouldTriggerAudioSharingShareThenPairFlow(); + mFragment.mShouldTriggerShareThenPairFlow = mFragment.shouldTriggerShareThenPairFlow(); + } + + private void setUpAudioSharingStates(boolean enabled, boolean needSetTempBondMetadata) { + when(mLocalManager.getProfileManager()).thenReturn(mProfileManager); + when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); + when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant); + when(mLocalManager.getCachedDeviceManager()).thenReturn(mDeviceManager); + if (!enabled) { + when(mBroadcast.isEnabled(null)).thenReturn(false); + } else { + when(mBroadcast.isEnabled(null)).thenReturn(true); + when(mBroadcast.getLatestBroadcastId()).thenReturn(1); + BluetoothDevice device1 = mock(BluetoothDevice.class); + CachedBluetoothDevice cachedDevice1 = mock(CachedBluetoothDevice.class); + when(mDeviceManager.findDevice(device1)).thenReturn(cachedDevice1); + when(cachedDevice1.getGroupId()).thenReturn(1); + when(cachedDevice1.getDevice()).thenReturn(device1); + BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); + when(state.getBroadcastId()).thenReturn(1); + when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(state)); + if (needSetTempBondMetadata) { + when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device1)); + } else { + BluetoothDevice device2 = mock(BluetoothDevice.class); + CachedBluetoothDevice cachedDevice2 = mock(CachedBluetoothDevice.class); + when(mDeviceManager.findDevice(device2)).thenReturn(cachedDevice2); + when(cachedDevice2.getGroupId()).thenReturn(2); + when(cachedDevice2.getDevice()).thenReturn(device2); + when(mAssistant.getAllConnectedDevices()).thenReturn( + ImmutableList.of(device1, device2)); + } + } } private static class TestBluetoothDevicePairingDetailBase extends diff --git a/tests/robotests/src/com/android/settings/bluetooth/UtilsTest.java b/tests/robotests/src/com/android/settings/bluetooth/UtilsTest.java index 25808b58a0a..4fafcda0c15 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/UtilsTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/UtilsTest.java @@ -15,6 +15,9 @@ */ package com.android.settings.bluetooth; +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -22,34 +25,72 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.google.common.collect.ImmutableList; + +import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothUtils.class}) public class UtilsTest { - + private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; + private static final String TEMP_BOND_METADATA = + "le_audio_sharing"; + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext; + @Mock + private LocalBluetoothManager mLocalBtManager; + @Mock + private LocalBluetoothProfileManager mProfileManager; + @Mock + private LocalBluetoothLeBroadcast mBroadcast; + @Mock + private LocalBluetoothLeBroadcastAssistant mAssistant; + @Mock + private CachedBluetoothDeviceManager mDeviceManager; private MetricsFeatureProvider mMetricsFeatureProvider; @Before public void setUp() { - MockitoAnnotations.initMocks(this); - mMetricsFeatureProvider = FakeFeatureFactory.setupForTest().getMetricsFeatureProvider(); + ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; + mLocalBtManager = Utils.getLocalBtManager(mContext); + when(mLocalBtManager.getProfileManager()).thenReturn(mProfileManager); + when(mLocalBtManager.getCachedDeviceManager()).thenReturn(mDeviceManager); + when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); + when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant); + } + + @After + public void tearDown() { + ShadowBluetoothUtils.reset(); } @Test @@ -60,4 +101,65 @@ public class UtilsTest { verify(mMetricsFeatureProvider).visible(eq(mContext), anyInt(), eq(MetricsEvent.ACTION_SETTINGS_BLUETOOTH_CONNECT_ERROR), anyInt()); } + + @Test + public void shouldBlockPairingInAudioSharing_broadcastOff_returnFalse() { + when(mBroadcast.isEnabled(null)).thenReturn(false); + assertThat(Utils.shouldBlockPairingInAudioSharing(mLocalBtManager)).isFalse(); + } + + @Test + public void shouldBlockPairingInAudioSharing_singlePermanentBondSinkInSharing_returnFalse() { + when(mBroadcast.isEnabled(null)).thenReturn(true); + when(mBroadcast.getLatestBroadcastId()).thenReturn(1); + BluetoothDevice device = mock(BluetoothDevice.class); + CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); + when(mDeviceManager.findDevice(device)).thenReturn(cachedDevice); + when(cachedDevice.getGroupId()).thenReturn(1); + when(cachedDevice.getDevice()).thenReturn(device); + when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device)); + BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); + when(state.getBroadcastId()).thenReturn(1); + when(mAssistant.getAllSources(device)).thenReturn(ImmutableList.of(state)); + assertThat(Utils.shouldBlockPairingInAudioSharing(mLocalBtManager)).isFalse(); + } + + @Test + public void shouldBlockPairingInAudioSharing_singleTempBondSinkInSharing_returnTrue() { + when(mBroadcast.isEnabled(null)).thenReturn(true); + when(mBroadcast.getLatestBroadcastId()).thenReturn(1); + BluetoothDevice device = mock(BluetoothDevice.class); + CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); + when(mDeviceManager.findDevice(device)).thenReturn(cachedDevice); + when(cachedDevice.getGroupId()).thenReturn(1); + when(cachedDevice.getDevice()).thenReturn(device); + when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device)); + BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); + when(state.getBroadcastId()).thenReturn(1); + when(mAssistant.getAllSources(device)).thenReturn(ImmutableList.of(state)); + when(device.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); + assertThat(Utils.shouldBlockPairingInAudioSharing(mLocalBtManager)).isTrue(); + } + + @Test + public void shouldBlockPairingInAudioSharing_twoSinksInSharing_returnTrue() { + when(mBroadcast.isEnabled(null)).thenReturn(true); + when(mBroadcast.getLatestBroadcastId()).thenReturn(1); + BluetoothDevice device1 = mock(BluetoothDevice.class); + BluetoothDevice device2 = mock(BluetoothDevice.class); + CachedBluetoothDevice cachedDevice1 = mock(CachedBluetoothDevice.class); + CachedBluetoothDevice cachedDevice2 = mock(CachedBluetoothDevice.class); + when(mDeviceManager.findDevice(device1)).thenReturn(cachedDevice1); + when(mDeviceManager.findDevice(device2)).thenReturn(cachedDevice2); + when(cachedDevice1.getGroupId()).thenReturn(1); + when(cachedDevice2.getGroupId()).thenReturn(2); + when(cachedDevice1.getDevice()).thenReturn(device1); + when(cachedDevice2.getDevice()).thenReturn(device2); + when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device1, device2)); + BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); + when(state.getBroadcastId()).thenReturn(1); + when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(state)); + assertThat(Utils.shouldBlockPairingInAudioSharing(mLocalBtManager)).isTrue(); + } } From 83a1d4360f201f76fefd7a83808139e60a99e6cd Mon Sep 17 00:00:00 2001 From: Matthew DeVore Date: Tue, 4 Feb 2025 21:00:20 +0000 Subject: [PATCH 2/8] Show topology pane in per-display fragment Some changes made to the UI, i.e. presence or absence of items, based on feedback. There is some ambiguity because we are not fully v2 yet, hence this change. When all external displays are attached after opening the fragment, we now show no preference items, whereas before we showed the pane with built-in display options. If only one external display is attached, we skip the "display list" version of the fragment, and instead show the pane and other v2 items in the per-display fragment. If 2 or more external displays are attached, we do not show the pane after selecting a display, which makes the rotation/resolution items easier to find. I tried to get scrollToPreference to work but could not, and it could be a little disorienting anyway. To help manage the growing number of setOrder calls, keep the order values as well as other pref data in a single spot. This also reduces the amount of boilerplate around constants needed and makes uniform how multiple preferences are built. Flag: com.android.settings.flags.display_topology_pane_in_display_list Bug: b/366056922 Bug: b/394361999 Test: TODO Change-Id: Iaa33f6d9220a1658a372c0432fe159a69dbb88a5 --- .../display/DisplayTopology.kt | 3 - .../ExternalDisplayPreferenceFragment.java | 141 +++++++++++------- .../connecteddevice/display/Mirroring.kt | 3 - ...ExternalDisplayPreferenceFragmentTest.java | 136 ++++++++--------- 4 files changed, 152 insertions(+), 131 deletions(-) diff --git a/src/com/android/settings/connecteddevice/display/DisplayTopology.kt b/src/com/android/settings/connecteddevice/display/DisplayTopology.kt index a3c710c59db..949d5bb418d 100644 --- a/src/com/android/settings/connecteddevice/display/DisplayTopology.kt +++ b/src/com/android/settings/connecteddevice/display/DisplayTopology.kt @@ -159,8 +159,6 @@ class TopologyScale( } } -const val TOPOLOGY_PREFERENCE_KEY = "display_topology_preference" - /** Represents a draggable block in the topology pane. */ class DisplayBlock(context : Context) : Button(context) { @VisibleForTesting var mSelectedImage: Drawable = ColorDrawable(Color.BLACK) @@ -240,7 +238,6 @@ class DisplayTopologyPreference(context : Context) // Prevent highlight when hovering with mouse. isSelectable = false - key = TOPOLOGY_PREFERENCE_KEY isPersistent = false injector = Injector(context) diff --git a/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java b/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java index 2b92e50a2b7..af03bab7579 100644 --- a/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java +++ b/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java @@ -64,33 +64,69 @@ import java.util.List; * The Settings screen for External Displays configuration and connection management. */ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmentBase { + @VisibleForTesting enum PrefBasics { + DISPLAY_TOPOLOGY(10, "display_topology_preference", null), + MIRROR(20, "mirror_preference", null), + + // If shown, use toggle should be before other per-display settings. + EXTERNAL_DISPLAY_USE(30, "external_display_use_preference", + R.string.external_display_use_title), + + ILLUSTRATION(35, "external_display_illustration", null), + + // If shown, external display size is before other per-display settings. + EXTERNAL_DISPLAY_SIZE(40, "external_display_size", R.string.screen_zoom_title), + EXTERNAL_DISPLAY_ROTATION(50, "external_display_rotation", + R.string.external_display_rotation), + EXTERNAL_DISPLAY_RESOLUTION(60, "external_display_resolution", + R.string.external_display_resolution_settings_title), + + // Built-in display link is after per-display settings. + BUILTIN_DISPLAY_LIST(70, "builtin_display_list_preference", + R.string.builtin_display_settings_category), + + DISPLAYS_LIST(80, "displays_list_preference", null), + + // If shown, footer should appear below everything. + FOOTER(90, "footer_preference", null); + + + PrefBasics(int order, String key, @Nullable Integer titleResource) { + this.order = order; + this.key = key; + this.titleResource = titleResource; + } + + // Fields must be public to make the linter happy. + public final int order; + public final String key; + @Nullable public final Integer titleResource; + + void apply(Preference preference) { + if (order != -1) { + preference.setOrder(order); + } + if (titleResource != null) { + preference.setTitle(titleResource); + } + preference.setKey(key); + preference.setPersistent(false); + } + } + static final int EXTERNAL_DISPLAY_SETTINGS_RESOURCE = R.xml.external_display_settings; - static final String DISPLAYS_LIST_PREFERENCE_KEY = "displays_list_preference"; - static final String BUILTIN_DISPLAY_LIST_PREFERENCE_KEY = "builtin_display_list_preference"; - static final String EXTERNAL_DISPLAY_USE_PREFERENCE_KEY = "external_display_use_preference"; - static final String EXTERNAL_DISPLAY_ROTATION_KEY = "external_display_rotation"; - static final String EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY = "external_display_resolution"; - static final String EXTERNAL_DISPLAY_SIZE_PREFERENCE_KEY = "external_display_size"; static final int EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE = R.string.external_display_change_resolution_footer_title; static final int EXTERNAL_DISPLAY_LANDSCAPE_DRAWABLE = R.drawable.external_display_mirror_landscape; static final int EXTERNAL_DISPLAY_TITLE_RESOURCE = R.string.external_display_settings_title; - static final int EXTERNAL_DISPLAY_USE_TITLE_RESOURCE = - R.string.external_display_use_title; static final int EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE = R.string.external_display_not_found_footer_title; static final int EXTERNAL_DISPLAY_PORTRAIT_DRAWABLE = R.drawable.external_display_mirror_portrait; - static final int EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE = - R.string.external_display_rotation; - static final int EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE = - R.string.external_display_resolution_settings_title; - static final int EXTERNAL_DISPLAY_SIZE_TITLE_RESOURCE = R.string.screen_zoom_title; static final int EXTERNAL_DISPLAY_SIZE_SUMMARY_RESOURCE = R.string.screen_zoom_short_summary; - static final int BUILTIN_DISPLAY_SETTINGS_CATEGORY_RESOURCE = - R.string.builtin_display_settings_category; + @VisibleForTesting static final String PREVIOUSLY_SHOWN_LIST_KEY = "mPreviouslyShownListOfDisplays"; private boolean mStarted; @@ -253,9 +289,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen ListPreference getRotationPreference(@NonNull Context context) { if (mRotationPref == null) { mRotationPref = new ListPreference(context); - mRotationPref.setPersistent(false); - mRotationPref.setKey(EXTERNAL_DISPLAY_ROTATION_KEY); - mRotationPref.setTitle(EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE); + PrefBasics.EXTERNAL_DISPLAY_ROTATION.apply(mRotationPref); } return mRotationPref; } @@ -265,9 +299,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen Preference getResolutionPreference(@NonNull Context context) { if (mResolutionPreference == null) { mResolutionPreference = new Preference(context); - mResolutionPreference.setPersistent(false); - mResolutionPreference.setKey(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); - mResolutionPreference.setTitle(EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE); + PrefBasics.EXTERNAL_DISPLAY_RESOLUTION.apply(mResolutionPreference); } return mResolutionPreference; } @@ -277,9 +309,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen MainSwitchPreference getUseDisplayPreference(@NonNull Context context) { if (mUseDisplayPref == null) { mUseDisplayPref = new MainSwitchPreference(context); - mUseDisplayPref.setPersistent(false); - mUseDisplayPref.setKey(EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); - mUseDisplayPref.setTitle(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE); + PrefBasics.EXTERNAL_DISPLAY_USE.apply(mUseDisplayPref); } return mUseDisplayPref; } @@ -289,8 +319,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen IllustrationPreference getIllustrationPreference(@NonNull Context context) { if (mImagePreference == null) { mImagePreference = new IllustrationPreference(context); - mImagePreference.setPersistent(false); - mImagePreference.setKey("external_display_illustration"); + PrefBasics.ILLUSTRATION.apply(mImagePreference); } return mImagePreference; } @@ -308,9 +337,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen private PreferenceCategory getDisplaysListPreference(@NonNull Context context) { if (mDisplaysPreference == null) { mDisplaysPreference = new PreferenceCategory(context); - mDisplaysPreference.setPersistent(false); - mDisplaysPreference.setOrder(40); - mDisplaysPreference.setKey(DISPLAYS_LIST_PREFERENCE_KEY); + PrefBasics.DISPLAYS_LIST.apply(mDisplaysPreference); } return mDisplaysPreference; } @@ -319,10 +346,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen private PreferenceCategory getBuiltinDisplayListPreference(@NonNull Context context) { if (mBuiltinDisplayPreference == null) { mBuiltinDisplayPreference = new PreferenceCategory(context); - mBuiltinDisplayPreference.setPersistent(false); - mBuiltinDisplayPreference.setOrder(30); - mBuiltinDisplayPreference.setKey(BUILTIN_DISPLAY_LIST_PREFERENCE_KEY); - mBuiltinDisplayPreference.setTitle(BUILTIN_DISPLAY_SETTINGS_CATEGORY_RESOURCE); + PrefBasics.BUILTIN_DISPLAY_LIST.apply(mBuiltinDisplayPreference); } return mBuiltinDisplayPreference; } @@ -338,7 +362,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen @NonNull Preference getDisplayTopologyPreference(@NonNull Context context) { if (mDisplayTopologyPreference == null) { mDisplayTopologyPreference = new DisplayTopologyPreference(context); - mDisplayTopologyPreference.setOrder(10); + PrefBasics.DISPLAY_TOPOLOGY.apply(mDisplayTopologyPreference); } return mDisplayTopologyPreference; } @@ -346,7 +370,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen @NonNull Preference getMirrorPreference(@NonNull Context context) { if (mMirrorPreference == null) { mMirrorPreference = new MirrorPreference(context); - mMirrorPreference.setOrder(20); + PrefBasics.MIRROR.apply(mMirrorPreference); } return mMirrorPreference; } @@ -386,16 +410,18 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen private void updateScreenForDisplayId(final int displayId, @NonNull final PrefRefresh screen, @NonNull Context context) { - final boolean forceShowList = displayId == INVALID_DISPLAY - && isTopologyPaneEnabled(mInjector); final var displaysToShow = externalDisplaysToShow(displayId); - if (!forceShowList && displaysToShow.isEmpty() && displayId == INVALID_DISPLAY) { + if (displaysToShow.isEmpty() && displayId == INVALID_DISPLAY) { showTextWhenNoDisplaysToShow(screen, context); - } else if (!forceShowList && displaysToShow.size() == 1 + } else if (displaysToShow.size() == 1 && ((displayId == INVALID_DISPLAY && !mPreviouslyShownListOfDisplays) || displaysToShow.get(0).getDisplayId() == displayId)) { showDisplaySettings(displaysToShow.get(0), screen, context); + if (displayId == INVALID_DISPLAY && isTopologyPaneEnabled(mInjector)) { + // Only show the topology pane if the user did not arrive via the displays list. + maybeAddV2Components(context, screen); + } } else if (displayId == INVALID_DISPLAY) { // If ever shown a list of displays - keep showing it for consistency after // disconnecting one of the displays, and only one display is left. @@ -446,21 +472,30 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen screen.addPreference(updateResolutionPreference(context, display)); screen.addPreference(updateRotationPreference(context, display, displayRotation)); if (isResolutionSettingEnabled(mInjector)) { - screen.addPreference(updateFooterPreference(context, - EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE)); + // Do not show the footer about changing resolution affecting apps. This is not in the + // UX design for v2, and there is no good place to put it, since (a) if it is on the + // bottom of the screen, the external resolution setting must be below the built-in + // display options for the per-display fragment, which is too hidden for the per-display + // fragment, or (b) the footer is above the Built-in display settings, rather than the + // bottom of the screen, which contradicts the visual style and purpose of the + // FooterPreference class, or (c) we must hide the built-in display settings, which is + // inconsistent with the topology pane, which shows that display. + // TODO(b/352648432): probably remove footer once the pane and rest of v2 UI is in + // place. + if (!isTopologyPaneEnabled(mInjector)) { + screen.addPreference(updateFooterPreference(context, + EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE)); + } } if (isDisplaySizeSettingEnabled(mInjector)) { screen.addPreference(updateSizePreference(context)); } } - private void showDisplaysList(@NonNull List displaysToShow, - @NonNull PrefRefresh screen, @NonNull Context context) { + private void maybeAddV2Components(Context context, PrefRefresh screen) { if (isTopologyPaneEnabled(mInjector)) { screen.addPreference(getDisplayTopologyPreference(context)); - if (!displaysToShow.isEmpty()) { - screen.addPreference(getMirrorPreference(context)); - } + screen.addPreference(getMirrorPreference(context)); // If topology is shown, we also show a preference for the built-in display for // consistency with the topology. @@ -468,7 +503,11 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen screen.addPreference(builtinCategory); builtinCategory.addPreference(getBuiltinDisplaySizeAndTextPreference(context)); } + } + private void showDisplaysList(@NonNull List displaysToShow, + @NonNull PrefRefresh screen, @NonNull Context context) { + maybeAddV2Components(context, screen); var displayGroupPref = getDisplaysListPreference(context); if (!displaysToShow.isEmpty()) { screen.addPreference(displayGroupPref); @@ -501,8 +540,9 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen groupCleanable.addPreference(category); var prefItem = new DisplayPreference(context, display); - prefItem.setTitle(context.getString(EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE) - + " | " + context.getString(EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE)); + prefItem.setTitle( + context.getString(PrefBasics.EXTERNAL_DISPLAY_RESOLUTION.titleResource) + " | " + + context.getString(PrefBasics.EXTERNAL_DISPLAY_ROTATION.titleResource)); prefItem.setKey(itemKey); category.addPreference(prefItem); @@ -577,6 +617,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen private Preference updateFooterPreference(@NonNull final Context context, final int title) { var pref = getFooterPreference(context); pref.setTitle(title); + PrefBasics.FOOTER.apply(pref); return pref; } @@ -625,10 +666,8 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen private Preference updateSizePreference(@NonNull final Context context) { var pref = (Preference) getSizePreference(context); - pref.setKey(EXTERNAL_DISPLAY_SIZE_PREFERENCE_KEY); + PrefBasics.EXTERNAL_DISPLAY_SIZE.apply(pref); pref.setSummary(EXTERNAL_DISPLAY_SIZE_SUMMARY_RESOURCE); - pref.setPersistent(false); - pref.setTitle(EXTERNAL_DISPLAY_SIZE_TITLE_RESOURCE); pref.setOnPreferenceClickListener( (Preference p) -> { writePreferenceClickMetric(p); diff --git a/src/com/android/settings/connecteddevice/display/Mirroring.kt b/src/com/android/settings/connecteddevice/display/Mirroring.kt index 37ff375fa7f..453f270ba4e 100644 --- a/src/com/android/settings/connecteddevice/display/Mirroring.kt +++ b/src/com/android/settings/connecteddevice/display/Mirroring.kt @@ -23,8 +23,6 @@ import androidx.preference.SwitchPreferenceCompat import com.android.settings.R -const val MIRROR_PREFERENCE_KEY = "mirror_builtin_display" - /** * A switch preference which is backed by the MIRROR_BUILT_IN_DISPLAY global setting. */ @@ -32,7 +30,6 @@ class MirrorPreference(context: Context): SwitchPreferenceCompat(context) { init { setTitle(R.string.external_display_mirroring_title) - key = MIRROR_PREFERENCE_KEY isPersistent = false } diff --git a/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java index d22dbaeed00..12af7726a42 100644 --- a/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java +++ b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java @@ -17,19 +17,10 @@ package com.android.settings.connecteddevice.display; import static android.view.Display.INVALID_DISPLAY; -import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.DISPLAYS_LIST_PREFERENCE_KEY; import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE; import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE; -import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY; -import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE; -import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_ROTATION_KEY; -import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE; import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_SETTINGS_RESOURCE; -import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_SIZE_PREFERENCE_KEY; import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_SIZE_SUMMARY_RESOURCE; -import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_SIZE_TITLE_RESOURCE; -import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_USE_PREFERENCE_KEY; -import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_USE_TITLE_RESOURCE; import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.PREVIOUSLY_SHOWN_LIST_KEY; import static com.android.settings.flags.Flags.FLAG_DISPLAY_SIZE_CONNECTED_DISPLAY_SETTING; import static com.android.settings.flags.Flags.FLAG_DISPLAY_TOPOLOGY_PANE_IN_DISPLAY_LIST; @@ -61,6 +52,7 @@ import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.DisplayPreference; +import com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.PrefBasics; import com.android.settingslib.widget.FooterPreference; import com.android.settingslib.widget.MainSwitchPreference; @@ -97,23 +89,22 @@ public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBa fragment.onSaveInstanceStateCallback(outState); assertThat(outState.getBoolean(PREVIOUSLY_SHOWN_LIST_KEY)).isFalse(); assertThat(mHandler.getPendingMessages().size()).isEqualTo(1); - PreferenceCategory pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + PreferenceCategory pref = mPreferenceScreen.findPreference(PrefBasics.DISPLAYS_LIST.key); assertThat(pref).isNull(); verify(mMockedInjector, never()).getAllDisplays(); mHandler.flush(); assertThat(mHandler.getPendingMessages().size()).isEqualTo(0); verify(mMockedInjector).getAllDisplays(); - pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + pref = mPreferenceScreen.findPreference(PrefBasics.DISPLAYS_LIST.key); assertThat(pref).isNotNull(); assertThat(pref.getPreferenceCount()).isEqualTo(2); fragment.onSaveInstanceStateCallback(outState); assertThat(outState.getBoolean(PREVIOUSLY_SHOWN_LIST_KEY)).isTrue(); - pref = mPreferenceScreen.findPreference(DisplayTopologyKt.TOPOLOGY_PREFERENCE_KEY); + pref = mPreferenceScreen.findPreference(PrefBasics.DISPLAY_TOPOLOGY.key); assertThat(pref).isNull(); - pref = mPreferenceScreen.findPreference( - ExternalDisplayPreferenceFragment.BUILTIN_DISPLAY_LIST_PREFERENCE_KEY); + pref = mPreferenceScreen.findPreference(PrefBasics.BUILTIN_DISPLAY_LIST.key); assertThat(pref).isNull(); } @@ -126,56 +117,51 @@ public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBa doReturn(new Display[] {mDisplays[1]}).when(mMockedInjector).getAllDisplays(); mHandler.flush(); - var pref = mPreferenceScreen.findPreference(DisplayTopologyKt.TOPOLOGY_PREFERENCE_KEY); + var pref = mPreferenceScreen.findPreference(PrefBasics.DISPLAY_TOPOLOGY.key); assertThat(pref).isNotNull(); - pref = mPreferenceScreen.findPreference(MirroringKt.MIRROR_PREFERENCE_KEY); + pref = mPreferenceScreen.findPreference(PrefBasics.MIRROR.key); assertThat(pref).isNotNull(); - PreferenceCategory listPref = - mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); - assertThat(listPref).isNotNull(); - assertThat(listPref.getPreferenceCount()).isEqualTo(1); - - listPref = mPreferenceScreen.findPreference( - ExternalDisplayPreferenceFragment.BUILTIN_DISPLAY_LIST_PREFERENCE_KEY); - assertThat(listPref).isNotNull(); - assertThat(listPref.getPreferenceCount()).isEqualTo(1); - } - - @Test - @UiThreadTest - public void testShowDisplayListWithPane_NoExternalDisplays() { - mFlags.setFlag(FLAG_DISPLAY_TOPOLOGY_PANE_IN_DISPLAY_LIST, true); - - initFragment(); - doReturn(new Display[0]).when(mMockedInjector).getAllDisplays(); - mHandler.flush(); - - var pref = mPreferenceScreen.findPreference(DisplayTopologyKt.TOPOLOGY_PREFERENCE_KEY); - assertThat(pref).isNotNull(); - pref = mPreferenceScreen.findPreference(MirroringKt.MIRROR_PREFERENCE_KEY); + pref = mPreferenceScreen.findPreference(PrefBasics.DISPLAYS_LIST.key); assertThat(pref).isNull(); PreferenceCategory listPref = - mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); - assertThat(listPref).isNull(); - - listPref = mPreferenceScreen.findPreference( - ExternalDisplayPreferenceFragment.BUILTIN_DISPLAY_LIST_PREFERENCE_KEY); - assertThat(listPref).isNotNull(); - assertThat(listPref.getPreferenceCount()).isEqualTo(1); + mPreferenceScreen.findPreference(PrefBasics.BUILTIN_DISPLAY_LIST.key); var builtinPref = listPref.getPreference(0); assertThat(builtinPref.getOnPreferenceClickListener().onPreferenceClick(builtinPref)) .isTrue(); assertThat(mLaunchedBuiltinSettings).isTrue(); } + @Test + @UiThreadTest + public void testDontShowDisplayListOrPane_NoExternalDisplays() { + mFlags.setFlag(FLAG_DISPLAY_TOPOLOGY_PANE_IN_DISPLAY_LIST, true); + + initFragment(); + doReturn(new Display[0]).when(mMockedInjector).getAllDisplays(); + mHandler.flush(); + + // When no external display is attached, interactive preferences are omitted. + var pref = mPreferenceScreen.findPreference(PrefBasics.DISPLAY_TOPOLOGY.key); + assertThat(pref).isNull(); + pref = mPreferenceScreen.findPreference(PrefBasics.MIRROR.key); + assertThat(pref).isNull(); + + PreferenceCategory listPref = + mPreferenceScreen.findPreference(PrefBasics.DISPLAYS_LIST.key); + assertThat(listPref).isNull(); + + listPref = mPreferenceScreen.findPreference(PrefBasics.BUILTIN_DISPLAY_LIST.key); + assertThat(listPref).isNull(); + } + @Test @UiThreadTest public void testLaunchDisplaySettingFromList() { initFragment(); mHandler.flush(); - PreferenceCategory pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + PreferenceCategory pref = mPreferenceScreen.findPreference(PrefBasics.DISPLAYS_LIST.key); assertThat(pref).isNotNull(); var display1Category = (PreferenceCategory) pref.getPreference(0); var display1Pref = (DisplayPreference) display1Category.getPreference(0); @@ -204,7 +190,7 @@ public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBa // Only one display available doReturn(new Display[] {mDisplays[1]}).when(mMockedInjector).getAllDisplays(); mHandler.flush(); - PreferenceCategory pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + PreferenceCategory pref = mPreferenceScreen.findPreference(PrefBasics.DISPLAYS_LIST.key); assertThat(pref).isNotNull(); assertThat(pref.getPreferenceCount()).isEqualTo(1); } @@ -219,15 +205,15 @@ public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBa // Init initFragment(); mHandler.flush(); - PreferenceCategory list = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + PreferenceCategory list = mPreferenceScreen.findPreference(PrefBasics.DISPLAYS_LIST.key); assertThat(list).isNull(); - var pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + var pref = mPreferenceScreen.findPreference(PrefBasics.EXTERNAL_DISPLAY_RESOLUTION.key); assertThat(pref).isNotNull(); - pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_ROTATION_KEY); + pref = mPreferenceScreen.findPreference(PrefBasics.EXTERNAL_DISPLAY_ROTATION.key); assertThat(pref).isNotNull(); var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER); assertThat(footerPref).isNotNull(); - var sizePref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_SIZE_PREFERENCE_KEY); + var sizePref = mPreferenceScreen.findPreference(PrefBasics.EXTERNAL_DISPLAY_SIZE.key); assertThat(sizePref).isNull(); verify(footerPref).setTitle(EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE); } @@ -241,15 +227,15 @@ public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBa // Init initFragment(); mHandler.flush(); - PreferenceCategory list = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + PreferenceCategory list = mPreferenceScreen.findPreference(PrefBasics.DISPLAYS_LIST.key); assertThat(list).isNull(); - var pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + var pref = mPreferenceScreen.findPreference(PrefBasics.EXTERNAL_DISPLAY_RESOLUTION.key); assertThat(pref).isNotNull(); - pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_ROTATION_KEY); + pref = mPreferenceScreen.findPreference(PrefBasics.EXTERNAL_DISPLAY_ROTATION.key); assertThat(pref).isNotNull(); var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER); assertThat(footerPref).isNotNull(); - var sizePref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_SIZE_PREFERENCE_KEY); + var sizePref = mPreferenceScreen.findPreference(PrefBasics.EXTERNAL_DISPLAY_SIZE.key); assertThat(sizePref).isNotNull(); verify(footerPref).setTitle(EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE); } @@ -263,13 +249,13 @@ public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBa verify(mMockedInjector, never()).getDisplay(anyInt()); mHandler.flush(); verify(mMockedInjector).getDisplay(mDisplayIdArg); - var pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + var pref = mPreferenceScreen.findPreference(PrefBasics.EXTERNAL_DISPLAY_RESOLUTION.key); assertThat(pref).isNotNull(); - pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_ROTATION_KEY); + pref = mPreferenceScreen.findPreference(PrefBasics.EXTERNAL_DISPLAY_ROTATION.key); assertThat(pref).isNotNull(); var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER); assertThat(footerPref).isNotNull(); - var sizePref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_SIZE_PREFERENCE_KEY); + var sizePref = mPreferenceScreen.findPreference(PrefBasics.EXTERNAL_DISPLAY_SIZE.key); assertThat(sizePref).isNotNull(); verify(footerPref).setTitle(EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE); } @@ -283,20 +269,20 @@ public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBa mHandler.flush(); verify(mMockedInjector).getDisplay(mDisplayIdArg); var mainPref = (MainSwitchPreference) mPreferenceScreen.findPreference( - EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); + PrefBasics.EXTERNAL_DISPLAY_USE.key); assertThat(mainPref).isNotNull(); assertThat("" + mainPref.getTitle()).isEqualTo( - getText(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE)); + getText(PrefBasics.EXTERNAL_DISPLAY_USE.titleResource)); assertThat(mainPref.isChecked()).isFalse(); assertThat(mainPref.isEnabled()).isTrue(); assertThat(mainPref.getOnPreferenceChangeListener()).isNotNull(); - var pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + var pref = mPreferenceScreen.findPreference(PrefBasics.EXTERNAL_DISPLAY_RESOLUTION.key); assertThat(pref).isNull(); - pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_ROTATION_KEY); + pref = mPreferenceScreen.findPreference(PrefBasics.EXTERNAL_DISPLAY_ROTATION.key); assertThat(pref).isNull(); var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER); assertThat(footerPref).isNull(); - var sizePref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_SIZE_PREFERENCE_KEY); + var sizePref = mPreferenceScreen.findPreference(PrefBasics.EXTERNAL_DISPLAY_SIZE.key); assertThat(sizePref).isNull(); } @@ -307,10 +293,10 @@ public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBa initFragment(); mHandler.flush(); var mainPref = (MainSwitchPreference) mPreferenceScreen.findPreference( - EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); + PrefBasics.EXTERNAL_DISPLAY_USE.key); assertThat(mainPref).isNotNull(); assertThat("" + mainPref.getTitle()).isEqualTo( - getText(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE)); + getText(PrefBasics.EXTERNAL_DISPLAY_USE.titleResource)); assertThat(mainPref.isChecked()).isFalse(); assertThat(mainPref.isEnabled()).isFalse(); assertThat(mainPref.getOnPreferenceChangeListener()).isNull(); @@ -327,9 +313,9 @@ public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBa var fragment = initFragment(); mHandler.flush(); var pref = fragment.getRotationPreference(mContext); - assertThat(pref.getKey()).isEqualTo(EXTERNAL_DISPLAY_ROTATION_KEY); + assertThat(pref.getKey()).isEqualTo(PrefBasics.EXTERNAL_DISPLAY_ROTATION.key); assertThat("" + pref.getTitle()).isEqualTo( - getText(EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE)); + getText(PrefBasics.EXTERNAL_DISPLAY_ROTATION.titleResource)); assertThat(pref.getEntries().length).isEqualTo(4); assertThat(pref.getEntryValues().length).isEqualTo(4); assertThat(pref.getEntryValues()[0].toString()).isEqualTo("0"); @@ -359,9 +345,9 @@ public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBa var fragment = initFragment(); mHandler.flush(); var pref = fragment.getResolutionPreference(mContext); - assertThat(pref.getKey()).isEqualTo(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + assertThat(pref.getKey()).isEqualTo(PrefBasics.EXTERNAL_DISPLAY_RESOLUTION.key); assertThat("" + pref.getTitle()).isEqualTo( - getText(EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE)); + getText(PrefBasics.EXTERNAL_DISPLAY_RESOLUTION.titleResource)); assertThat("" + pref.getSummary()).isEqualTo("1920 x 1080"); assertThat(pref.isEnabled()).isTrue(); assertThat(pref.getOnPreferenceClickListener()).isNotNull(); @@ -378,8 +364,9 @@ public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBa var fragment = initFragment(); mHandler.flush(); var pref = fragment.getSizePreference(mContext); - assertThat(pref.getKey()).isEqualTo(EXTERNAL_DISPLAY_SIZE_PREFERENCE_KEY); - assertThat("" + pref.getTitle()).isEqualTo(getText(EXTERNAL_DISPLAY_SIZE_TITLE_RESOURCE)); + assertThat(pref.getKey()).isEqualTo(PrefBasics.EXTERNAL_DISPLAY_SIZE.key); + assertThat("" + pref.getTitle()) + .isEqualTo(getText(PrefBasics.EXTERNAL_DISPLAY_SIZE.titleResource)); assertThat("" + pref.getSummary()) .isEqualTo(getText(EXTERNAL_DISPLAY_SIZE_SUMMARY_RESOURCE)); assertThat(pref.isEnabled()).isTrue(); @@ -398,8 +385,9 @@ public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBa var fragment = initFragment(); mHandler.flush(); var pref = fragment.getUseDisplayPreference(mContext); - assertThat(pref.getKey()).isEqualTo(EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); - assertThat("" + pref.getTitle()).isEqualTo(getText(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE)); + assertThat(pref.getKey()).isEqualTo(PrefBasics.EXTERNAL_DISPLAY_USE.key); + assertThat("" + pref.getTitle()) + .isEqualTo(getText(PrefBasics.EXTERNAL_DISPLAY_USE.titleResource)); assertThat(pref.isEnabled()).isTrue(); assertThat(pref.isChecked()).isTrue(); assertThat(pref.getOnPreferenceChangeListener()).isNotNull(); From a8db4d190e2f6b0a10702d3e6e87d51e619fd3fc Mon Sep 17 00:00:00 2001 From: Jack Yu Date: Fri, 7 Feb 2025 15:41:18 -0800 Subject: [PATCH 3/8] Removed the flag enable_modem_cipher_transparency_unsol_events Removed the 24Q3 flag enable_modem_cipher_transparency_unsol_events Bug: 283336425 Test: atest FrameworksTelephonyTests Test: Basic telephony functionality tests Flag: EXEMPT flag cleanup Change-Id: I08e206d721f99f9422a117dbe009561b434a43bf --- .../CellularSecurityPreferenceController.java | 4 +-- ...ecurityNotificationsDividerController.java | 4 --- ...rityNotificationsPreferenceController.java | 21 --------------- ...lularSecurityPreferenceControllerTest.java | 19 -------------- ...ityNotificationsDividerControllerTest.java | 20 -------------- ...NotificationsPreferenceControllerTest.java | 26 ------------------- 6 files changed, 1 insertion(+), 93 deletions(-) diff --git a/src/com/android/settings/network/CellularSecurityPreferenceController.java b/src/com/android/settings/network/CellularSecurityPreferenceController.java index 6ab32e31729..9ad2eac4ac0 100644 --- a/src/com/android/settings/network/CellularSecurityPreferenceController.java +++ b/src/com/android/settings/network/CellularSecurityPreferenceController.java @@ -35,7 +35,6 @@ import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.internal.telephony.flags.Flags; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.SubSettingLauncher; import com.android.settings.network.telephony.CellularSecuritySettingsFragment; @@ -71,8 +70,7 @@ public class CellularSecurityPreferenceController extends BasePreferenceControll @Override public int getAvailabilityStatus() { - if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY) - || !Flags.enableModemCipherTransparencyUnsolEvents()) { + if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { return UNSUPPORTED_ON_DEVICE; } if (mTelephonyManager == null) { diff --git a/src/com/android/settings/network/telephony/CellularSecurityNotificationsDividerController.java b/src/com/android/settings/network/telephony/CellularSecurityNotificationsDividerController.java index 8d498e2619c..4d1ee878239 100644 --- a/src/com/android/settings/network/telephony/CellularSecurityNotificationsDividerController.java +++ b/src/com/android/settings/network/telephony/CellularSecurityNotificationsDividerController.java @@ -26,7 +26,6 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import com.android.internal.telephony.flags.Flags; import com.android.settings.core.BasePreferenceController; import com.android.settings.network.SubscriptionUtil; @@ -60,9 +59,6 @@ public class CellularSecurityNotificationsDividerController extends @Override public int getAvailabilityStatus() { - if (!Flags.enableModemCipherTransparencyUnsolEvents()) { - return UNSUPPORTED_ON_DEVICE; - } if (!isSafetyCenterSupported()) { return UNSUPPORTED_ON_DEVICE; } diff --git a/src/com/android/settings/network/telephony/CellularSecurityNotificationsPreferenceController.java b/src/com/android/settings/network/telephony/CellularSecurityNotificationsPreferenceController.java index 6b18f3c4b18..347f95b50f3 100644 --- a/src/com/android/settings/network/telephony/CellularSecurityNotificationsPreferenceController.java +++ b/src/com/android/settings/network/telephony/CellularSecurityNotificationsPreferenceController.java @@ -26,7 +26,6 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import com.android.internal.telephony.flags.Flags; import com.android.settings.network.SubscriptionUtil; import java.util.List; @@ -75,10 +74,6 @@ public class CellularSecurityNotificationsPreferenceController extends return UNSUPPORTED_ON_DEVICE; } - if (!areFlagsEnabled()) { - return UNSUPPORTED_ON_DEVICE; - } - // Check there are valid SIM cards which can be displayed to the user, otherwise this // setting should not be shown. List availableSubs = SubscriptionUtil.getAvailableSubscriptions(mContext); @@ -106,10 +101,6 @@ public class CellularSecurityNotificationsPreferenceController extends */ @Override public boolean isChecked() { - if (!areFlagsEnabled()) { - return false; - } - try { // Note: the default behavior for this toggle is disabled (as the underlying // TelephonyManager APIs are disabled by default) @@ -144,11 +135,6 @@ public class CellularSecurityNotificationsPreferenceController extends Log.i(LOG_TAG, "Disabling cellular security notifications."); } - // Check flag status - if (!areFlagsEnabled()) { - return false; - } - try { setNotifications(isChecked); } catch (Exception e) { @@ -177,13 +163,6 @@ public class CellularSecurityNotificationsPreferenceController extends && mTelephonyManager.isCellularIdentifierDisclosureNotificationsEnabled(); } - private boolean areFlagsEnabled() { - if (!Flags.enableModemCipherTransparencyUnsolEvents()) { - return false; - } - return true; - } - protected boolean isSafetyCenterSupported() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { return false; diff --git a/tests/unit/src/com/android/settings/network/CellularSecurityPreferenceControllerTest.java b/tests/unit/src/com/android/settings/network/CellularSecurityPreferenceControllerTest.java index 05ca990e789..0cb42ecb09c 100644 --- a/tests/unit/src/com/android/settings/network/CellularSecurityPreferenceControllerTest.java +++ b/tests/unit/src/com/android/settings/network/CellularSecurityPreferenceControllerTest.java @@ -31,7 +31,6 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.Intent; import android.os.Build; -import android.platform.test.flag.junit.SetFlagsRule; import android.safetycenter.SafetyCenterManager; import android.telephony.TelephonyManager; @@ -42,11 +41,8 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.internal.telephony.flags.Flags; - import org.junit.Assume; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -54,8 +50,6 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public final class CellularSecurityPreferenceControllerTest { - @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Mock private TelephonyManager mTelephonyManager; private Preference mPreference; @@ -91,9 +85,6 @@ public final class CellularSecurityPreferenceControllerTest { @Test public void getAvailabilityStatus_hardwareSupported_shouldReturnTrue() { - // Enable telephony API flags for testing - enableFlags(true); - // Hardware support is enabled doReturn(true).when(mTelephonyManager).isNullCipherNotificationsEnabled(); doReturn(true).when(mTelephonyManager) @@ -120,9 +111,6 @@ public final class CellularSecurityPreferenceControllerTest { @Test public void getAvailabilityStatus_noHardwareSupport_shouldReturnFalse() { - // Enable telephony API flags for testing - enableFlags(true); - // Hardware support is disabled doThrow(new UnsupportedOperationException("test")).when(mTelephonyManager) .isNullCipherNotificationsEnabled(); @@ -133,11 +121,4 @@ public final class CellularSecurityPreferenceControllerTest { assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); } - - private void enableFlags(boolean enabled) { - if (enabled) { - } else { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY_UNSOL_EVENTS); - } - } } diff --git a/tests/unit/src/com/android/settings/network/telephony/CellularSecurityNotificationsDividerControllerTest.java b/tests/unit/src/com/android/settings/network/telephony/CellularSecurityNotificationsDividerControllerTest.java index 516c1970d0b..f8b3798207c 100644 --- a/tests/unit/src/com/android/settings/network/telephony/CellularSecurityNotificationsDividerControllerTest.java +++ b/tests/unit/src/com/android/settings/network/telephony/CellularSecurityNotificationsDividerControllerTest.java @@ -28,7 +28,6 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.os.Build; -import android.platform.test.flag.junit.SetFlagsRule; import android.safetycenter.SafetyCenterManager; import android.telephony.TelephonyManager; @@ -39,11 +38,8 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.internal.telephony.flags.Flags; - import org.junit.Assume; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -51,8 +47,6 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class CellularSecurityNotificationsDividerControllerTest { - @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Mock private TelephonyManager mTelephonyManager; private Preference mPreference; @@ -86,9 +80,6 @@ public class CellularSecurityNotificationsDividerControllerTest { @Test public void getAvailabilityStatus_hardwareSupported_shouldReturnTrue() { - // Enable telephony API flags for testing - enableFlags(true); - // Hardware support is enabled doReturn(true).when(mTelephonyManager).isNullCipherNotificationsEnabled(); doReturn(true).when(mTelephonyManager) @@ -99,9 +90,6 @@ public class CellularSecurityNotificationsDividerControllerTest { @Test public void getAvailabilityStatus_noHardwareSupport_shouldReturnFalse() { - // Enable telephony API flags for testing - enableFlags(true); - // Hardware support is disabled doThrow(new UnsupportedOperationException("test")).when(mTelephonyManager) .isNullCipherNotificationsEnabled(); @@ -110,12 +98,4 @@ public class CellularSecurityNotificationsDividerControllerTest { assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); } - - private void enableFlags(boolean enabled) { - if (enabled) { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY_UNSOL_EVENTS); - } else { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY_UNSOL_EVENTS); - } - } } diff --git a/tests/unit/src/com/android/settings/network/telephony/CellularSecurityNotificationsPreferenceControllerTest.java b/tests/unit/src/com/android/settings/network/telephony/CellularSecurityNotificationsPreferenceControllerTest.java index 27cba35e189..8b4ce1adb97 100644 --- a/tests/unit/src/com/android/settings/network/telephony/CellularSecurityNotificationsPreferenceControllerTest.java +++ b/tests/unit/src/com/android/settings/network/telephony/CellularSecurityNotificationsPreferenceControllerTest.java @@ -29,7 +29,6 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.os.Build; -import android.platform.test.flag.junit.SetFlagsRule; import android.safetycenter.SafetyCenterManager; import android.telephony.TelephonyManager; @@ -40,11 +39,8 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.internal.telephony.flags.Flags; - import org.junit.Assume; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -52,8 +48,6 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class CellularSecurityNotificationsPreferenceControllerTest { - @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Mock private TelephonyManager mTelephonyManager; private Preference mPreference; @@ -87,9 +81,6 @@ public class CellularSecurityNotificationsPreferenceControllerTest { @Test public void getAvailabilityStatus_hardwareSupported_shouldReturnTrue() { - // All flags enabled - enableFlags(true); - // Hardware support is enabled doReturn(true).when(mTelephonyManager).isNullCipherNotificationsEnabled(); doReturn(true).when(mTelephonyManager) @@ -100,9 +91,6 @@ public class CellularSecurityNotificationsPreferenceControllerTest { @Test public void getAvailabilityStatus_noHardwareSupport_shouldReturnFalse() { - // All flags enabled - enableFlags(true); - // Hardware support is disabled doThrow(new UnsupportedOperationException("test")).when(mTelephonyManager) .isNullCipherNotificationsEnabled(); @@ -114,8 +102,6 @@ public class CellularSecurityNotificationsPreferenceControllerTest { @Test public void setChecked_shouldReturnTrue() { - enableFlags(true); - // Hardware support is enabled, enabling the feature doNothing().when(mTelephonyManager).setNullCipherNotificationsEnabled(true); doNothing().when(mTelephonyManager) @@ -140,8 +126,6 @@ public class CellularSecurityNotificationsPreferenceControllerTest { @Test public void isChecked_hardwareUnsupported_shouldReturnFalse() { - enableFlags(true); - // Hardware support is disabled doThrow(new UnsupportedOperationException("test")).when(mTelephonyManager) .isNullCipherNotificationsEnabled(); @@ -153,8 +137,6 @@ public class CellularSecurityNotificationsPreferenceControllerTest { @Test public void isChecked_notificationsDisabled_shouldReturnFalse() { - enableFlags(true); - // Hardware support is enabled, but APIs are disabled doReturn(false).when(mTelephonyManager).isNullCipherNotificationsEnabled(); doReturn(false).when(mTelephonyManager) @@ -165,12 +147,4 @@ public class CellularSecurityNotificationsPreferenceControllerTest { doReturn(true).when(mTelephonyManager).isNullCipherNotificationsEnabled(); assertThat(mController.isChecked()).isFalse(); } - - private void enableFlags(boolean enabled) { - if (enabled) { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY_UNSOL_EVENTS); - } else { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY_UNSOL_EVENTS); - } - } } From e9f0fdcd6641957d1b2b57e49c36e90267de1b5d Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Sat, 8 Feb 2025 08:37:35 +0000 Subject: [PATCH 4/8] [Catalyst] Implement metrics/tags for Settings Catalyst NO_IFTTT=Catalyst only Bug: 394002861 Flag: com.android.settings.flags.catalyst Test: devtool Change-Id: Ia879883c3f29cd7ac286b431680d66ab52e87db0 --- .../VibrationMainSwitchPreference.kt | 9 +++++ .../AmbientDisplayAlwaysOnPreference.kt | 8 +++++ .../BatteryPercentageSwitchPreference.kt | 34 ++++++------------- .../display/BrightnessLevelPreference.kt | 9 +++++ .../PeakRefreshRateSwitchPreference.kt | 9 +++++ .../display/darkmode/DarkModeScreen.kt | 9 +++++ .../fuelgauge/BatteryHeaderPreference.kt | 13 ++++++- .../AdaptiveConnectivityTogglePreference.kt | 11 +++++- .../notification/CallVolumePreference.kt | 9 +++++ .../notification/MediaVolumePreference.kt | 9 +++++ .../SeparateRingVolumePreference.kt | 9 +++++ .../tether/WifiHotspotSwitchPreference.kt | 13 +++++-- 12 files changed, 114 insertions(+), 28 deletions(-) diff --git a/src/com/android/settings/accessibility/VibrationMainSwitchPreference.kt b/src/com/android/settings/accessibility/VibrationMainSwitchPreference.kt index a9a05160557..54b1d9588e2 100644 --- a/src/com/android/settings/accessibility/VibrationMainSwitchPreference.kt +++ b/src/com/android/settings/accessibility/VibrationMainSwitchPreference.kt @@ -15,13 +15,16 @@ */ package com.android.settings.accessibility +import android.app.settings.SettingsEnums.ACTION_VIBRATION_HAPTICS import android.content.Context import android.os.VibrationAttributes import android.os.Vibrator import android.provider.Settings import android.widget.CompoundButton import android.widget.CompoundButton.OnCheckedChangeListener +import com.android.settings.PreferenceActionMetricsProvider import com.android.settings.R +import com.android.settings.contract.KEY_VIBRATION_HAPTICS import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyedObservableDelegate import com.android.settingslib.datastore.SettingsStore @@ -39,6 +42,7 @@ class VibrationMainSwitchPreference : key = Settings.System.VIBRATE_ON, title = R.string.accessibility_vibration_primary_switch_title, ), + PreferenceActionMetricsProvider, PreferenceLifecycleProvider, OnCheckedChangeListener { override val keywords: Int @@ -46,6 +50,11 @@ class VibrationMainSwitchPreference : lateinit var vibrator: Vibrator + override val preferenceActionMetrics: Int + get() = ACTION_VIBRATION_HAPTICS + + override fun tags(context: Context) = arrayOf(KEY_VIBRATION_HAPTICS) + override fun storage(context: Context): KeyValueStore = VibrationMainSwitchToggleStorage(SettingsSystemStore.get(context)) diff --git a/src/com/android/settings/display/AmbientDisplayAlwaysOnPreference.kt b/src/com/android/settings/display/AmbientDisplayAlwaysOnPreference.kt index 453593fd5c0..a8227acd84d 100644 --- a/src/com/android/settings/display/AmbientDisplayAlwaysOnPreference.kt +++ b/src/com/android/settings/display/AmbientDisplayAlwaysOnPreference.kt @@ -16,12 +16,14 @@ package com.android.settings.display +import android.app.settings.SettingsEnums.ACTION_AMBIENT_DISPLAY_ALWAYS_ON import android.content.Context import android.hardware.display.AmbientDisplayConfiguration import android.os.SystemProperties import android.os.UserHandle import android.os.UserManager import android.provider.Settings.Secure.DOZE_ALWAYS_ON +import com.android.settings.PreferenceActionMetricsProvider import com.android.settings.PreferenceRestrictionMixin import com.android.settings.R import com.android.settings.contract.KEY_AMBIENT_DISPLAY_ALWAYS_ON @@ -41,6 +43,7 @@ import com.android.settingslib.metadata.SwitchPreference // LINT.IfChange class AmbientDisplayAlwaysOnPreference : SwitchPreference(KEY, R.string.doze_always_on_title, R.string.doze_always_on_summary), + PreferenceActionMetricsProvider, PreferenceAvailabilityProvider, PreferenceSummaryProvider, PreferenceRestrictionMixin { @@ -48,6 +51,11 @@ class AmbientDisplayAlwaysOnPreference : override val keywords: Int get() = R.string.keywords_always_show_time_info + override val preferenceActionMetrics: Int + get() = ACTION_AMBIENT_DISPLAY_ALWAYS_ON + + override fun tags(context: Context) = arrayOf(KEY_AMBIENT_DISPLAY_ALWAYS_ON) + override val restrictionKeys: Array get() = arrayOf(UserManager.DISALLOW_AMBIENT_DISPLAY) diff --git a/src/com/android/settings/display/BatteryPercentageSwitchPreference.kt b/src/com/android/settings/display/BatteryPercentageSwitchPreference.kt index 0cdca343ea8..2c228af57fa 100644 --- a/src/com/android/settings/display/BatteryPercentageSwitchPreference.kt +++ b/src/com/android/settings/display/BatteryPercentageSwitchPreference.kt @@ -15,30 +15,32 @@ */ package com.android.settings.display -import android.app.settings.SettingsEnums +import android.app.settings.SettingsEnums.OPEN_BATTERY_PERCENTAGE import android.content.Context import android.provider.Settings -import androidx.preference.Preference +import com.android.settings.PreferenceActionMetricsProvider import com.android.settings.R import com.android.settings.Utils -import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.settings.contract.KEY_BATTERY_PERCENTAGE import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyedObservableDelegate import com.android.settingslib.datastore.SettingsStore import com.android.settingslib.datastore.SettingsSystemStore import com.android.settingslib.metadata.PreferenceAvailabilityProvider -import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.ReadWritePermit import com.android.settingslib.metadata.SensitivityLevel import com.android.settingslib.metadata.SwitchPreference -import com.android.settingslib.preference.SwitchPreferenceBinding // LINT.IfChange class BatteryPercentageSwitchPreference : SwitchPreference(KEY, R.string.battery_percentage, R.string.battery_percentage_description), - SwitchPreferenceBinding, - PreferenceAvailabilityProvider, - Preference.OnPreferenceChangeListener { + PreferenceActionMetricsProvider, + PreferenceAvailabilityProvider { + + override val preferenceActionMetrics: Int + get() = OPEN_BATTERY_PERCENTAGE + + override fun tags(context: Context) = arrayOf(KEY_BATTERY_PERCENTAGE) override fun storage(context: Context): KeyValueStore = BatteryPercentageStorage(context, SettingsSystemStore.get(context)) @@ -66,22 +68,6 @@ class BatteryPercentageSwitchPreference : override val sensitivityLevel get() = SensitivityLevel.NO_SENSITIVITY - override fun bind(preference: Preference, metadata: PreferenceMetadata) { - super.bind(preference, metadata) - preference.onPreferenceChangeListener = this - } - - override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean { - val showPercentage = newValue as Boolean - - featureFactory.metricsFeatureProvider.action( - preference.context, - SettingsEnums.OPEN_BATTERY_PERCENTAGE, - showPercentage, - ) - return true - } - @Suppress("UNCHECKED_CAST") private class BatteryPercentageStorage( private val context: Context, diff --git a/src/com/android/settings/display/BrightnessLevelPreference.kt b/src/com/android/settings/display/BrightnessLevelPreference.kt index 84f88c8abdd..8f0b791b60f 100644 --- a/src/com/android/settings/display/BrightnessLevelPreference.kt +++ b/src/com/android/settings/display/BrightnessLevelPreference.kt @@ -16,6 +16,7 @@ package com.android.settings.display import android.app.ActivityOptions +import android.app.settings.SettingsEnums.ACTION_BRIGHTNESS_LEVEL import android.content.Context import android.content.Intent import android.content.Intent.ACTION_SHOW_BRIGHTNESS_DIALOG @@ -26,9 +27,11 @@ import android.hardware.display.DisplayManager.DisplayListener import android.os.UserManager import android.provider.Settings.System import androidx.preference.Preference +import com.android.settings.PreferenceActionMetricsProvider import com.android.settings.PreferenceRestrictionMixin import com.android.settings.R import com.android.settings.Utils +import com.android.settings.contract.KEY_BRIGHTNESS_LEVEL import com.android.settings.core.SettingsBaseActivity import com.android.settingslib.RestrictedPreference import com.android.settingslib.datastore.AbstractKeyedDataObservable @@ -56,6 +59,7 @@ class BrightnessLevelPreference : IntRangeValuePreference, PreferenceBinding, PreferenceRestrictionMixin, + PreferenceActionMetricsProvider, PreferenceSummaryProvider, Preference.OnPreferenceClickListener { @@ -68,6 +72,11 @@ class BrightnessLevelPreference : override val keywords: Int get() = R.string.keywords_display_brightness_level + override val preferenceActionMetrics: Int + get() = ACTION_BRIGHTNESS_LEVEL + + override fun tags(context: Context) = arrayOf(KEY_BRIGHTNESS_LEVEL) + override fun getSummary(context: Context): CharSequence? = NumberFormat.getPercentInstance().format(context.brightnessPercent) diff --git a/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt b/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt index 81592cabd31..4d132ddd667 100644 --- a/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt +++ b/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt @@ -15,6 +15,7 @@ */ package com.android.settings.display +import android.app.settings.SettingsEnums.ACTION_SMOOTH_DISPLAY import android.content.Context import android.hardware.display.DisplayManager import android.provider.DeviceConfig @@ -23,7 +24,9 @@ import com.android.internal.display.RefreshRateSettingsUtils.DEFAULT_REFRESH_RAT import com.android.internal.display.RefreshRateSettingsUtils.findHighestRefreshRateAmongAllDisplays import com.android.internal.display.RefreshRateSettingsUtils.findHighestRefreshRateForDefaultDisplay import com.android.server.display.feature.flags.Flags +import com.android.settings.PreferenceActionMetricsProvider import com.android.settings.R +import com.android.settings.contract.KEY_SMOOTH_DISPLAY import com.android.settingslib.datastore.HandlerExecutor import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyedObservableDelegate @@ -41,12 +44,18 @@ import kotlin.math.roundToInt // LINT.IfChange class PeakRefreshRateSwitchPreference : SwitchPreference(KEY, R.string.peak_refresh_rate_title), + PreferenceActionMetricsProvider, PreferenceAvailabilityProvider, PreferenceSummaryProvider, PreferenceLifecycleProvider { private var propertiesChangedListener: DeviceConfig.OnPropertiesChangedListener? = null + override val preferenceActionMetrics: Int + get() = ACTION_SMOOTH_DISPLAY + + override fun tags(context: Context) = arrayOf(KEY_SMOOTH_DISPLAY) + override fun storage(context: Context): KeyValueStore = PeakRefreshRateStore(context, SettingsSystemStore.get(context)) diff --git a/src/com/android/settings/display/darkmode/DarkModeScreen.kt b/src/com/android/settings/display/darkmode/DarkModeScreen.kt index 527cd192b79..86ead6b3fd8 100644 --- a/src/com/android/settings/display/darkmode/DarkModeScreen.kt +++ b/src/com/android/settings/display/darkmode/DarkModeScreen.kt @@ -17,11 +17,14 @@ package com.android.settings.display.darkmode import android.Manifest +import android.app.settings.SettingsEnums.ACTION_DARK_THEME import android.content.Context import android.os.PowerManager import androidx.preference.Preference import androidx.preference.PreferenceScreen +import com.android.settings.PreferenceActionMetricsProvider import com.android.settings.R +import com.android.settings.contract.KEY_DARK_THEME import com.android.settings.flags.Flags import com.android.settingslib.PrimarySwitchPreferenceBinding import com.android.settingslib.datastore.KeyValueStore @@ -42,6 +45,7 @@ class DarkModeScreen(context: Context) : PreferenceScreenCreator, PreferenceScreenBinding, // binding for screen page PrimarySwitchPreferenceBinding, // binding for screen entry point widget + PreferenceActionMetricsProvider, BooleanValuePreference, PreferenceSummaryProvider { @@ -56,6 +60,11 @@ class DarkModeScreen(context: Context) : override val keywords: Int get() = R.string.keywords_dark_ui_mode + override val preferenceActionMetrics: Int + get() = ACTION_DARK_THEME + + override fun tags(context: Context) = arrayOf(KEY_DARK_THEME) + override fun getReadPermissions(context: Context) = Permissions.EMPTY override fun getWritePermissions(context: Context) = diff --git a/src/com/android/settings/fuelgauge/BatteryHeaderPreference.kt b/src/com/android/settings/fuelgauge/BatteryHeaderPreference.kt index 268dea2778a..0eebaa2343a 100644 --- a/src/com/android/settings/fuelgauge/BatteryHeaderPreference.kt +++ b/src/com/android/settings/fuelgauge/BatteryHeaderPreference.kt @@ -16,10 +16,13 @@ package com.android.settings.fuelgauge +import android.app.settings.SettingsEnums.ACTION_BATTERY_LEVEL import android.content.Context import androidx.annotation.VisibleForTesting import androidx.preference.Preference +import com.android.settings.PreferenceActionMetricsProvider import com.android.settings.R +import com.android.settings.contract.KEY_BATTERY_LEVEL import com.android.settings.fuelgauge.BatteryBroadcastReceiver.BatteryUpdateType.BATTERY_NOT_PRESENT import com.android.settingslib.Utils import com.android.settingslib.datastore.KeyValueStore @@ -37,7 +40,10 @@ import com.android.settingslib.widget.UsageProgressBarPreference // LINT.IfChange class BatteryHeaderPreference : - IntRangeValuePreference, PreferenceBinding, PreferenceLifecycleProvider { + IntRangeValuePreference, + PreferenceBinding, + PreferenceActionMetricsProvider, + PreferenceLifecycleProvider { @VisibleForTesting var batteryBroadcastReceiver: BatteryBroadcastReceiver? = null @@ -47,6 +53,11 @@ class BatteryHeaderPreference : override val title: Int get() = R.string.summary_placeholder + override val preferenceActionMetrics: Int + get() = ACTION_BATTERY_LEVEL + + override fun tags(context: Context) = arrayOf(KEY_BATTERY_LEVEL) + override fun createWidget(context: Context) = UsageProgressBarPreference(context) override fun bind(preference: Preference, metadata: PreferenceMetadata) { diff --git a/src/com/android/settings/network/AdaptiveConnectivityTogglePreference.kt b/src/com/android/settings/network/AdaptiveConnectivityTogglePreference.kt index c9ba7141b49..51e0ddd61db 100644 --- a/src/com/android/settings/network/AdaptiveConnectivityTogglePreference.kt +++ b/src/com/android/settings/network/AdaptiveConnectivityTogglePreference.kt @@ -16,10 +16,13 @@ package com.android.settings.network +import android.app.settings.SettingsEnums.ACTION_ADAPTIVE_CONNECTIVITY import android.content.Context import android.net.wifi.WifiManager import android.provider.Settings.Secure.ADAPTIVE_CONNECTIVITY_ENABLED +import com.android.settings.PreferenceActionMetricsProvider import com.android.settings.R +import com.android.settings.contract.KEY_ADAPTIVE_CONNECTIVITY import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyedObservableDelegate import com.android.settingslib.datastore.SettingsSecureStore @@ -31,7 +34,13 @@ import com.android.settingslib.metadata.SensitivityLevel // LINT.IfChange class AdaptiveConnectivityTogglePreference : - MainSwitchPreference(KEY, R.string.adaptive_connectivity_main_switch_title) { + MainSwitchPreference(KEY, R.string.adaptive_connectivity_main_switch_title), + PreferenceActionMetricsProvider { + + override val preferenceActionMetrics: Int + get() = ACTION_ADAPTIVE_CONNECTIVITY + + override fun tags(context: Context) = arrayOf(KEY_ADAPTIVE_CONNECTIVITY) override fun storage(context: Context): KeyValueStore = AdaptiveConnectivityToggleStorage(context, SettingsSecureStore.get(context)) diff --git a/src/com/android/settings/notification/CallVolumePreference.kt b/src/com/android/settings/notification/CallVolumePreference.kt index f3ee3ca7022..a0d7561f7f3 100644 --- a/src/com/android/settings/notification/CallVolumePreference.kt +++ b/src/com/android/settings/notification/CallVolumePreference.kt @@ -19,6 +19,7 @@ package com.android.settings.notification import android.Manifest.permission.MODIFY_AUDIO_SETTINGS import android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED import android.Manifest.permission.MODIFY_PHONE_STATE +import android.app.settings.SettingsEnums.ACTION_CALL_VOLUME import android.content.Context import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE import android.media.AudioManager @@ -26,8 +27,10 @@ import android.media.AudioManager.STREAM_BLUETOOTH_SCO import android.media.AudioManager.STREAM_VOICE_CALL import android.os.UserManager import androidx.preference.Preference +import com.android.settings.PreferenceActionMetricsProvider import com.android.settings.PreferenceRestrictionMixin import com.android.settings.R +import com.android.settings.contract.KEY_CALL_VOLUME import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.NoOpKeyedObservable import com.android.settingslib.datastore.Permissions @@ -44,6 +47,7 @@ import com.android.settingslib.preference.PreferenceBinding open class CallVolumePreference : IntRangeValuePreference, PreferenceBinding, + PreferenceActionMetricsProvider, PreferenceAvailabilityProvider, PreferenceIconProvider, PreferenceRestrictionMixin { @@ -53,6 +57,11 @@ open class CallVolumePreference : override val title: Int get() = R.string.call_volume_option_title + override val preferenceActionMetrics: Int + get() = ACTION_CALL_VOLUME + + override fun tags(context: Context) = arrayOf(KEY_CALL_VOLUME) + override fun getIcon(context: Context) = R.drawable.ic_local_phone_24_lib override fun isAvailable(context: Context) = diff --git a/src/com/android/settings/notification/MediaVolumePreference.kt b/src/com/android/settings/notification/MediaVolumePreference.kt index b2a2fbe7bc7..8af2353dfdc 100644 --- a/src/com/android/settings/notification/MediaVolumePreference.kt +++ b/src/com/android/settings/notification/MediaVolumePreference.kt @@ -18,13 +18,16 @@ package com.android.settings.notification import android.Manifest.permission.MODIFY_AUDIO_SETTINGS import android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED +import android.app.settings.SettingsEnums.ACTION_MEDIA_VOLUME import android.content.Context import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE import android.media.AudioManager.STREAM_MUSIC import android.os.UserManager import androidx.preference.Preference +import com.android.settings.PreferenceActionMetricsProvider import com.android.settings.PreferenceRestrictionMixin import com.android.settings.R +import com.android.settings.contract.KEY_MEDIA_VOLUME import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.NoOpKeyedObservable import com.android.settingslib.datastore.Permissions @@ -41,6 +44,7 @@ import com.android.settingslib.preference.PreferenceBinding open class MediaVolumePreference : IntRangeValuePreference, PreferenceBinding, + PreferenceActionMetricsProvider, PreferenceAvailabilityProvider, PreferenceIconProvider, PreferenceRestrictionMixin { @@ -50,6 +54,11 @@ open class MediaVolumePreference : override val title: Int get() = R.string.media_volume_option_title + override val preferenceActionMetrics: Int + get() = ACTION_MEDIA_VOLUME + + override fun tags(context: Context) = arrayOf(KEY_MEDIA_VOLUME) + override fun getIcon(context: Context) = when { VolumeHelper.isMuted(context, STREAM_MUSIC) -> R.drawable.ic_media_stream_off diff --git a/src/com/android/settings/notification/SeparateRingVolumePreference.kt b/src/com/android/settings/notification/SeparateRingVolumePreference.kt index 16e6e5950c9..fba3eb404a7 100644 --- a/src/com/android/settings/notification/SeparateRingVolumePreference.kt +++ b/src/com/android/settings/notification/SeparateRingVolumePreference.kt @@ -21,6 +21,7 @@ import android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED import android.app.INotificationManager import android.app.NotificationManager import android.app.NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED +import android.app.settings.SettingsEnums.ACTION_RING_VOLUME import android.content.BroadcastReceiver import android.content.Context import android.content.Context.NOTIFICATION_SERVICE @@ -39,8 +40,10 @@ import android.os.Vibrator import android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS import android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS import androidx.preference.Preference +import com.android.settings.PreferenceActionMetricsProvider import com.android.settings.PreferenceRestrictionMixin import com.android.settings.R +import com.android.settings.contract.KEY_RING_VOLUME import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.NoOpKeyedObservable import com.android.settingslib.datastore.Permissions @@ -59,6 +62,7 @@ import com.android.settingslib.preference.PreferenceBinding open class SeparateRingVolumePreference : IntRangeValuePreference, PreferenceBinding, + PreferenceActionMetricsProvider, PreferenceAvailabilityProvider, PreferenceIconProvider, PreferenceLifecycleProvider, @@ -72,6 +76,11 @@ open class SeparateRingVolumePreference : override val title: Int get() = R.string.separate_ring_volume_option_title + override val preferenceActionMetrics: Int + get() = ACTION_RING_VOLUME + + override fun tags(context: Context) = arrayOf(KEY_RING_VOLUME) + override fun getIcon(context: Context) = context.getIconRes() override fun isAvailable(context: Context) = !createAudioHelper(context).isSingleVolume diff --git a/src/com/android/settings/wifi/tether/WifiHotspotSwitchPreference.kt b/src/com/android/settings/wifi/tether/WifiHotspotSwitchPreference.kt index 21b0e88d7ac..fbe431718ec 100644 --- a/src/com/android/settings/wifi/tether/WifiHotspotSwitchPreference.kt +++ b/src/com/android/settings/wifi/tether/WifiHotspotSwitchPreference.kt @@ -17,7 +17,8 @@ package com.android.settings.wifi.tether import android.Manifest -import android.app.settings.SettingsEnums +import android.app.settings.SettingsEnums.ACTION_WIFI_HOTSPOT +import android.app.settings.SettingsEnums.WIFI_TETHER_SETTINGS import android.content.Context import android.content.Intent import android.net.TetheringManager @@ -27,9 +28,11 @@ import android.net.wifi.WifiManager import android.os.UserManager import android.text.BidiFormatter import android.util.Log +import com.android.settings.PreferenceActionMetricsProvider import com.android.settings.PreferenceRestrictionMixin import com.android.settings.R import com.android.settings.Utils +import com.android.settings.contract.KEY_WIFI_HOTSPOT import com.android.settings.core.SubSettingLauncher import com.android.settings.datausage.DataSaverMainSwitchPreference.Companion.KEY as DATA_SAVER_KEY import com.android.settings.wifi.WifiUtils.canShowWifiHotspot @@ -56,10 +59,16 @@ import com.android.settingslib.wifi.WifiUtils.Companion.getWifiTetherSummaryForC class WifiHotspotSwitchPreference(context: Context, dataSaverStore: KeyValueStore) : SwitchPreference(KEY, R.string.wifi_hotspot_checkbox_text), PrimarySwitchPreferenceBinding, + PreferenceActionMetricsProvider, PreferenceAvailabilityProvider, PreferenceSummaryProvider, PreferenceRestrictionMixin { + override val preferenceActionMetrics: Int + get() = ACTION_WIFI_HOTSPOT + + override fun tags(context: Context) = arrayOf(KEY_WIFI_HOTSPOT) + private val wifiHotspotStore = WifiHotspotStore(context, dataSaverStore) override fun isAvailable(context: Context) = @@ -97,7 +106,7 @@ class WifiHotspotSwitchPreference(context: Context, dataSaverStore: KeyValueStor .apply { setDestination(WifiTetherSettings::class.java.name) setTitleRes(R.string.wifi_hotspot_checkbox_text) - setSourceMetricsCategory(SettingsEnums.WIFI_TETHER_SETTINGS) + setSourceMetricsCategory(WIFI_TETHER_SETTINGS) } .toIntent() From 5700104b92dc892ac36c09efbb62b1f38725a953 Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Sat, 8 Feb 2025 08:57:30 +0000 Subject: [PATCH 5/8] [Catalyst] Implement metrics/tags for "Remove Animation" NO_IFTTT=Catalyst only Bug: 394002861 Flag: com.android.settings.flags.catalyst Test: devtool Change-Id: I2891c31de419294324e8c7e17cd191a370cfa305 --- .../settings/accessibility/RemoveAnimationsPreference.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/com/android/settings/accessibility/RemoveAnimationsPreference.kt b/src/com/android/settings/accessibility/RemoveAnimationsPreference.kt index bf2776dc97f..0665e96d5b4 100644 --- a/src/com/android/settings/accessibility/RemoveAnimationsPreference.kt +++ b/src/com/android/settings/accessibility/RemoveAnimationsPreference.kt @@ -17,9 +17,12 @@ package com.android.settings.accessibility import android.annotation.DrawableRes +import android.app.settings.SettingsEnums.ACTION_REMOVE_ANIMATION import android.content.Context import android.provider.Settings +import com.android.settings.PreferenceActionMetricsProvider import com.android.settings.R +import com.android.settings.contract.KEY_REMOVE_ANIMATION import com.android.settingslib.datastore.HandlerExecutor import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyedObserver @@ -37,6 +40,7 @@ class RemoveAnimationsPreference : R.string.accessibility_disable_animations, R.string.accessibility_disable_animations_summary, ), + PreferenceActionMetricsProvider, PreferenceLifecycleProvider { private var mSettingsKeyedObserver: KeyedObserver? = null @@ -44,6 +48,11 @@ class RemoveAnimationsPreference : override val icon: Int @DrawableRes get() = R.drawable.ic_accessibility_animation + override val preferenceActionMetrics: Int + get() = ACTION_REMOVE_ANIMATION + + override fun tags(context: Context) = arrayOf(KEY_REMOVE_ANIMATION) + override fun onStart(context: PreferenceLifecycleContext) { val observer = KeyedObserver { _, _ -> context.notifyPreferenceChange(KEY) } mSettingsKeyedObserver = observer From 6a311b9f5b31636e3fe979a34c05a839ac58df17 Mon Sep 17 00:00:00 2001 From: Tomasz Wasilczyk Date: Tue, 14 Jan 2025 15:05:42 -0800 Subject: [PATCH 6/8] Fix settings crashes on missing Telephony features Bug: 310710841 Test: open settings app Flag: EXEMPT bugfix Change-Id: Ic96585fb34902de54ec838c5692673b33edd2c27 --- .../simstatus/SimStatusDialogController.java | 12 ++++++++++-- .../ims/ImsQueryEnhanced4gLteModeUserSetting.java | 2 ++ .../Enhanced4gBasePreferenceController.java | 8 ++++++-- .../simstatus/SimStatusDialogControllerTest.java | 10 +++++++++- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogController.java b/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogController.java index 89f286c368a..f86f7aeefc3 100644 --- a/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogController.java +++ b/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogController.java @@ -450,8 +450,16 @@ public class SimStatusDialogController implements DefaultLifecycleObserver { String dataNetworkTypeName = null; String voiceNetworkTypeName = null; final int subId = mSubscriptionInfo.getSubscriptionId(); - final int actualDataNetworkType = getTelephonyManager().getDataNetworkType(); - final int actualVoiceNetworkType = getTelephonyManager().getVoiceNetworkType(); + int actualDataNetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN; + int actualVoiceNetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN; + PackageManager pm = mContext.getPackageManager(); + if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS)) { + actualDataNetworkType = getTelephonyManager().getDataNetworkType(); + } + if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) { + actualVoiceNetworkType = getTelephonyManager().getVoiceNetworkType(); + } + final int overrideNetworkType = mTelephonyDisplayInfo == null ? TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE : mTelephonyDisplayInfo.getOverrideNetworkType(); diff --git a/src/com/android/settings/network/ims/ImsQueryEnhanced4gLteModeUserSetting.java b/src/com/android/settings/network/ims/ImsQueryEnhanced4gLteModeUserSetting.java index c6c5ad3e924..a3e0f69c7a6 100644 --- a/src/com/android/settings/network/ims/ImsQueryEnhanced4gLteModeUserSetting.java +++ b/src/com/android/settings/network/ims/ImsQueryEnhanced4gLteModeUserSetting.java @@ -47,6 +47,8 @@ public class ImsQueryEnhanced4gLteModeUserSetting implements ImsQuery { return imsMmTelManager.isAdvancedCallingSettingEnabled(); } catch (IllegalArgumentException exception) { Log.w(LOG_TAG, "fail to get VoLte settings. subId=" + mSubId, exception); + } catch (UnsupportedOperationException ex) { + // expected on devices without IMS } return false; } diff --git a/src/com/android/settings/network/telephony/Enhanced4gBasePreferenceController.java b/src/com/android/settings/network/telephony/Enhanced4gBasePreferenceController.java index d1988c4a3b7..3886c3beeab 100644 --- a/src/com/android/settings/network/telephony/Enhanced4gBasePreferenceController.java +++ b/src/com/android/settings/network/telephony/Enhanced4gBasePreferenceController.java @@ -112,8 +112,12 @@ public class Enhanced4gBasePreferenceController extends TelephonyTogglePreferenc return CONDITIONALLY_UNAVAILABLE; } - if (!queryState.isReadyToVoLte()) { - return CONDITIONALLY_UNAVAILABLE; + try { + if (!queryState.isReadyToVoLte()) { + return CONDITIONALLY_UNAVAILABLE; + } + } catch (UnsupportedOperationException ex) { + return UNSUPPORTED_ON_DEVICE; } return (isUserControlAllowed(carrierConfig) && queryState.isAllowUserControl()) ? AVAILABLE : AVAILABLE_UNSEARCHABLE; diff --git a/tests/unit/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogControllerTest.java b/tests/unit/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogControllerTest.java index 776261a126e..eb03ee581c9 100644 --- a/tests/unit/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogControllerTest.java +++ b/tests/unit/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogControllerTest.java @@ -40,6 +40,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.content.pm.PackageManager; import android.os.PersistableBundle; import android.telephony.CarrierConfigManager; import android.telephony.ServiceState; @@ -67,9 +68,9 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.HashMap; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.Executor; @RunWith(AndroidJUnit4.class) public class SimStatusDialogControllerTest { @@ -95,6 +96,7 @@ public class SimStatusDialogControllerTest { private SimStatusDialogController mController; private Context mContext; + private PackageManager mPackageManager; @Mock private LifecycleOwner mLifecycleOwner; private Lifecycle mLifecycle; @@ -112,6 +114,12 @@ public class SimStatusDialogControllerTest { MockitoAnnotations.initMocks(this); mContext = spy(ApplicationProvider.getApplicationContext()); when(mDialog.getContext()).thenReturn(mContext); + mPackageManager = spy(mContext.getPackageManager()); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS)) + .thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) + .thenReturn(true); mLifecycle = new Lifecycle(mLifecycleOwner); mTelephonyManager = spy(mContext.getSystemService(TelephonyManager.class)); From 063420816141ce07186f3ff4301247a43fee49cc Mon Sep 17 00:00:00 2001 From: Shraddha Basantwani Date: Mon, 2 Dec 2024 09:36:05 +0000 Subject: [PATCH 7/8] Add biometric authentication for package modification Add an extra step of Lock Screen for disabling, force-stopping or uninstalling updates for protected packages Bug: 352504490, 344865740 Test: atest AppButtonsPreferenceControllerTest Flag: EXEMPT High Security Bug (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:32e388ad3199de3c062bb2e2db5d3239f934d0eb) Merged-In: I0c494e307b02229d751de118abcc89e4e61a6861 Change-Id: I0c494e307b02229d751de118abcc89e4e61a6861 --- src/com/android/settings/Utils.java | 16 +++++ .../AppButtonsPreferenceController.java | 64 +++++++++++++------ .../appinfo/AppInfoDashboardFragment.java | 3 +- .../AppButtonsPreferenceControllerTest.java | 3 + .../testutils/shadow/ShadowUtils.java | 11 ++++ 5 files changed, 78 insertions(+), 19 deletions(-) diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index 58788de2f88..5d217cf302d 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -114,6 +114,7 @@ import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settingslib.widget.ActionBarShadowController; import com.android.settingslib.widget.AdaptiveIcon; +import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -1247,4 +1248,19 @@ public final class Utils extends com.android.settingslib.Utils { final UserManager userManager = context.getSystemService(UserManager.class); return userManager != null && userManager.isSystemUser(); } + + /** + * Returns {@code true} if the supplied package is a protected package. Otherwise, returns + * {@code false}. + * + * @param context the context + * @param packageName the package name + */ + public static boolean isProtectedPackage( + @NonNull Context context, @NonNull String packageName) { + final List protectedPackageNames = Arrays.asList(context.getResources() + .getStringArray(com.android.internal.R.array + .config_biometric_protected_package_names)); + return protectedPackageNames != null && protectedPackageNames.contains(packageName); + } } diff --git a/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java b/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java index 1b270d63b4d..69f23fe5648 100644 --- a/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java @@ -52,6 +52,7 @@ import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.Utils; import com.android.settings.applications.ApplicationFeatureProvider; +import com.android.settings.applications.appinfo.AppInfoDashboardFragment; import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.InstrumentedPreferenceFragment; @@ -249,13 +250,21 @@ public class AppButtonsPreferenceController extends BasePreferenceController imp } else { showDialogInner(ButtonActionDialogFragment.DialogType.DISABLE); } + } else if (mAppEntry.info.enabled) { + requireAuthAndExecute(() -> { + mMetricsFeatureProvider.action( + mActivity, + SettingsEnums.ACTION_SETTINGS_DISABLE_APP, + getPackageNameForMetric()); + AsyncTask.execute(new DisableChangerRunnable(mPm, + mAppEntry.info.packageName, + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)); + }); } else { mMetricsFeatureProvider.action( mActivity, - mAppEntry.info.enabled - ? SettingsEnums.ACTION_SETTINGS_DISABLE_APP - : SettingsEnums.ACTION_SETTINGS_ENABLE_APP, - getPackageNameForMetric()); + SettingsEnums.ACTION_SETTINGS_ENABLE_APP, + getPackageNameForMetric()); AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)); } @@ -303,13 +312,28 @@ public class AppButtonsPreferenceController extends BasePreferenceController imp } } + /** + * Runs the given action with restricted lock authentication if it is a protected package. + * + * @param action The action to run. + */ + private void requireAuthAndExecute(Runnable action) { + if (Utils.isProtectedPackage(mContext, mAppEntry.info.packageName)) { + AppInfoDashboardFragment.showLockScreen(mContext, () -> action.run()); + } else { + action.run(); + } + } + public void handleDialogClick(int id) { switch (id) { case ButtonActionDialogFragment.DialogType.DISABLE: - mMetricsFeatureProvider.action(mActivity, - SettingsEnums.ACTION_SETTINGS_DISABLE_APP); - AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER)); + requireAuthAndExecute(() -> { + mMetricsFeatureProvider.action(mActivity, + SettingsEnums.ACTION_SETTINGS_DISABLE_APP); + AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER)); + }); break; case ButtonActionDialogFragment.DialogType.SPECIAL_DISABLE: mMetricsFeatureProvider.action(mActivity, @@ -317,7 +341,9 @@ public class AppButtonsPreferenceController extends BasePreferenceController imp uninstallPkg(mAppEntry.info.packageName, false, true); break; case ButtonActionDialogFragment.DialogType.FORCE_STOP: - forceStopPackage(mAppEntry.info.packageName); + requireAuthAndExecute(() -> { + forceStopPackage(mAppEntry.info.packageName); + }); break; } } @@ -547,16 +573,18 @@ public class AppButtonsPreferenceController extends BasePreferenceController imp @VisibleForTesting void uninstallPkg(String packageName, boolean allUsers, boolean andDisable) { - stopListeningToPackageRemove(); - // Create new intent to launch Uninstaller activity - Uri packageUri = Uri.parse("package:" + packageName); - Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri); - uninstallIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, allUsers); + requireAuthAndExecute(() -> { + stopListeningToPackageRemove(); + // Create new intent to launch Uninstaller activity + Uri packageUri = Uri.parse("package:" + packageName); + Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri); + uninstallIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, allUsers); - mMetricsFeatureProvider.action( - mActivity, SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP); - mFragment.startActivityForResult(uninstallIntent, mRequestUninstall); - mDisableAfterUninstall = andDisable; + mMetricsFeatureProvider.action( + mActivity, SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP); + mFragment.startActivityForResult(uninstallIntent, mRequestUninstall); + mDisableAfterUninstall = andDisable; + }); } @VisibleForTesting diff --git a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java index 54455d4f110..8f79b910055 100755 --- a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java +++ b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java @@ -430,7 +430,8 @@ public class AppInfoDashboardFragment extends DashboardFragment } } - private static void showLockScreen(Context context, Runnable successRunnable) { + /** Shows the lock screen if the keyguard is secured. */ + public static void showLockScreen(Context context, Runnable successRunnable) { final KeyguardManager keyguardManager = context.getSystemService( KeyguardManager.class); diff --git a/tests/robotests/src/com/android/settings/applications/appinfo/AppButtonsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/appinfo/AppButtonsPreferenceControllerTest.java index 9a65dc8829c..39201f317dc 100644 --- a/tests/robotests/src/com/android/settings/applications/appinfo/AppButtonsPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/applications/appinfo/AppButtonsPreferenceControllerTest.java @@ -56,6 +56,7 @@ import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.shadow.ShadowUtils; import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.instantapps.InstantAppDataProvider; @@ -81,6 +82,7 @@ import org.robolectric.util.ReflectionHelpers; import java.util.Set; +@Config(shadows = {ShadowUtils.class}) @RunWith(RobolectricTestRunner.class) public class AppButtonsPreferenceControllerTest { @@ -166,6 +168,7 @@ public class AppButtonsPreferenceControllerTest { @After public void tearDown() { ShadowAppUtils.reset(); + ShadowUtils.reset(); } @Test diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUtils.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUtils.java index 5f8c434fc9f..1dd381f3570 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUtils.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUtils.java @@ -50,6 +50,7 @@ public class ShadowUtils { private static ArraySet sResultLinks = new ArraySet<>(); private static boolean sIsBatteryPresent; private static boolean sIsMultipleBiometricsSupported; + private static boolean sIsProtectedPackage; @Implementation protected static int enforceSameOwner(Context context, int userId) { @@ -82,6 +83,7 @@ public class ShadowUtils { sResultLinks = new ArraySet<>(); sIsBatteryPresent = true; sIsMultipleBiometricsSupported = false; + sIsProtectedPackage = false; } public static void setIsDemoUser(boolean isDemoUser) { @@ -188,4 +190,13 @@ public class ShadowUtils { public static void setIsMultipleBiometricsSupported(boolean isMultipleBiometricsSupported) { sIsMultipleBiometricsSupported = isMultipleBiometricsSupported; } + + @Implementation + protected static boolean isProtectedPackage(Context context, String packageName) { + return sIsProtectedPackage; + } + + public static void setIsProtectedPackage(boolean isProtectedPackage) { + sIsProtectedPackage = isProtectedPackage; + } } From 2782cc9d213d475b5cacbbd3c276944c854aea19 Mon Sep 17 00:00:00 2001 From: mxyyiyi Date: Mon, 10 Feb 2025 13:56:15 +0800 Subject: [PATCH 8/8] [A11y] Update symbol from '>' to '->' to improve talkback announcement. Fix: 319172554 Test: talkback Flag: EXEMPT for res update Change-Id: I35141587492f437170fe2a99b93992205d79976e --- 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 39e0dbdf4e8..c49d6e3f767 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -10399,7 +10399,7 @@ Allowing %1$s to always run in the background may reduce battery life. - \n\nYou can change this later from Settings > Apps. + \n\nYou can change this later from Settings -> Apps. %1$s use since last full charge