From c860f87861d563e25b74a8aa10be580b63a89e3e Mon Sep 17 00:00:00 2001 From: shaoweishen Date: Wed, 26 Oct 2022 07:28:36 +0000 Subject: [PATCH 1/5] [Sound Panel] Set inset for button background add inset for button of Sound panel, to prevent truncated when set Display/Font size to biggest. attached screenshots in bug. Test: Verified on device Bug: 252952472 Change-Id: I407d4d6cdac47cfcc63470aa8b5d4e5817e1705b --- ...olume_dialog_button_background_outline.xml | 31 ++++++++++--------- .../volume_dialog_button_background_solid.xml | 29 +++++++++-------- res/layout/panel_layout.xml | 4 +-- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/res/drawable/volume_dialog_button_background_outline.xml b/res/drawable/volume_dialog_button_background_outline.xml index 78f3fcf3c5b..efbaec63bd5 100644 --- a/res/drawable/volume_dialog_button_background_outline.xml +++ b/res/drawable/volume_dialog_button_background_outline.xml @@ -14,18 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - - - - \ No newline at end of file + android:insetTop="6dp" + android:insetBottom="6dp"> + + + + + + + \ No newline at end of file diff --git a/res/drawable/volume_dialog_button_background_solid.xml b/res/drawable/volume_dialog_button_background_solid.xml index 1fa8f20cf31..697131db247 100644 --- a/res/drawable/volume_dialog_button_background_solid.xml +++ b/res/drawable/volume_dialog_button_background_solid.xml @@ -15,17 +15,20 @@ limitations under the License. --> - - - - - - \ No newline at end of file + android:insetTop="6dp" + android:insetBottom="6dp"> + + + + + + + \ No newline at end of file diff --git a/res/layout/panel_layout.xml b/res/layout/panel_layout.xml index 2b9daebf85d..f154abc40cb 100644 --- a/res/layout/panel_layout.xml +++ b/res/layout/panel_layout.xml @@ -117,7 +117,7 @@ android:id="@+id/see_more" style="@style/PanelOptionRoundedOutlinedButton" android:layout_width="wrap_content" - android:layout_height="36dp" + android:layout_height="wrap_content" android:minWidth="0dp" android:text="@string/settings_button"/> @@ -130,7 +130,7 @@ android:id="@+id/done" style="@style/PanelOptionRoundedSolidButton" android:layout_width="wrap_content" - android:layout_height="36dp" + android:layout_height="wrap_content" android:minWidth="0dp" android:text="@string/done"/> From 2ac6fc57093487e87085298471d173947bf39015 Mon Sep 17 00:00:00 2001 From: Behnam Heydarshahi Date: Wed, 12 Oct 2022 15:08:22 +0000 Subject: [PATCH 2/5] Read flag to show/hide notification slider Replace the build-time boolean resource with a run time device config flag that signals whether the ring notification slider should be displayed or not Bug: b/38477228 Test: make DEBUG_ROBOLECTRIC=1 ROBOTEST_FILTER=NotificationVolumePreferenceControllerTest RunSettingsRoboTests -j40 make DEBUG_ROBOLECTRIC=1 ROBOTEST_FILTER=RingVolumePreferenceControllerTest RunSettingsRoboTests -j40 Change-Id: I8b9a2cbd5af7fa1bba56ff9ba62771d677d4a932 --- ...otificationVolumePreferenceController.java | 69 ++++++++--- .../RingVolumePreferenceController.java | 107 ++++++++++++------ .../VolumeSeekBarPreferenceController.java | 12 +- ...icationVolumePreferenceControllerTest.java | 92 ++++++++++++++- .../RingVolumePreferenceControllerTest.java | 29 +++-- 5 files changed, 240 insertions(+), 69 deletions(-) diff --git a/src/com/android/settings/notification/NotificationVolumePreferenceController.java b/src/com/android/settings/notification/NotificationVolumePreferenceController.java index 322bb6c229b..112debca647 100644 --- a/src/com/android/settings/notification/NotificationVolumePreferenceController.java +++ b/src/com/android/settings/notification/NotificationVolumePreferenceController.java @@ -16,6 +16,7 @@ package com.android.settings.notification; +import android.app.ActivityThread; import android.app.INotificationManager; import android.app.NotificationManager; import android.content.BroadcastReceiver; @@ -29,26 +30,32 @@ import android.os.Looper; import android.os.Message; import android.os.ServiceManager; import android.os.Vibrator; +import android.provider.DeviceConfig; import android.service.notification.NotificationListenerService; import android.text.TextUtils; import android.util.Log; import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.PreferenceScreen; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.settings.R; import com.android.settings.Utils; import com.android.settingslib.core.lifecycle.Lifecycle; import java.util.Objects; +import java.util.Set; /** - * Update notification volume icon in Settings in response to user adjusting volume + * Update notification volume icon in Settings in response to user adjusting volume. */ public class NotificationVolumePreferenceController extends VolumeSeekBarPreferenceController { private static final String TAG = "NotificationVolumePreferenceController"; private static final String KEY_NOTIFICATION_VOLUME = "notification_volume"; + private static final boolean CONFIG_DEFAULT_VAL = false; + private boolean mSeparateNotification; private Vibrator mVibrator; private int mRingerMode = AudioManager.RINGER_MODE_NORMAL; @@ -56,39 +63,74 @@ public class NotificationVolumePreferenceController extends VolumeSeekBarPrefere private final RingReceiver mReceiver = new RingReceiver(); private final H mHandler = new H(); private INotificationManager mNoMan; - - private int mMuteIcon; private final int mNormalIconId = R.drawable.ic_notifications; private final int mVibrateIconId = R.drawable.ic_volume_ringer_vibrate; private final int mSilentIconId = R.drawable.ic_notifications_off_24dp; - private final boolean mRingNotificationAliased; - - public NotificationVolumePreferenceController(Context context) { this(context, KEY_NOTIFICATION_VOLUME); } public NotificationVolumePreferenceController(Context context, String key) { super(context, key); + mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); if (mVibrator != null && !mVibrator.hasVibrator()) { mVibrator = null; } - mRingNotificationAliased = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_alias_ring_notif_stream_types); updateRingerMode(); } + /** + * Allow for notification slider to be enabled in the scenario where the config switches on + * while settings page is already on the screen by always configuring the preference, even if it + * is currently inactive. + */ + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + if (mPreference == null) { + setupVolPreference(screen); + } + mSeparateNotification = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, CONFIG_DEFAULT_VAL); + if (mPreference != null) { + mPreference.setVisible(getAvailabilityStatus() == AVAILABLE); + } + updateEffectsSuppressor(); + updatePreferenceIconAndSliderState(); + } + + /** + * Only display the notification slider when the corresponding device config flag is set + */ + private void onDeviceConfigChange(DeviceConfig.Properties properties) { + Set changeSet = properties.getKeyset(); + + if (changeSet.contains(SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION)) { + boolean newVal = properties.getBoolean( + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, CONFIG_DEFAULT_VAL); + if (newVal != mSeparateNotification) { + mSeparateNotification = newVal; + // manually hiding the preference because being unavailable does not do the job + if (mPreference != null) { + mPreference.setVisible(getAvailabilityStatus() == AVAILABLE); + } + } + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @Override public void onResume() { super.onResume(); mReceiver.register(true); - updateEffectsSuppressor(); - updatePreferenceIconAndSliderState(); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + ActivityThread.currentApplication().getMainExecutor(), + this::onDeviceConfigChange); } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) @@ -96,16 +138,17 @@ public class NotificationVolumePreferenceController extends VolumeSeekBarPrefere public void onPause() { super.onPause(); mReceiver.register(false); + DeviceConfig.removeOnPropertiesChangedListener(this::onDeviceConfigChange); } @Override public int getAvailabilityStatus() { + boolean separateNotification = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false); - // Show separate notification slider if ring/notification are not aliased by AudioManager -- - // if they are, notification volume is controlled by RingVolumePreferenceController. return mContext.getResources().getBoolean(R.bool.config_show_notification_volume) - && (!mRingNotificationAliased || !Utils.isVoiceCapable(mContext)) && !mHelper.isSingleVolume() + && (separateNotification || !Utils.isVoiceCapable(mContext)) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } diff --git a/src/com/android/settings/notification/RingVolumePreferenceController.java b/src/com/android/settings/notification/RingVolumePreferenceController.java index a78689f5a0a..7fdb1e16141 100644 --- a/src/com/android/settings/notification/RingVolumePreferenceController.java +++ b/src/com/android/settings/notification/RingVolumePreferenceController.java @@ -16,6 +16,7 @@ package com.android.settings.notification; +import android.app.ActivityThread; import android.app.INotificationManager; import android.app.NotificationManager; import android.content.BroadcastReceiver; @@ -29,6 +30,7 @@ import android.os.Looper; import android.os.Message; import android.os.ServiceManager; import android.os.Vibrator; +import android.provider.DeviceConfig; import android.service.notification.NotificationListenerService; import android.text.TextUtils; import android.util.Log; @@ -36,11 +38,13 @@ import android.util.Log; import androidx.lifecycle.OnLifecycleEvent; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.settings.R; import com.android.settings.Utils; import com.android.settingslib.core.lifecycle.Lifecycle; import java.util.Objects; +import java.util.Set; /** * This slider can represent both ring and notification, if the corresponding streams are aliased, @@ -59,24 +63,21 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr private int mMuteIcon; - /* - * Whether ring and notification streams are aliased together by AudioManager. - * If they are, we'll present one volume control for both. - * If not, we'll present separate volume controls. - */ - private final boolean mRingAliasNotif; - - private final int mNormalIconId; + private int mNormalIconId; @VisibleForTesting - final int mVibrateIconId; + int mVibrateIconId; @VisibleForTesting - final int mSilentIconId; + int mSilentIconId; @VisibleForTesting - final int mTitleId; + int mTitleId; + + private boolean mSeparateNotification; private INotificationManager mNoMan; + private static final boolean CONFIG_DEFAULT_VAL = false; + public RingVolumePreferenceController(Context context) { this(context, KEY_RING_VOLUME); } @@ -87,29 +88,56 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr if (mVibrator != null && !mVibrator.hasVibrator()) { mVibrator = null; } - - mRingAliasNotif = isRingAliasNotification(); - if (mRingAliasNotif) { - mTitleId = R.string.ring_volume_option_title; - - mNormalIconId = R.drawable.ic_notifications; - mSilentIconId = R.drawable.ic_notifications_off_24dp; - } else { - mTitleId = R.string.separate_ring_volume_option_title; - - mNormalIconId = R.drawable.ic_ring_volume; - mSilentIconId = R.drawable.ic_ring_volume_off; - } - // todo: set a distinct vibrate icon for ring vs notification - mVibrateIconId = R.drawable.ic_volume_ringer_vibrate; - + mSeparateNotification = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, CONFIG_DEFAULT_VAL); + loadPreferenceIconResources(mSeparateNotification); updateRingerMode(); } - @VisibleForTesting - boolean isRingAliasNotification() { - return mContext.getResources().getBoolean( - com.android.internal.R.bool.config_alias_ring_notif_stream_types); + private void loadPreferenceIconResources(boolean separateNotification) { + if (separateNotification) { + mTitleId = R.string.separate_ring_volume_option_title; + mNormalIconId = R.drawable.ic_ring_volume; + mSilentIconId = R.drawable.ic_ring_volume_off; + } else { + mTitleId = R.string.ring_volume_option_title; + mNormalIconId = R.drawable.ic_notifications; + mSilentIconId = R.drawable.ic_notifications_off_24dp; + } + // todo: set a distinct vibrate icon for ring vs notification + mVibrateIconId = R.drawable.ic_volume_ringer_vibrate; + } + + /** + * As the responsibility of this slider changes, so should its title & icon + */ + public void onDeviceConfigChange(DeviceConfig.Properties properties) { + Set changeSet = properties.getKeyset(); + if (changeSet.contains(SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION)) { + boolean valueUpdated = readSeparateNotificationVolumeConfig(); + if (valueUpdated) { + updateEffectsSuppressor(); + selectPreferenceIconState(); + setPreferenceTitle(); + } + } + } + + /** + * side effect: updates the cached value of the config, and also the icon + * @return has the config changed? + */ + private boolean readSeparateNotificationVolumeConfig() { + boolean newVal = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, CONFIG_DEFAULT_VAL); + + boolean valueUpdated = newVal != mSeparateNotification; + if (valueUpdated) { + mSeparateNotification = newVal; + loadPreferenceIconResources(newVal); + } + + return valueUpdated; } @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @@ -117,8 +145,11 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr public void onResume() { super.onResume(); mReceiver.register(true); + readSeparateNotificationVolumeConfig(); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + ActivityThread.currentApplication().getMainExecutor(), this::onDeviceConfigChange); updateEffectsSuppressor(); - updatePreferenceIcon(); + selectPreferenceIconState(); setPreferenceTitle(); } @@ -127,6 +158,7 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr public void onPause() { super.onPause(); mReceiver.register(false); + DeviceConfig.removeOnPropertiesChangedListener(this::onDeviceConfigChange); } @Override @@ -170,7 +202,7 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr final int ringerMode = mHelper.getRingerModeInternal(); if (mRingerMode == ringerMode) return; mRingerMode = ringerMode; - updatePreferenceIcon(); + selectPreferenceIconState(); } private void updateEffectsSuppressor() { @@ -190,7 +222,8 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr return; } - if (hintsMatch(hints, mRingAliasNotif)) { + if (hintsMatch(hints, DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false))) { mSuppressor = suppressor; if (mPreference != null) { final String text = SuppressorHelper.getSuppressionText(mContext, suppressor); @@ -200,11 +233,11 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr } @VisibleForTesting - boolean hintsMatch(int hints, boolean ringNotificationAliased) { + boolean hintsMatch(int hints, boolean notificationSeparated) { return (hints & NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS) != 0 || (hints & NotificationListenerService.HINT_HOST_DISABLE_EFFECTS) != 0 || ((hints & NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS) - != 0 && ringNotificationAliased); + != 0 && !notificationSeparated); } @VisibleForTesting @@ -217,7 +250,7 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr mVibrator = vibrator; } - private void updatePreferenceIcon() { + private void selectPreferenceIconState() { if (mPreference != null) { if (mRingerMode == AudioManager.RINGER_MODE_NORMAL) { mPreference.showIcon(mNormalIconId); diff --git a/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java b/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java index d1701599c34..0414565721e 100644 --- a/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java +++ b/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java @@ -55,13 +55,17 @@ public abstract class VolumeSeekBarPreferenceController extends public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); if (isAvailable()) { - mPreference = screen.findPreference(getPreferenceKey()); - mPreference.setCallback(mVolumePreferenceCallback); - mPreference.setStream(getAudioStream()); - mPreference.setMuteIcon(getMuteIcon()); + setupVolPreference(screen); } } + protected void setupVolPreference(PreferenceScreen screen) { + mPreference = screen.findPreference(getPreferenceKey()); + mPreference.setCallback(mVolumePreferenceCallback); + mPreference.setStream(getAudioStream()); + mPreference.setMuteIcon(getMuteIcon()); + } + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void onResume() { if (mPreference != null) { diff --git a/tests/robotests/src/com/android/settings/notification/NotificationVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/NotificationVolumePreferenceControllerTest.java index 96b9e6219c8..7e7ad10d8c1 100644 --- a/tests/robotests/src/com/android/settings/notification/NotificationVolumePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/NotificationVolumePreferenceControllerTest.java @@ -18,6 +18,7 @@ package com.android.settings.notification; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -25,10 +26,17 @@ import android.content.Context; import android.content.res.Resources; import android.media.AudioManager; import android.os.Vibrator; +import android.provider.DeviceConfig; import android.service.notification.NotificationListenerService; import android.telephony.TelephonyManager; -import com.android.internal.R; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.testutils.shadow.ShadowDeviceConfig; import org.junit.Before; import org.junit.Test; @@ -37,11 +45,12 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.Shadows; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowDeviceConfig.class}) public class NotificationVolumePreferenceControllerTest { - @Mock private AudioHelper mHelper; @Mock @@ -52,6 +61,11 @@ public class NotificationVolumePreferenceControllerTest { private Vibrator mVibrator; @Mock private Resources mResources; + @Mock + private PreferenceManager mPreferenceManager; + + private static final String READ_DEVICE_CONFIG_PERMISSION = + "android.permission.READ_DEVICE_CONFIG"; private Context mContext; private NotificationVolumePreferenceController mController; @@ -87,7 +101,9 @@ public class NotificationVolumePreferenceControllerTest { public void isAvailable_voiceCapable_aliasedWithRing_shouldReturnFalse() { when(mResources.getBoolean( com.android.settings.R.bool.config_show_notification_volume)).thenReturn(true); - when(mResources.getBoolean(R.bool.config_alias_ring_notif_stream_types)).thenReturn(true); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false); NotificationVolumePreferenceController controller = new NotificationVolumePreferenceController(mContext); @@ -105,7 +121,9 @@ public class NotificationVolumePreferenceControllerTest { public void isAvailable_voiceCapable_separatedFromRing_shouldReturnTrue() { when(mResources.getBoolean( com.android.settings.R.bool.config_show_notification_volume)).thenReturn(true); - when(mResources.getBoolean(R.bool.config_alias_ring_notif_stream_types)).thenReturn(false); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "true", false); NotificationVolumePreferenceController controller = new NotificationVolumePreferenceController(mContext); @@ -170,4 +188,70 @@ public class NotificationVolumePreferenceControllerTest { .isTrue(); } + @Test + public void enableSeparateNotificationConfig_controllerBecomesAvailable() { + PreferenceScreen screen = spy(new PreferenceScreen(mContext, null)); + VolumeSeekBarPreference volumeSeekBarPreference = mock(VolumeSeekBarPreference.class); + when(screen.getPreferenceManager()).thenReturn(mPreferenceManager); + when(screen.getContext()).thenReturn(mContext); + when(mResources.getBoolean( + com.android.settings.R.bool.config_show_notification_volume)).thenReturn(true); + // block the alternative condition to enable controller + when(mTelephonyManager.isVoiceCapable()).thenReturn(true); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false); + + NotificationVolumePreferenceController controller = + new NotificationVolumePreferenceController(mContext); + when(screen.findPreference(controller.getPreferenceKey())) + .thenReturn(volumeSeekBarPreference); + + // allow the controller to subscribe + Shadows.shadowOf((android.app.Application) ApplicationProvider.getApplicationContext()) + .grantPermissions(READ_DEVICE_CONFIG_PERMISSION); + controller.onResume(); + controller.displayPreference(screen); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, Boolean.toString(true), + false); + + assertThat(controller.getAvailabilityStatus() + == BasePreferenceController.AVAILABLE).isTrue(); + } + + @Test + public void disableSeparateNotificationConfig_controllerBecomesUnavailable() { + PreferenceScreen screen = spy(new PreferenceScreen(mContext, null)); + VolumeSeekBarPreference volumeSeekBarPreference = mock(VolumeSeekBarPreference.class); + when(screen.getPreferenceManager()).thenReturn(mPreferenceManager); + when(screen.getContext()).thenReturn(mContext); + when(mResources.getBoolean( + com.android.settings.R.bool.config_show_notification_volume)).thenReturn(true); + + // block the alternative condition to enable controller + when(mTelephonyManager.isVoiceCapable()).thenReturn(true); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "true", false); + + NotificationVolumePreferenceController controller = + new NotificationVolumePreferenceController(mContext); + + when(screen.findPreference(controller.getPreferenceKey())) + .thenReturn(volumeSeekBarPreference); + + Shadows.shadowOf((android.app.Application) ApplicationProvider.getApplicationContext()) + .grantPermissions(READ_DEVICE_CONFIG_PERMISSION); + controller.onResume(); + controller.displayPreference(screen); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false); + + assertThat(controller.getAvailabilityStatus() + == BasePreferenceController.UNSUPPORTED_ON_DEVICE).isTrue(); + } + } diff --git a/tests/robotests/src/com/android/settings/notification/RingVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/RingVolumePreferenceControllerTest.java index 02757d52874..1ad26c71546 100644 --- a/tests/robotests/src/com/android/settings/notification/RingVolumePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/RingVolumePreferenceControllerTest.java @@ -27,10 +27,13 @@ import android.content.Context; import android.content.res.Resources; import android.media.AudioManager; import android.os.Vibrator; +import android.provider.DeviceConfig; import android.service.notification.NotificationListenerService; import android.telephony.TelephonyManager; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.settings.R; +import com.android.settings.testutils.shadow.ShadowDeviceConfig; import org.junit.Before; import org.junit.Test; @@ -39,9 +42,11 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowApplication; @RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowDeviceConfig.class}) public class RingVolumePreferenceControllerTest { @Mock @@ -124,9 +129,10 @@ public class RingVolumePreferenceControllerTest { // todo: verify that the title change is displayed, by examining the underlying preference @Test public void ringNotificationStreamsNotAliased_sliderTitleSetToRingOnly() { - when(mResources.getBoolean( - com.android.internal.R.bool.config_alias_ring_notif_stream_types)) - .thenReturn(false); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "true", false); + final RingVolumePreferenceController controller = new RingVolumePreferenceController(mContext); @@ -138,8 +144,9 @@ public class RingVolumePreferenceControllerTest { @Test public void ringNotificationStreamsAliased_sliderTitleIncludesBothRingNotification() { - when(mResources.getBoolean( - com.android.internal.R.bool.config_alias_ring_notif_stream_types)).thenReturn(true); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false); + final RingVolumePreferenceController control = new RingVolumePreferenceController(mContext); int expectedTitleId = R.string.ring_volume_option_title; @@ -150,39 +157,39 @@ public class RingVolumePreferenceControllerTest { @Test public void setHintsRing_aliased_Matches() { assertThat(mController.hintsMatch( - NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS, true)).isTrue(); + NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS, false)).isTrue(); } @Test public void setHintsRingNotification_aliased_Matches() { assertThat(mController.hintsMatch(NotificationListenerService.HINT_HOST_DISABLE_EFFECTS, - true)).isTrue(); + false)).isTrue(); } @Test public void setHintNotification_aliased_Matches() { assertThat(mController .hintsMatch(NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS, - true)).isTrue(); + false)).isTrue(); } @Test public void setHintsRing_unaliased_Matches() { assertThat(mController.hintsMatch( - NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS, false)).isTrue(); + NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS, true)).isTrue(); } @Test public void setHintsRingNotification_unaliased_Matches() { assertThat(mController.hintsMatch(NotificationListenerService.HINT_HOST_DISABLE_EFFECTS, - false)).isTrue(); + true)).isTrue(); } @Test public void setHintNotification_unaliased_doesNotMatch() { assertThat(mController .hintsMatch(NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS, - false)).isFalse(); + true)).isFalse(); } @Test From 13d3bdc4c028ceaaad662b8e091355a1166e2317 Mon Sep 17 00:00:00 2001 From: Grace Cheng Date: Thu, 6 Oct 2022 11:44:26 +0000 Subject: [PATCH 3/5] =?UTF-8?q?Implement=20SFPS=20=E2=80=9Crequire=20scree?= =?UTF-8?q?n=20on=20to=20auth=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creates new setting on SFPS to require screen on before unlocking a device. Also sets up toggles for this setting at the end of fingerprint enrollment and on the fingerprint settings page, and adds tests to verify expected behavior. Test: make RunSettingsRoboTests ROBOTEST_FILTER=FingerprintSettingsRequireScreenOnToAuthPreferenceControllerTest Fixes: 249169615 Fixes: 245343077 Fixes: 248530806 Change-Id: Id588796426d071860b3cc2af9ec5798c0027c202 Merged-In: Ia44604b059c4847c40608419b2e16219976ced3e --- res/layout/sfps_enroll_finish_base.xml | 16 +- .../sfps_require_screen_on_to_auth_toggle.xml | 79 ++++++++++ res/values/strings.xml | 8 + res/xml/security_settings_fingerprint.xml | 17 +- .../fingerprint/FingerprintEnrollFinish.java | 34 ++++ ...ingerprintRequireScreenOnToAuthToggle.java | 77 +++++++++ .../fingerprint/FingerprintSettings.java | 91 ++++++++++- ...ngerprintSettingsPreferenceController.java | 60 +++++++ ...ireScreenOnToAuthPreferenceController.java | 102 ++++++++++++ ...creenOnToAuthPreferenceControllerTest.java | 149 ++++++++++++++++++ 10 files changed, 623 insertions(+), 10 deletions(-) create mode 100644 res/layout/sfps_require_screen_on_to_auth_toggle.xml create mode 100644 src/com/android/settings/biometrics/fingerprint/FingerprintRequireScreenOnToAuthToggle.java create mode 100644 src/com/android/settings/biometrics/fingerprint/FingerprintSettingsPreferenceController.java create mode 100644 src/com/android/settings/biometrics/fingerprint/FingerprintSettingsRequireScreenOnToAuthPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsRequireScreenOnToAuthPreferenceControllerTest.java diff --git a/res/layout/sfps_enroll_finish_base.xml b/res/layout/sfps_enroll_finish_base.xml index 6e468c6472d..8d062d9409c 100644 --- a/res/layout/sfps_enroll_finish_base.xml +++ b/res/layout/sfps_enroll_finish_base.xml @@ -55,16 +55,20 @@ android:src="@drawable/sfps_enroll_finish" /> - - + + + android:layout_gravity="center_horizontal|bottom" /> - + \ No newline at end of file diff --git a/res/layout/sfps_require_screen_on_to_auth_toggle.xml b/res/layout/sfps_require_screen_on_to_auth_toggle.xml new file mode 100644 index 00000000000..929b64bb2c4 --- /dev/null +++ b/res/layout/sfps_require_screen_on_to_auth_toggle.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 42f8e4c0811..49b1ddd975a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1012,6 +1012,8 @@ Fingerprint + + When using Fingerprint Unlock Fingerprint for work @@ -1293,6 +1295,12 @@ Now you can use your fingerprint to unlock your device or verify it\u2019s you, like when you sign in to apps Now you can use your fingerprint to unlock your phone or verify it\u2019s you, like when you sign in to apps + + Unlock only when screen is on + + The screen must be on before you can unlock with your fingerprint. This makes accidental unlocking less likely. + + Screen, Unlock Do it later diff --git a/res/xml/security_settings_fingerprint.xml b/res/xml/security_settings_fingerprint.xml index 804ef881630..a4ce545a7ff 100644 --- a/res/xml/security_settings_fingerprint.xml +++ b/res/xml/security_settings_fingerprint.xml @@ -16,5 +16,20 @@ + xmlns:settings="http://schemas.android.com/apk/res-auto" + android:title="@string/security_settings_fingerprint_preference_title"> + + + + + + diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java index 0c7ef982408..45063104134 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java @@ -25,6 +25,7 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Bundle; import android.util.Log; import android.view.View; +import android.widget.CompoundButton; import androidx.annotation.VisibleForTesting; @@ -44,16 +45,24 @@ import java.util.List; public class FingerprintEnrollFinish extends BiometricEnrollBase { private static final String TAG = "FingerprintEnrollFinish"; + private static final String KEY_REQUIRE_SCREEN_ON_TO_AUTH = "require_screen_on_to_auth_toggle"; private static final String ACTION_FINGERPRINT_SETTINGS = "android.settings.FINGERPRINT_SETTINGS"; @VisibleForTesting static final String FINGERPRINT_SUGGESTION_ACTIVITY = "com.android.settings.SetupFingerprintSuggestionActivity"; + private FingerprintManager mFingerprintManager; + + private FingerprintSettingsRequireScreenOnToAuthPreferenceController + mRequireScreenOnToAuthPreferenceController; + private FingerprintRequireScreenOnToAuthToggle mRequireScreenOnToAuthToggle; private boolean mCanAssumeSfps; private boolean mIsAddAnotherOrFinish; + private CompoundButton.OnCheckedChangeListener mRequireScreenOnToAuthToggleListener; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -63,6 +72,11 @@ public class FingerprintEnrollFinish extends BiometricEnrollBase { mCanAssumeSfps = props != null && props.size() == 1 && props.get(0).isAnySidefpsType(); if (mCanAssumeSfps) { setContentView(R.layout.sfps_enroll_finish); + mRequireScreenOnToAuthPreferenceController = + new FingerprintSettingsRequireScreenOnToAuthPreferenceController( + getApplicationContext(), + KEY_REQUIRE_SCREEN_ON_TO_AUTH + ); } else { setContentView(R.layout.fingerprint_enroll_finish); } @@ -90,6 +104,20 @@ public class FingerprintEnrollFinish extends BiometricEnrollBase { .setTheme(R.style.SudGlifButton_Primary) .build() ); + + if (mCanAssumeSfps) { + mRequireScreenOnToAuthToggleListener = + (buttonView, isChecked) -> { + mRequireScreenOnToAuthPreferenceController.setChecked(isChecked); + }; + mRequireScreenOnToAuthToggle = findViewById(R.id.require_screen_on_to_auth_toggle); + mRequireScreenOnToAuthToggle.setChecked( + mRequireScreenOnToAuthPreferenceController.isChecked()); + mRequireScreenOnToAuthToggle.setListener(mRequireScreenOnToAuthToggleListener); + mRequireScreenOnToAuthToggle.setOnClickListener(v -> { + mRequireScreenOnToAuthToggle.getSwitch().toggle(); + }); + } } @Override @@ -103,6 +131,12 @@ public class FingerprintEnrollFinish extends BiometricEnrollBase { @Override protected void onResume() { super.onResume(); + if (mCanAssumeSfps) { + mRequireScreenOnToAuthToggleListener.onCheckedChanged( + mRequireScreenOnToAuthToggle.getSwitch(), + mRequireScreenOnToAuthToggle.isChecked() + ); + } FooterButton addButton = mFooterBarMixin.getSecondaryButton(); diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintRequireScreenOnToAuthToggle.java b/src/com/android/settings/biometrics/fingerprint/FingerprintRequireScreenOnToAuthToggle.java new file mode 100644 index 00000000000..f88c9aaec54 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintRequireScreenOnToAuthToggle.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.biometrics.fingerprint; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.Switch; + +import com.android.settings.R; + +/** + * A layout that contains a start-justified title, and an end-justified switch. + */ +public class FingerprintRequireScreenOnToAuthToggle extends LinearLayout { + private Switch mSwitch; + + public FingerprintRequireScreenOnToAuthToggle(Context context) { + this(context, null /* attrs */); + } + + public FingerprintRequireScreenOnToAuthToggle(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FingerprintRequireScreenOnToAuthToggle( + Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + LayoutInflater.from(context).inflate(R.layout.sfps_require_screen_on_to_auth_toggle, + this, true /* attachToRoot */); + + mSwitch = findViewById(R.id.toggle); + mSwitch.setClickable(true); + mSwitch.setFocusable(false); + } + + public boolean isChecked() { + return mSwitch.isChecked(); + } + + /** + * + * @param checked + */ + public void setChecked(boolean checked) { + mSwitch.setChecked(checked); + } + + /** + * + * @param listener + */ + public void setListener(CompoundButton.OnCheckedChangeListener listener) { + mSwitch.setOnCheckedChangeListener(listener); + } + + public Switch getSwitch() { + return mSwitch; + } +} diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java index c031fe646dd..1c7c891899e 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java @@ -56,21 +56,24 @@ import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; +import androidx.preference.SwitchPreference; import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; import com.android.settings.SubSettings; import com.android.settings.Utils; import com.android.settings.biometrics.BiometricEnrollBase; import com.android.settings.biometrics.BiometricUtils; import com.android.settings.core.SettingsBaseActivity; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.dashboard.DashboardFragment; import com.android.settings.password.ChooseLockGeneric; import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settingslib.HelpUtils; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.RestrictedSwitchPreference; +import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.transition.SettingsTransitionHelper; import com.android.settingslib.widget.FooterPreference; import com.android.settingslib.widget.TwoTargetPreference; @@ -115,7 +118,26 @@ public class FingerprintSettings extends SubSettings { setTitle(msg); } - public static class FingerprintSettingsFragment extends SettingsPreferenceFragment + /** + * @param context + * @return true if the Fingerprint hardware is detected. + */ + public static boolean isFingerprintHardwareDetected(Context context) { + FingerprintManager manager = Utils.getFingerprintManagerOrNull(context); + boolean isHardwareDetected = false; + if (manager == null) { + Log.d(TAG, "FingerprintManager is null"); + } else { + isHardwareDetected = manager.isHardwareDetected(); + Log.d(TAG, "FingerprintManager is not null. Hardware detected: " + isHardwareDetected); + } + return manager != null && isHardwareDetected; + } + + /** + * + */ + public static class FingerprintSettingsFragment extends DashboardFragment implements OnPreferenceChangeListener, FingerprintPreference.OnDeleteClickListener { private static class FooterColumn { @@ -134,6 +156,8 @@ public class FingerprintSettings extends SubSettings { private static final String KEY_LAUNCHED_CONFIRM = "launched_confirm"; private static final String KEY_HAS_FIRST_ENROLLED = "has_first_enrolled"; private static final String KEY_IS_ENROLLING = "is_enrolled"; + private static final String KEY_REQUIRE_SCREEN_ON_TO_AUTH = + "security_settings_require_screen_on_to_auth"; private static final int MSG_REFRESH_FINGERPRINT_TEMPLATES = 1000; private static final int MSG_FINGER_AUTH_SUCCESS = 1001; @@ -149,6 +173,11 @@ public class FingerprintSettings extends SubSettings { protected static final boolean DEBUG = false; + private List mControllers; + private FingerprintSettingsRequireScreenOnToAuthPreferenceController + mRequireScreenOnToAuthPreferenceController; + private RestrictedSwitchPreference mRequireScreenOnToAuthPreference; + private FingerprintManager mFingerprintManager; private FingerprintUpdater mFingerprintUpdater; private List mSensorProperties; @@ -214,6 +243,7 @@ public class FingerprintSettings extends SubSettings { } private void updateDialog() { + setRequireScreenOnToAuthVisibility(); RenameDialog renameDialog = (RenameDialog) getFragmentManager(). findFragmentByTag(RenameDialog.class.getName()); if (renameDialog != null) { @@ -448,13 +478,36 @@ public class FingerprintSettings extends SubSettings { if (root != null) { root.removeAll(); } - addPreferencesFromResource(R.xml.security_settings_fingerprint); root = getPreferenceScreen(); addFingerprintItemPreferences(root); + addPreferencesFromResource(getPreferenceScreenResId()); + mRequireScreenOnToAuthPreference = findPreference(KEY_REQUIRE_SCREEN_ON_TO_AUTH); + for (AbstractPreferenceController controller : mControllers) { + ((FingerprintSettingsPreferenceController) controller).setUserId(mUserId); + } + mRequireScreenOnToAuthPreference.setChecked( + mRequireScreenOnToAuthPreferenceController.isChecked()); + mRequireScreenOnToAuthPreference.setOnPreferenceChangeListener( + (preference, newValue) -> { + boolean isChecked = ((SwitchPreference) preference).isChecked(); + mRequireScreenOnToAuthPreferenceController.setChecked(!isChecked); + return true; + }); setPreferenceScreen(root); return root; } + private void setRequireScreenOnToAuthVisibility() { + int fingerprintsEnrolled = mFingerprintManager.getEnrolledFingerprints(mUserId).size(); + final boolean removalInProgress = mRemovalSidecar.inProgress(); + // Removing last remaining fingerprint + if (fingerprintsEnrolled == 0 && removalInProgress) { + mRequireScreenOnToAuthPreference.setVisible(false); + } else { + mRequireScreenOnToAuthPreference.setVisible(true); + } + } + private void addFingerprintItemPreferences(PreferenceGroup root) { root.removeAll(); final List items = mFingerprintManager.getEnrolledFingerprints(mUserId); @@ -477,6 +530,7 @@ public class FingerprintSettings extends SubSettings { root.addPreference(pref); pref.setOnPreferenceChangeListener(this); } + Preference addPreference = new Preference(root.getContext()); addPreference.setKey(KEY_FINGERPRINT_ADD); addPreference.setTitle(R.string.fingerprint_add_title); @@ -568,6 +622,16 @@ public class FingerprintSettings extends SubSettings { } } + @Override + protected int getPreferenceScreenResId() { + return R.xml.security_settings_fingerprint; + } + + @Override + protected String getLogTag() { + return TAG; + } + @Override public void onSaveInstanceState(final Bundle outState) { outState.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, @@ -662,6 +726,27 @@ public class FingerprintSettings extends SubSettings { return R.string.help_url_fingerprint; } + @Override + protected List createPreferenceControllers(Context context) { + if (!isFingerprintHardwareDetected(context)) { + return null; + } + + mControllers = buildPreferenceControllers(context); + return mControllers; + } + + private List buildPreferenceControllers(Context context) { + final List controllers = new ArrayList<>(); + mRequireScreenOnToAuthPreferenceController = + new FingerprintSettingsRequireScreenOnToAuthPreferenceController( + context, + KEY_REQUIRE_SCREEN_ON_TO_AUTH + ); + controllers.add(mRequireScreenOnToAuthPreferenceController); + return controllers; + } + @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsPreferenceController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsPreferenceController.java new file mode 100644 index 00000000000..2ca5da8928e --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsPreferenceController.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.biometrics.fingerprint; + +import android.app.admin.DevicePolicyManager; +import android.content.Context; + +import com.android.settings.core.TogglePreferenceController; +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; + +/** + * Abstract base class for all fingerprint settings toggles. + */ +public abstract class FingerprintSettingsPreferenceController extends TogglePreferenceController { + + private int mUserId; + + public FingerprintSettingsPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + public void setUserId(int userId) { + mUserId = userId; + } + + protected int getUserId() { + return mUserId; + } + + protected EnforcedAdmin getRestrictingAdmin() { + return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( + mContext, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT, mUserId); + } + + @Override + public final boolean isSliceable() { + return false; + } + + @Override + public int getSliceHighlightMenuRes() { + // not needed since it's not sliceable + return NO_RES; + } +} diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsRequireScreenOnToAuthPreferenceController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsRequireScreenOnToAuthPreferenceController.java new file mode 100644 index 00000000000..5b183c1433f --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsRequireScreenOnToAuthPreferenceController.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.biometrics.fingerprint; + +import android.content.Context; +import android.hardware.fingerprint.FingerprintManager; +import android.os.UserHandle; +import android.provider.Settings; + +import androidx.preference.Preference; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.Utils; + +/** + * Preference controller that controls whether a SFPS device is required to be interactive for + * fingerprint authentication to unlock the device. + */ +public class FingerprintSettingsRequireScreenOnToAuthPreferenceController + extends FingerprintSettingsPreferenceController { + private static final String TAG = + "FingerprintSettingsRequireScreenOnToAuthPreferenceController"; + + @VisibleForTesting + protected FingerprintManager mFingerprintManager; + + public FingerprintSettingsRequireScreenOnToAuthPreferenceController( + Context context, String prefKey) { + super(context, prefKey); + mFingerprintManager = Utils.getFingerprintManagerOrNull(context); + } + + @Override + public boolean isChecked() { + if (!FingerprintSettings.isFingerprintHardwareDetected(mContext)) { + return false; + } else if (getRestrictingAdmin() != null) { + return false; + } + int defaultValue = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_requireScreenOnToAuthEnabled) ? 1 : 0; + + return Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, + defaultValue, + getUserHandle()) != 0; + } + + @Override + public boolean setChecked(boolean isChecked) { + Settings.Secure.putIntForUser( + mContext.getContentResolver(), + Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, + isChecked ? 1 : 0, + getUserHandle()); + return true; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (!FingerprintSettings.isFingerprintHardwareDetected(mContext)) { + preference.setEnabled(false); + } else if (!mFingerprintManager.hasEnrolledTemplates(getUserId())) { + preference.setEnabled(false); + } else { + preference.setEnabled(true); + } + } + + @Override + public int getAvailabilityStatus() { + if (mFingerprintManager != null + && mFingerprintManager.isHardwareDetected() + && mFingerprintManager.isPowerbuttonFps()) { + return mFingerprintManager.hasEnrolledTemplates(getUserId()) + ? AVAILABLE : DISABLED_DEPENDENT_SETTING; + } else { + return UNSUPPORTED_ON_DEVICE; + } + } + + private int getUserHandle() { + return UserHandle.of(getUserId()).getIdentifier(); + } + +} diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsRequireScreenOnToAuthPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsRequireScreenOnToAuthPreferenceControllerTest.java new file mode 100644 index 00000000000..ff74d59782a --- /dev/null +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsRequireScreenOnToAuthPreferenceControllerTest.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.biometrics.fingerprint; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.DISABLED_DEPENDENT_SETTING; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.hardware.fingerprint.FingerprintManager; +import android.provider.Settings; + +import com.android.settings.testutils.shadow.ShadowUtils; +import com.android.settingslib.RestrictedSwitchPreference; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowUtils.class}) +public class FingerprintSettingsRequireScreenOnToAuthPreferenceControllerTest { + + @Mock + private FingerprintManager mFingerprintManager; + @Mock + private PackageManager mPackageManager; + @Mock + private RestrictedSwitchPreference mPreference; + + private Context mContext; + private FingerprintSettingsRequireScreenOnToAuthPreferenceController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + when(mContext.getSystemService(eq(Context.FINGERPRINT_SERVICE))).thenReturn( + mFingerprintManager); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + + mController = spy(new FingerprintSettingsRequireScreenOnToAuthPreferenceController(mContext, + "test_key")); + ReflectionHelpers.setField(mController, "mFingerprintManager", mFingerprintManager); + } + + @After + public void tearDown() { + ShadowUtils.reset(); + } + + @Test + public void onPreferenceChange_settingIsUpdated() { + boolean state = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, 1) != 0; + + assertThat(mController.isChecked()).isFalse(); + assertThat(mController.onPreferenceChange(mPreference, !state)).isTrue(); + boolean newState = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, 1) != 0; + assertThat(newState).isEqualTo(!state); + } + + @Test + public void isAvailable_isEnabled_whenSfpsHardwareDetected_AndHasEnrolledFingerprints() { + assertThat(mController.isAvailable()).isEqualTo(false); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + configure_hardwareDetected_isSfps_hasEnrolledTemplates( + true /* isHardwareDetected */, + true /* isPowerbuttonFps */, + true /* hasEnrolledTemplates */); + assertThat(mController.isAvailable()).isEqualTo(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void isAvailable_isDisabled_whenSfpsHardwareDetected_AndNoEnrolledFingerprints() { + assertThat(mController.isAvailable()).isEqualTo(false); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + configure_hardwareDetected_isSfps_hasEnrolledTemplates( + true /* isHardwareDetected */, + true /* isPowerbuttonFps */, + false /* hasEnrolledTemplates */); + assertThat(mController.isAvailable()).isEqualTo(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(DISABLED_DEPENDENT_SETTING); + } + + @Test + public void isUnavailable_whenHardwareNotDetected() { + assertThat(mController.isAvailable()).isFalse(); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + configure_hardwareDetected_isSfps_hasEnrolledTemplates( + false /* isHardwareDetected */, + true /* isPowerbuttonFps */, + true /* hasEnrolledTemplates */); + assertThat(mController.isAvailable()).isFalse(); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void isUnavailable_onNonSfpsDevice() { + assertThat(mController.isAvailable()).isFalse(); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + configure_hardwareDetected_isSfps_hasEnrolledTemplates( + true /* isHardwareDetected */, + false /* isPowerbuttonFps */, + true /* hasEnrolledTemplates */); + assertThat(mController.isAvailable()).isFalse(); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + private void configure_hardwareDetected_isSfps_hasEnrolledTemplates( + boolean isHardwareDetected, boolean isPowerbuttonFps, boolean hasEnrolledTemplates) { + when(mFingerprintManager.isHardwareDetected()).thenReturn(isHardwareDetected); + when(mFingerprintManager.isPowerbuttonFps()).thenReturn(isPowerbuttonFps); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(hasEnrolledTemplates); + } + + +} From 33033fe7557090b583f8ab6ba96fccbedfea361f Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 3 Nov 2022 16:26:37 +0800 Subject: [PATCH 4/5] Add InstantAppDomainsPreference for Spa To try: 1. adb shell am start -n com.android.settings/.spa.SpaActivity 2. Go to Apps -> All apps -> [One Instant App] -> Supported links Bug: 236346018 Test: Unit test Test: Manually with Settings App Change-Id: I344ddb9c2f3dbc47d38554bf45f04ca7c26c0e5f --- src/com/android/settings/Utils.java | 9 +- .../applications/OpenSupportedLinks.java | 5 +- .../spa/app/appinfo/AppInfoSettings.kt | 2 +- .../appinfo/InstantAppDomainsPreference.kt | 111 +++++++++++ .../InstantAppDomainsPreferenceTest.kt | 173 ++++++++++++++++++ 5 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt create mode 100644 tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index b2de0041b1b..8ee4ebac502 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -117,6 +117,7 @@ import com.android.settingslib.widget.AdaptiveIcon; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Set; public final class Utils extends com.android.settingslib.Utils { @@ -589,7 +590,9 @@ public final class Utils extends com.android.settingslib.Utils { return inflater.inflate(resId, parent, false); } - public static ArraySet getHandledDomains(PackageManager pm, String packageName) { + /** Gets all the domains that the given package could handled. */ + @NonNull + public static Set getHandledDomains(PackageManager pm, String packageName) { final List iviList = pm.getIntentFilterVerifications(packageName); final List filters = pm.getAllIntentFilters(packageName); @@ -597,9 +600,7 @@ public final class Utils extends com.android.settingslib.Utils { final ArraySet result = new ArraySet<>(); if (iviList != null && iviList.size() > 0) { for (IntentFilterVerificationInfo ivi : iviList) { - for (String host : ivi.getDomains()) { - result.add(host); - } + result.addAll(ivi.getDomains()); } } if (filters != null && filters.size() > 0) { diff --git a/src/com/android/settings/applications/OpenSupportedLinks.java b/src/com/android/settings/applications/OpenSupportedLinks.java index 4f5f2a8001c..c4e478cefcb 100644 --- a/src/com/android/settings/applications/OpenSupportedLinks.java +++ b/src/com/android/settings/applications/OpenSupportedLinks.java @@ -23,7 +23,6 @@ import android.app.settings.SettingsEnums; import android.content.pm.PackageManager; import android.os.Bundle; import android.text.TextUtils; -import android.util.ArraySet; import android.util.Log; import android.view.View; @@ -36,6 +35,8 @@ import com.android.settings.Utils; import com.android.settingslib.widget.FooterPreference; import com.android.settingslib.widget.SelectorWithWidgetPreference; +import java.util.Set; + /** * Display the Open Supported Links page. Allow users choose what kind supported links they need. */ @@ -195,7 +196,7 @@ public class OpenSupportedLinks extends AppInfoWithHeader implements @VisibleForTesting void addLinksToFooter(FooterPreference footer) { - final ArraySet result = Utils.getHandledDomains(mPackageManager, mPackageName); + final Set result = Utils.getHandledDomains(mPackageManager, mPackageName); if (result.isEmpty()) { Log.w(TAG, "Can't find any app links."); return; diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt index e3d0805434b..3a4d3f6529e 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt @@ -98,7 +98,7 @@ private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) { // TODO: notification_settings AppPermissionPreference(app) AppStoragePreference(app) - // TODO: instant_app_launch_supported_domain_urls + InstantAppDomainsPreference(app) AppDataUsagePreference(app) AppTimeSpentPreference(app) AppBatteryPreference(app) diff --git a/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt b/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt new file mode 100644 index 00000000000..3a7d50d24c9 --- /dev/null +++ b/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.spa.app.appinfo + +import android.content.Context +import android.content.pm.ApplicationInfo +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.android.settings.R +import com.android.settings.Utils +import com.android.settingslib.spa.framework.compose.collectAsStateWithLifecycle +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spaprivileged.framework.common.asUser +import com.android.settingslib.spaprivileged.model.app.userHandle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +@Composable +fun InstantAppDomainsPreference(app: ApplicationInfo) { + val context = LocalContext.current + if (!app.isInstantApp) return + + val presenter = remember { InstantAppDomainsPresenter(context, app) } + var openDialog by rememberSaveable { mutableStateOf(false) } + + Preference(object : PreferenceModel { + override val title = stringResource(R.string.app_launch_supported_domain_urls_title) + override val summary = presenter.summaryFlow.collectAsStateWithLifecycle( + initialValue = stringResource(R.string.summary_placeholder), + ) + override val onClick = { openDialog = true } + }) + + val domainsState = presenter.domainsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) + if (openDialog) { + Dialog(domainsState) { + openDialog = false + } + } +} + +@Composable +private fun Dialog(domainsState: State>, onDismissRequest: () -> Unit) { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = {}, + title = { + Text(stringResource(R.string.app_launch_supported_domain_urls_title)) + }, + text = { + Column { + domainsState.value.forEach { domain -> + Text( + text = domain, + modifier = Modifier.padding(vertical = SettingsDimension.itemPaddingAround), + ) + } + } + }, + ) +} + +private class InstantAppDomainsPresenter( + private val context: Context, + private val app: ApplicationInfo, +) { + private val userContext = context.asUser(app.userHandle) + private val userPackageManager = userContext.packageManager + + val domainsFlow = flow { + emit(Utils.getHandledDomains(userPackageManager, app.packageName)) + }.flowOn(Dispatchers.IO) + + val summaryFlow = domainsFlow.map { entries -> + when (entries.size) { + 0 -> context.getString(R.string.domain_urls_summary_none) + 1 -> context.getString(R.string.domain_urls_summary_one, entries.first()) + else -> context.getString(R.string.domain_urls_summary_some, entries.first()) + } + }.flowOn(Dispatchers.IO) +} diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt new file mode 100644 index 00000000000..9782817f742 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.spa.app.appinfo + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isDialog +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.settings.R +import com.android.settings.Utils +import com.android.settings.testutils.delay +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.MockitoSession +import org.mockito.Spy +import org.mockito.quality.Strictness +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class InstantAppDomainsPreferenceTest { + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var mockSession: MockitoSession + + @Spy + private val context: Context = ApplicationProvider.getApplicationContext() + + @Mock + private lateinit var packageManager: PackageManager + + @Before + fun setUp() { + mockSession = ExtendedMockito.mockitoSession() + .initMocks(this) + .mockStatic(Utils::class.java) + .strictness(Strictness.LENIENT) + .startMocking() + whenever(context.packageManager).thenReturn(packageManager) + Mockito.doReturn(context).`when`(context).createContextAsUser(any(), anyInt()) + mockDomains(emptySet()) + } + + @After + fun tearDown() { + mockSession.finishMocking() + } + + private fun mockDomains(domains: Set) { + whenever(Utils.getHandledDomains(packageManager, PACKAGE_NAME)).thenReturn(domains) + } + + @Test + fun notInstantApp_notDisplayed() { + val app = ApplicationInfo() + + setContent(app) + + composeTestRule.onRoot().assertIsNotDisplayed() + } + + @Test + fun title_displayed() { + setContent() + + composeTestRule + .onNodeWithText(context.getString(R.string.app_launch_supported_domain_urls_title)) + .assertIsDisplayed() + .assertIsEnabled() + } + + @Test + fun noDomain() { + mockDomains(emptySet()) + + setContent() + + composeTestRule.onNodeWithText(context.getString(R.string.domain_urls_summary_none)) + .assertIsDisplayed() + } + + @Test + fun oneDomain() { + mockDomains(setOf("abc")) + + setContent() + + composeTestRule.onNodeWithText("Open abc").assertIsDisplayed() + } + + @Test + fun twoDomains() { + mockDomains(setOf("abc", "def")) + + setContent() + + composeTestRule.onNodeWithText("Open abc and other URLs").assertIsDisplayed() + } + + @Test + fun whenClicked() { + mockDomains(setOf("abc", "def")) + + setContent() + composeTestRule.onRoot().performClick() + composeTestRule.delay() + + assertDialogHasText(context.getString(R.string.app_launch_supported_domain_urls_title)) + assertDialogHasText("abc") + assertDialogHasText("def") + } + + private fun assertDialogHasText(text: String) { + composeTestRule.onAllNodes(hasAnyAncestor(isDialog())) + .filterToOne(hasText(text)) + .assertIsDisplayed() + } + + private fun setContent(app:ApplicationInfo = INSTANT_APP) { + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + InstantAppDomainsPreference(app) + } + } + } + + private companion object { + const val PACKAGE_NAME = "package.name" + const val UID = 123 + + val INSTANT_APP = ApplicationInfo().apply { + packageName = PACKAGE_NAME + uid = UID + privateFlags = ApplicationInfo.PRIVATE_FLAG_INSTANT + } + } +} From eda172ce2c658b4ecee50d9b6e03defd3f03aaf6 Mon Sep 17 00:00:00 2001 From: Wesley Wang Date: Fri, 28 Oct 2022 18:25:21 +0800 Subject: [PATCH 5/5] Fix app battery usage list launch incorrect works app - App usage page only use package name to launch the page, it can not identify the different userId between normal app and work profile app, add extra userId info into launch args - Remove no battery usage state/string if usage time is empty Bug: 251471047 Test: make SettingsRoboTests Change-Id: Id06ebb0f0fcd863e423acd7839f89bc0ae2444c1 --- .../appinfo/AppBatteryPreferenceController.java | 3 ++- .../manageapplications/ManageApplications.java | 3 ++- .../settings/fuelgauge/AdvancedPowerUsageDetail.java | 10 ++++------ .../fuelgauge/AdvancedPowerUsageDetailTest.java | 9 +++++---- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java b/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java index 49f676ed508..19f3a46aaee 100644 --- a/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java @@ -22,6 +22,7 @@ import android.os.AsyncTask; import android.os.BatteryUsageStats; import android.os.Bundle; import android.os.UidBatteryConsumer; +import android.os.UserHandle; import android.os.UserManager; import android.util.Log; @@ -144,7 +145,7 @@ public class AppBatteryPreferenceController extends BasePreferenceController } else { Log.i(TAG, "Launch : " + mPackageName + " with package name"); AdvancedPowerUsageDetail.startBatteryDetailPage(mParent.getActivity(), mParent, - mPackageName); + mPackageName, UserHandle.CURRENT); } return true; } diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index 80b49312475..b678e24b5e2 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -686,7 +686,8 @@ public class ManageApplications extends InstrumentedFragment break; case LIST_TYPE_BATTERY_OPTIMIZATION: AdvancedPowerUsageDetail.startBatteryDetailPage( - getActivity(), this, mCurrentPkgName); + getActivity(), this, mCurrentPkgName, + UserHandle.getUserHandleForUid(mCurrentUid)); break; // TODO: Figure out if there is a way where we can spin up the profile's settings // process ahead of time, to avoid a long load of data when user clicks on a managed diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java index c08a1b8985e..9705154d7b3 100644 --- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java +++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java @@ -219,7 +219,8 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements * Start packageName's battery detail page. */ public static void startBatteryDetailPage( - Activity caller, Instrumentable instrumentable, String packageName) { + Activity caller, Instrumentable instrumentable, String packageName, + UserHandle userHandle) { final Bundle args = new Bundle(3); final PackageManager packageManager = caller.getPackageManager(); args.putString(EXTRA_PACKAGE_NAME, packageName); @@ -235,6 +236,7 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements .setTitleRes(R.string.battery_details_title) .setArguments(args) .setSourceMetricsCategory(instrumentable.getMetricsCategory()) + .setUserHandle(userHandle) .launch(); } @@ -475,16 +477,12 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements private CharSequence getAppActiveTime(Bundle bundle) { final long foregroundTimeMs = bundle.getLong(EXTRA_FOREGROUND_TIME); final long backgroundTimeMs = bundle.getLong(EXTRA_BACKGROUND_TIME); - final int consumedPower = bundle.getInt(EXTRA_POWER_USAGE_AMOUNT); - final int uid = bundle.getInt(EXTRA_UID, 0); final String slotTime = bundle.getString(EXTRA_SLOT_TIME, null); final long totalTimeMs = foregroundTimeMs + backgroundTimeMs; final CharSequence usageTimeSummary; if (totalTimeMs == 0) { - usageTimeSummary = getText( - consumedPower > 0 ? R.string.battery_usage_without_time - : R.string.battery_not_usage); + usageTimeSummary = getText(R.string.battery_usage_without_time); } else if (slotTime == null) { // Shows summary text with last full charge if slot time is null. usageTimeSummary = getAppFullChargeActiveSummary( diff --git a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java index 45e80891faa..2245920d046 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java @@ -280,8 +280,7 @@ public class AdvancedPowerUsageDetailTest { ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); verify(mEntityHeaderController).setSummary(captor.capture()); - assertThat(captor.getValue().toString()) - .isEqualTo("No usage from last full charge"); + assertThat(captor.getValue().toString()).isEmpty(); } @Test @@ -571,7 +570,8 @@ public class AdvancedPowerUsageDetailTest { public void startBatteryDetailPage_noBatteryUsage_hasBasicData() { final ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); - AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment, PACKAGE_NAME[0]); + AdvancedPowerUsageDetail.startBatteryDetailPage( + mActivity, mFragment, PACKAGE_NAME[0], UserHandle.OWNER); verify(mActivity).startActivity(captor.capture()); @@ -589,7 +589,8 @@ public class AdvancedPowerUsageDetailTest { PackageManager.NameNotFoundException { doReturn(UID).when(mPackageManager).getPackageUid(PACKAGE_NAME[0], 0 /* no flag */); - AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment, PACKAGE_NAME[0]); + AdvancedPowerUsageDetail.startBatteryDetailPage( + mActivity, mFragment, PACKAGE_NAME[0], UserHandle.OWNER); assertThat(mBundle.getInt(AdvancedPowerUsageDetail.EXTRA_UID)).isEqualTo(UID); }