From 81d230b250c3a42ca2e99b2d1530a1f7a3845414 Mon Sep 17 00:00:00 2001 From: Bonian Chen Date: Tue, 19 Apr 2022 23:04:13 +0800 Subject: [PATCH 1/8] [Settings] Code refactor for BroadcastReceiver under Lifecycle This is an implementation of BroadcastReceiver which supported by LifecycleCallbackConverter. Registration of BroadcastReceiver only take place when Lifecycle in RESUME status. Bug: 229689535 Test: unit test Change-Id: Ia2af82d5cbb391034627e5259a9e0c8683a0c5a1 (cherry picked from commit c2030898ef2540d5619c66d5fd4066f2267712bd) --- .../LifecycleCallbackIntentReceiver.java | 104 ++++++++++ .../LifecycleCallbackIntentReceiverTest.java | 184 ++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 src/com/android/settings/network/helper/LifecycleCallbackIntentReceiver.java create mode 100644 tests/unit/src/com/android/settings/network/helper/LifecycleCallbackIntentReceiverTest.java diff --git a/src/com/android/settings/network/helper/LifecycleCallbackIntentReceiver.java b/src/com/android/settings/network/helper/LifecycleCallbackIntentReceiver.java new file mode 100644 index 00000000000..8aaa53e0af8 --- /dev/null +++ b/src/com/android/settings/network/helper/LifecycleCallbackIntentReceiver.java @@ -0,0 +1,104 @@ +/* + * 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.network.helper; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.Lifecycle; +import java.util.function.Consumer; + +/** + * A {@link BroadcastReceiver} for {@link Intent}. + * + * This is {@link BroadcastReceiver} supported by {@link LifecycleCallbackConverter}, + * and only register when state is either START or RESUME. + */ +@VisibleForTesting +public class LifecycleCallbackIntentReceiver extends LifecycleCallbackConverter { + private static final String TAG = "LifecycleCallbackIntentReceiver"; + + @VisibleForTesting + protected final BroadcastReceiver mReceiver; + + private final Runnable mRegisterCallback; + private final Runnable mUnRegisterCallback; + + /** + * Constructor + * @param lifecycle {@link Lifecycle} to monitor + * @param context for this BroadcastReceiver + * @param filter the IntentFilter for BroadcastReceiver + * @param broadcastPermission for permission when listening + * @param scheduler for running in background thread + * @param resultCallback for the Intent from BroadcastReceiver + */ + @VisibleForTesting + public LifecycleCallbackIntentReceiver(@NonNull Lifecycle lifecycle, + @NonNull Context context, @NonNull IntentFilter filter, + String broadcastPermission, Handler scheduler, + @NonNull Consumer resultCallback) { + super(lifecycle, resultCallback); + + // BroadcastReceiver + mReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + if (isInitialStickyBroadcast()) { + return; + } + final String action = intent.getAction(); + if ((action == null) || (action.length() <= 0)) { + return; + } + postResult(intent); + } + }; + + // Register operation + mRegisterCallback = () -> { + Intent initIntent = context.registerReceiver(mReceiver, + filter, broadcastPermission, scheduler); + if (initIntent != null) { + postResult(initIntent); + } + }; + + // Un-Register operation + mUnRegisterCallback = () -> { + context.unregisterReceiver(mReceiver); + }; + } + + @Override + public void setCallbackActive(boolean isActive) { + super.setCallbackActive(isActive); + Runnable op = (isActive) ? mRegisterCallback : mUnRegisterCallback; + op.run(); + } + + @Override + public void close() { + super.close(); + if (isCallbackActive()) { + setCallbackActive(false); + } + } +} diff --git a/tests/unit/src/com/android/settings/network/helper/LifecycleCallbackIntentReceiverTest.java b/tests/unit/src/com/android/settings/network/helper/LifecycleCallbackIntentReceiverTest.java new file mode 100644 index 00000000000..c85937d3a34 --- /dev/null +++ b/tests/unit/src/com/android/settings/network/helper/LifecycleCallbackIntentReceiverTest.java @@ -0,0 +1,184 @@ +/* + * 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.network.helper; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.HandlerThread; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.function.Consumer; + +@RunWith(AndroidJUnit4.class) +public class LifecycleCallbackIntentReceiverTest implements LifecycleOwner { + + private final LifecycleRegistry mRegistry = LifecycleRegistry.createUnsafe(this); + + private static final String TEST_SCHEDULER_HANDLER = "testScheduler"; + private static final String TEST_INTENT_ACTION = "testAction"; + private static final String TEST_INTENT_PERMISSION = "testPermission"; + + private Context mContext; + private Intent mIntent; + private IntentFilter mIntentFilter; + private Handler mHandler; + private TestConsumer mConsumer; + + private TestObj mTarget; + + @Before + public void setUp() { + mContext = ApplicationProvider.getApplicationContext(); + + mIntentFilter = new IntentFilter(TEST_INTENT_ACTION); + mIntent = new Intent(TEST_INTENT_ACTION); + + HandlerThread thread = new HandlerThread(TEST_SCHEDULER_HANDLER); + thread.start(); + + mHandler = new Handler(thread.getLooper()); + mConsumer = new TestConsumer(); + + mTarget = new TestObj(getLifecycle(), mContext, + mIntentFilter, TEST_INTENT_PERMISSION, + mHandler, mConsumer); + } + + public Lifecycle getLifecycle() { + return mRegistry; + } + + @Test + public void receiver_register_whenActive() { + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + + assertThat(mTarget.getCallbackActiveCount(true) + + mTarget.getCallbackActiveCount(false)).isEqualTo(0); + + mTarget.mReceiver.onReceive(mContext, mIntent); + + assertThat(mConsumer.getCallbackCount()).isEqualTo(0); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + assertThat(mTarget.getCallbackActiveCount(true)).isEqualTo(1); + assertThat(mConsumer.getCallbackCount()).isEqualTo(0); + + mTarget.mReceiver.onReceive(mContext, mIntent); + + assertThat(mConsumer.getCallbackCount()).isEqualTo(1); + assertThat(mConsumer.getData()).isEqualTo(mIntent); + } + + @Test + public void receiver_unregister_whenInActive() { + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + + assertThat(mTarget.getCallbackActiveCount(false)).isEqualTo(1); + + mTarget.mReceiver.onReceive(mContext, mIntent); + + assertThat(mConsumer.getCallbackCount()).isEqualTo(0); + } + + @Test + public void receiver_register_whenReActive() { + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + assertThat(mTarget.getCallbackActiveCount(true)).isEqualTo(2); + + mTarget.mReceiver.onReceive(mContext, mIntent); + + assertThat(mConsumer.getCallbackCount()).isEqualTo(1); + assertThat(mConsumer.getData()).isEqualTo(mIntent); + } + + @Test + public void receiver_close_whenDestroy() { + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); + + assertThat(mTarget.getCallbackActiveCount(false)).isEqualTo(1); + + mTarget.mReceiver.onReceive(mContext, mIntent); + + assertThat(mConsumer.getCallbackCount()).isEqualTo(0); + } + + public static class TestConsumer implements Consumer { + long mNumberOfCallback; + Intent mLatestData; + + public TestConsumer() {} + + public void accept(Intent data) { + mLatestData = data; + mNumberOfCallback ++; + } + + protected long getCallbackCount() { + return mNumberOfCallback; + } + + protected Intent getData() { + return mLatestData; + } + } + + public static class TestObj extends LifecycleCallbackIntentReceiver { + long mCallbackActiveCount; + long mCallbackInActiveCount; + + public TestObj(Lifecycle lifecycle, Context context, IntentFilter filter, + String broadcastPermission, Handler scheduler, Consumer resultCallback) { + super(lifecycle, context, filter, broadcastPermission, scheduler, resultCallback); + } + + @Override + public void setCallbackActive(boolean isActive) { + if (isActive) { + mCallbackActiveCount ++; + } else { + mCallbackInActiveCount ++; + } + super.setCallbackActive(isActive); + } + + protected long getCallbackActiveCount(boolean forActive) { + return forActive ? mCallbackActiveCount : mCallbackInActiveCount; + } + } +} From aeac0619dff98dbc1ca086da3178caf5a7d35034 Mon Sep 17 00:00:00 2001 From: Bonian Chen Date: Thu, 21 Apr 2022 11:04:44 +0800 Subject: [PATCH 2/8] [Settings] Code refactor for combining TelephonyCallback with Lifecycle This is a helper class which may support monitoring the TelephonyCallback under the condition of Lifecycle state STARTED or RESUMED. Bug: 229689535 Test: unit test Change-Id: I6d26bee604d9559e207e107b2f26583f700f8e0a (cherry picked from commit 7c5259efad7640fdd9b5d94110cb3073e8a9c1d2) --- .../LifecycleCallbackTelephonyAdapter.java | 72 ++++++++++++ ...LifecycleCallbackTelephonyAdapterTest.java | 105 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 src/com/android/settings/network/helper/LifecycleCallbackTelephonyAdapter.java create mode 100644 tests/unit/src/com/android/settings/network/helper/LifecycleCallbackTelephonyAdapterTest.java diff --git a/src/com/android/settings/network/helper/LifecycleCallbackTelephonyAdapter.java b/src/com/android/settings/network/helper/LifecycleCallbackTelephonyAdapter.java new file mode 100644 index 00000000000..0fae4f27aaf --- /dev/null +++ b/src/com/android/settings/network/helper/LifecycleCallbackTelephonyAdapter.java @@ -0,0 +1,72 @@ +/* + * 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.network.helper; + +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.Lifecycle; + +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * A {@link LifecycleCallbackConverter} for supporting the register/unregister work for + * {@link TelephonyCallback}. + */ +@VisibleForTesting +public class LifecycleCallbackTelephonyAdapter extends LifecycleCallbackConverter { + private static final String TAG = "LifecycleCallbackTelephony"; + + private final Runnable mRegisterCallback; + private final Runnable mUnRegisterCallback; + + /** + * Constructor + * @param lifecycle {@link Lifecycle} to monitor + * @param telephonyManager {@link TelephonyManager} to interact with + * @param telephonyCallback {@link TelephonyCallback} + * @param executor {@link Executor} for receiving the notify from telephony framework. + * @param resultCallback for the result from {@link TelephonyCallback} + */ + @VisibleForTesting + public LifecycleCallbackTelephonyAdapter(@NonNull Lifecycle lifecycle, + @NonNull TelephonyManager telephonyManager, + @NonNull TelephonyCallback telephonyCallback, + Executor executor, @NonNull Consumer resultCallback) { + super(lifecycle, resultCallback); + + // Register operation + mRegisterCallback = () -> { + telephonyManager.registerTelephonyCallback(executor, telephonyCallback); + }; + + // Un-Register operation + mUnRegisterCallback = () -> { + telephonyManager.unregisterTelephonyCallback(telephonyCallback); + }; + } + + @Override + public void setCallbackActive(boolean isActive) { + super.setCallbackActive(isActive); + Runnable op = (isActive) ? mRegisterCallback : mUnRegisterCallback; + op.run(); + } +} diff --git a/tests/unit/src/com/android/settings/network/helper/LifecycleCallbackTelephonyAdapterTest.java b/tests/unit/src/com/android/settings/network/helper/LifecycleCallbackTelephonyAdapterTest.java new file mode 100644 index 00000000000..be940f2fa95 --- /dev/null +++ b/tests/unit/src/com/android/settings/network/helper/LifecycleCallbackTelephonyAdapterTest.java @@ -0,0 +1,105 @@ +/* + * 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.network.helper; + +import static org.mockito.ArgumentMatchers.anyObject; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.atomic.AtomicReference; + +@RunWith(AndroidJUnit4.class) +public class LifecycleCallbackTelephonyAdapterTest implements LifecycleOwner { + + private final LifecycleRegistry mRegistry = LifecycleRegistry.createUnsafe(this); + + @Mock + private TelephonyManager mTelMgr; + + private TestCallback mTestCallback; + private AtomicReference mResult; + private LifecycleCallbackTelephonyAdapter mAdapter; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mResult = new AtomicReference(); + mTestCallback = new TestCallback(); + + doNothing().when(mTelMgr).registerTelephonyCallback(null, mTestCallback); + doNothing().when(mTelMgr).unregisterTelephonyCallback(mTestCallback); + + mAdapter = new LifecycleCallbackTelephonyAdapter(getLifecycle(), mTelMgr, + mTestCallback, null, result -> mResult.set(result)); + } + + public Lifecycle getLifecycle() { + return mRegistry; + } + + @Test + public void telephonyCallback_register_whenActive() { + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + + verify(mTelMgr, never()).registerTelephonyCallback(anyObject(), anyObject()); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + verify(mTelMgr).registerTelephonyCallback(anyObject(), anyObject()); + } + + @Test + public void telephonyCallback_unregister_whenInActive() { + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + + verify(mTelMgr, never()).unregisterTelephonyCallback(anyObject()); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + verify(mTelMgr, never()).unregisterTelephonyCallback(anyObject()); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + + verify(mTelMgr).unregisterTelephonyCallback(anyObject()); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); + + verify(mTelMgr, times(1)).unregisterTelephonyCallback(anyObject()); + } + + protected static class TestCallback extends TelephonyCallback + implements TelephonyCallback.CallStateListener { + @Override + public void onCallStateChanged(int state) {} + } +} From 640247eb583608e86a3300e1816218b8c58b8938 Mon Sep 17 00:00:00 2001 From: Will Date: Tue, 12 Apr 2022 16:22:06 -0700 Subject: [PATCH 3/8] Disable screen saver setting for non-system users. On devices where non-system users are not allowed to dream, disable the screen saver setting when the current user is not the system user. Bug: 213906883 Test: atest ScreenSaverPreferenceControllerTest Test: Manually with the following steps: Step 1: On a device with the config setting config_dreamsOnlyEnabledForSystemUser set to false, make sure that non-system users can still access the screen saver setting. Step 2: On a device with the config setting config_dreamsOnlyEnabledForSystemUser setting is set to true, make sure that non-system users can not access (or see) the screen saver setting. Step 3: Make sure that system users can still see the screen saver setting on a device where config_dreamsOnlyEnabledForSystemUser is set to true. Change-Id: I7e167ed5a1ea183c725ba89a57b8d0b372064b82 --- .../ScreenSaverPreferenceController.java | 11 +- .../ScreenSaverPreferenceControllerTest.java | 112 ++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 tests/unit/src/com/android/settings/display/ScreenSaverPreferenceControllerTest.java diff --git a/src/com/android/settings/display/ScreenSaverPreferenceController.java b/src/com/android/settings/display/ScreenSaverPreferenceController.java index c1b0b4e9eb6..676a567f202 100644 --- a/src/com/android/settings/display/ScreenSaverPreferenceController.java +++ b/src/com/android/settings/display/ScreenSaverPreferenceController.java @@ -14,6 +14,7 @@ package com.android.settings.display; import android.content.Context; +import android.os.UserManager; import androidx.preference.Preference; @@ -32,8 +33,11 @@ public class ScreenSaverPreferenceController extends AbstractPreferenceControlle @Override public boolean isAvailable() { - return mContext.getResources().getBoolean( + final boolean dreamsSupported = mContext.getResources().getBoolean( com.android.internal.R.bool.config_dreamsSupported); + final boolean dreamsOnlyEnabledForSystemUser = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_dreamsOnlyEnabledForSystemUser); + return dreamsSupported && (!dreamsOnlyEnabledForSystemUser || isSystemUser()); } @Override @@ -45,4 +49,9 @@ public class ScreenSaverPreferenceController extends AbstractPreferenceControlle public void updateState(Preference preference) { preference.setSummary(DreamSettings.getSummaryTextWithDreamName(mContext)); } + + private boolean isSystemUser() { + final UserManager userManager = mContext.getSystemService(UserManager.class); + return userManager.isSystemUser(); + } } diff --git a/tests/unit/src/com/android/settings/display/ScreenSaverPreferenceControllerTest.java b/tests/unit/src/com/android/settings/display/ScreenSaverPreferenceControllerTest.java new file mode 100644 index 00000000000..3319e2a710d --- /dev/null +++ b/tests/unit/src/com/android/settings/display/ScreenSaverPreferenceControllerTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.display; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.os.UserManager; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +@RunWith(AndroidJUnit4.class) +public class ScreenSaverPreferenceControllerTest { + @Spy + private final Context mContext = ApplicationProvider.getApplicationContext(); + @Spy + private final Resources mResources = mContext.getResources(); + @Mock + private UserManager mUserManager; + + private ScreenSaverPreferenceController mController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mController = new ScreenSaverPreferenceController(mContext); + + when(mContext.getResources()).thenReturn(mResources); + when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); + } + + @Test + public void isAvailable_dreamsEnabledForAllUsers_shouldBeTrueForSystemUser() { + when(mResources.getBoolean( + com.android.internal.R.bool.config_dreamsSupported)).thenReturn(true); + when(mResources.getBoolean( + com.android.internal.R.bool.config_dreamsOnlyEnabledForSystemUser)) + .thenReturn(false); + when(mUserManager.isSystemUser()).thenReturn(true); + assertTrue(mController.isAvailable()); + } + + @Test + public void isAvailable_dreamsEnabledForAllUsers_shouldBeTrueForNonSystemUser() { + when(mResources.getBoolean( + com.android.internal.R.bool.config_dreamsSupported)).thenReturn(true); + when(mResources.getBoolean( + com.android.internal.R.bool.config_dreamsOnlyEnabledForSystemUser)) + .thenReturn(false); + when(mUserManager.isSystemUser()).thenReturn(false); + assertTrue(mController.isAvailable()); + } + + @Test + public void isAvailable_dreamsDisabled_shouldBeFalseForSystemUser() { + when(mResources.getBoolean( + com.android.internal.R.bool.config_dreamsSupported)).thenReturn(false); + when(mResources.getBoolean( + com.android.internal.R.bool.config_dreamsOnlyEnabledForSystemUser)) + .thenReturn(false); + when(mUserManager.isSystemUser()).thenReturn(true); + assertFalse(mController.isAvailable()); + } + + @Test + public void isAvailable_dreamsOnlyEnabledForSystemUser_shouldBeTrueForSystemUser() { + when(mResources.getBoolean( + com.android.internal.R.bool.config_dreamsSupported)).thenReturn(true); + when(mResources.getBoolean( + com.android.internal.R.bool.config_dreamsOnlyEnabledForSystemUser)) + .thenReturn(true); + when(mUserManager.isSystemUser()).thenReturn(true); + assertTrue(mController.isAvailable()); + } + + @Test + public void isAvailable_dreamsOnlyEnabledForSystemUser_shouldBeFalseForNonSystemUser() { + when(mResources.getBoolean( + com.android.internal.R.bool.config_dreamsSupported)).thenReturn(true); + when(mResources.getBoolean( + com.android.internal.R.bool.config_dreamsOnlyEnabledForSystemUser)) + .thenReturn(true); + when(mUserManager.isSystemUser()).thenReturn(false); + assertFalse(mController.isAvailable()); + } +} From e48ebb9755dcbbac0679520872ad07b3eb0dd488 Mon Sep 17 00:00:00 2001 From: jasonwshsu Date: Mon, 25 Apr 2022 02:53:15 +0800 Subject: [PATCH 4/8] Correct Magnification shortcut title Root Cause: Magnification has its own ShortcutPreference, but we did not update correct title with getShortcutTitle() Solution: Call setTitle(getShortcutTitle()) in initShortcutPreference() Bug: 228752572 Test: manual test Change-Id: I85b09a22032bad0a0c840590a98be88538a0499b --- .../ToggleScreenMagnificationPreferenceFragment.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index d9a02c00a8a..a6aeb897357 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -470,9 +470,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends mShortcutPreference.setKey(getShortcutPreferenceKey()); mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); mShortcutPreference.setOnClickCallback(this); - - final CharSequence title = getString(R.string.accessibility_shortcut_title, mPackageName); - mShortcutPreference.setTitle(title); + mShortcutPreference.setTitle(getShortcutTitle()); final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY); generalCategory.addPreference(mShortcutPreference); From bf3a207fb5fe0ec0658e3c8b1fa5c91c03e5c7ff Mon Sep 17 00:00:00 2001 From: Jack Yu Date: Mon, 25 Apr 2022 15:27:05 +0800 Subject: [PATCH 5/8] Change the alignment of the foreground preferernce controller Set left align to the nfc foreground preference controller. Bug: 223073535 Test: manual Change-Id: I0d4ffcedb2646b9d3fd7f4d56adb0d000cfd5987 --- .../android/settings/nfc/NfcForegroundPreferenceController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/settings/nfc/NfcForegroundPreferenceController.java b/src/com/android/settings/nfc/NfcForegroundPreferenceController.java index 246bdb9716e..611d3fcb56f 100644 --- a/src/com/android/settings/nfc/NfcForegroundPreferenceController.java +++ b/src/com/android/settings/nfc/NfcForegroundPreferenceController.java @@ -93,7 +93,7 @@ public class NfcForegroundPreferenceController extends BasePreferenceController return; } final ListPreference listPreference = (ListPreference) preference; - listPreference.setIconSpaceReserved(true); + listPreference.setIconSpaceReserved(false); listPreference.setValue(mListValues[mPaymentBackend.isForegroundMode() ? 1 : 0]); } From 880471a1b09b566b8abc5bc8b228fa569c8f9cda Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Mon, 25 Apr 2022 15:32:06 +0800 Subject: [PATCH 6/8] Renew the highlight state color Fix: 230283855 Test: visual Change-Id: I725422518d07a0b0676731bd9da8384ab149ab1b --- res/color-night/accent_select_primary_text.xml | 18 ++++++++++++++++++ .../accent_select_secondary_text.xml | 18 ++++++++++++++++++ res/color/accent_select_background.xml | 18 ++++++++++++++++++ res/color/accent_select_primary_text.xml | 18 ++++++++++++++++++ res/color/accent_select_secondary_text.xml | 18 ++++++++++++++++++ .../homepage_highlighted_item_background.xml | 2 +- src/com/android/settings/Utils.java | 5 ++--- ...HighlightableTopLevelPreferenceAdapter.java | 6 ++---- 8 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 res/color-night/accent_select_primary_text.xml create mode 100644 res/color-night/accent_select_secondary_text.xml create mode 100644 res/color/accent_select_background.xml create mode 100644 res/color/accent_select_primary_text.xml create mode 100644 res/color/accent_select_secondary_text.xml diff --git a/res/color-night/accent_select_primary_text.xml b/res/color-night/accent_select_primary_text.xml new file mode 100644 index 00000000000..049ddf63678 --- /dev/null +++ b/res/color-night/accent_select_primary_text.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/res/color-night/accent_select_secondary_text.xml b/res/color-night/accent_select_secondary_text.xml new file mode 100644 index 00000000000..6b7b4f27dcf --- /dev/null +++ b/res/color-night/accent_select_secondary_text.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/res/color/accent_select_background.xml b/res/color/accent_select_background.xml new file mode 100644 index 00000000000..5dab7230532 --- /dev/null +++ b/res/color/accent_select_background.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/res/color/accent_select_primary_text.xml b/res/color/accent_select_primary_text.xml new file mode 100644 index 00000000000..f790d425a1a --- /dev/null +++ b/res/color/accent_select_primary_text.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/res/color/accent_select_secondary_text.xml b/res/color/accent_select_secondary_text.xml new file mode 100644 index 00000000000..25d24a8cdac --- /dev/null +++ b/res/color/accent_select_secondary_text.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/homepage_highlighted_item_background.xml b/res/drawable/homepage_highlighted_item_background.xml index d54ff601643..7e0dea546f3 100644 --- a/res/drawable/homepage_highlighted_item_background.xml +++ b/res/drawable/homepage_highlighted_item_background.xml @@ -17,7 +17,7 @@ + android:color="@color/accent_select_background" /> \ No newline at end of file diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index 2988ddcaedb..f996444b482 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -85,7 +85,6 @@ import android.text.TextUtils; import android.text.format.DateUtils; import android.text.style.TtsSpan; import android.util.ArraySet; -import android.util.FeatureFlagUtils; import android.util.IconDrawableFactory; import android.util.Log; import android.view.LayoutInflater; @@ -1220,7 +1219,7 @@ public final class Utils extends com.android.settingslib.Utils { */ @ColorInt public static int getHomepageIconColor(Context context) { - return getColorAttrDefaultColor(context, android.R.attr.textColorSecondary); + return getColorAttrDefaultColor(context, android.R.attr.textColorPrimary); } /** @@ -1228,6 +1227,6 @@ public final class Utils extends com.android.settingslib.Utils { */ @ColorInt public static int getHomepageIconColorHighlight(Context context) { - return getColorAttrDefaultColor(context, android.R.attr.textColorSecondaryInverse); + return context.getColor(R.color.accent_select_primary_text); } } diff --git a/src/com/android/settings/widget/HighlightableTopLevelPreferenceAdapter.java b/src/com/android/settings/widget/HighlightableTopLevelPreferenceAdapter.java index 338be483b6d..3cc7ef27f7e 100644 --- a/src/com/android/settings/widget/HighlightableTopLevelPreferenceAdapter.java +++ b/src/com/android/settings/widget/HighlightableTopLevelPreferenceAdapter.java @@ -78,12 +78,10 @@ public class HighlightableTopLevelPreferenceAdapter extends PreferenceGroupAdapt Context context = preferenceGroup.getContext(); mTitleColorNormal = Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary); - mTitleColorHighlight = Utils.getColorAttrDefaultColor(context, - android.R.attr.textColorPrimaryInverse); + mTitleColorHighlight = context.getColor(R.color.accent_select_primary_text); mSummaryColorNormal = Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary); - mSummaryColorHighlight = Utils.getColorAttrDefaultColor(context, - android.R.attr.textColorSecondaryInverse); + mSummaryColorHighlight = context.getColor(R.color.accent_select_secondary_text); mIconColorNormal = Utils.getHomepageIconColor(context); mIconColorHighlight = Utils.getHomepageIconColorHighlight(context); } From f25049378751acd8da848a214b846c6f773e80f3 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 12 Apr 2022 20:31:10 +0800 Subject: [PATCH 7/8] Update Personal / work selection UI. Implement the new-look by using AlertDialog's custom title and custom view. Using the RecyclerView so we can display profile horizontally. Bug: 174626616 Test: manual & robolectric Change-Id: I9f5a7685d9217fc62e01799ad73f9b9a3ddbf19a --- res/drawable/user_select_background.xml | 32 +++ res/layout/user_select.xml | 29 +++ res/layout/user_select_item.xml | 47 ++++ res/layout/user_select_title.xml | 27 ++ .../profileselector/ProfileSelectDialog.java | 69 +++-- .../profileselector/UserAdapter.java | 243 ++++++++++-------- ...thServiceSettingsPreferenceController.java | 62 ++--- .../ProfileSelectDialogTest.java | 27 +- .../profileselector/UserAdapterTest.java | 122 +++++++++ 9 files changed, 486 insertions(+), 172 deletions(-) create mode 100644 res/drawable/user_select_background.xml create mode 100644 res/layout/user_select.xml create mode 100644 res/layout/user_select_item.xml create mode 100644 res/layout/user_select_title.xml create mode 100644 tests/robotests/src/com/android/settings/dashboard/profileselector/UserAdapterTest.java diff --git a/res/drawable/user_select_background.xml b/res/drawable/user_select_background.xml new file mode 100644 index 00000000000..777bff95db7 --- /dev/null +++ b/res/drawable/user_select_background.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + diff --git a/res/layout/user_select.xml b/res/layout/user_select.xml new file mode 100644 index 00000000000..8c8c37a64fe --- /dev/null +++ b/res/layout/user_select.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/res/layout/user_select_item.xml b/res/layout/user_select_item.xml new file mode 100644 index 00000000000..fa0c91a4ab5 --- /dev/null +++ b/res/layout/user_select_item.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + diff --git a/res/layout/user_select_title.xml b/res/layout/user_select_title.xml new file mode 100644 index 00000000000..e01ca22554f --- /dev/null +++ b/res/layout/user_select_title.xml @@ -0,0 +1,27 @@ + + + diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java index d3234dd1151..f82694c5841 100644 --- a/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java +++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java @@ -20,7 +20,6 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; -import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnDismissListener; import android.content.DialogInterface.OnShowListener; import android.content.Intent; @@ -28,17 +27,27 @@ import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentManager; +import com.android.internal.widget.DialogTitle; +import com.android.internal.widget.LinearLayoutManager; +import com.android.internal.widget.RecyclerView; +import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.drawer.Tile; import java.util.List; -public class ProfileSelectDialog extends DialogFragment implements OnClickListener { +/** + * A {@link DialogFragment} that can select one of the different profiles. + */ +public class ProfileSelectDialog extends DialogFragment implements UserAdapter.OnClickListener { private static final String TAG = "ProfileSelectDialog"; private static final String ARG_SELECTED_TILE = "selectedTile"; @@ -53,12 +62,13 @@ public class ProfileSelectDialog extends DialogFragment implements OnClickListen /** * Display the profile select dialog, adding the fragment to the given FragmentManager. - * @param manager The FragmentManager this fragment will be added to. - * @param tile The tile for this fragment. + * + * @param manager The FragmentManager this fragment will be added to. + * @param tile The tile for this fragment. * @param sourceMetricCategory The source metric category. - * @param onShowListener The listener listens to the dialog showing event. - * @param onDismissListener The listener listens to the dialog dismissing event. - * @param onCancelListener The listener listens to the dialog cancelling event. + * @param onShowListener The listener listens to the dialog showing event. + * @param onDismissListener The listener listens to the dialog dismissing event. + * @param onCancelListener The listener listens to the dialog cancelling event. */ public static void show(FragmentManager manager, Tile tile, int sourceMetricCategory, OnShowListener onShowListener, OnDismissListener onDismissListener, @@ -77,32 +87,53 @@ public class ProfileSelectDialog extends DialogFragment implements OnClickListen @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mSelectedTile = getArguments().getParcelable(ARG_SELECTED_TILE); - mSourceMetricCategory = getArguments().getInt(ARG_SOURCE_METRIC_CATEGORY); + Bundle arguments = requireArguments(); + mSelectedTile = arguments.getParcelable(ARG_SELECTED_TILE, Tile.class); + mSourceMetricCategory = arguments.getInt(ARG_SOURCE_METRIC_CATEGORY); } + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - final Context context = getActivity(); - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - final UserAdapter adapter = UserAdapter.createUserAdapter(UserManager.get(context), context, - mSelectedTile.userHandle); - builder.setTitle(com.android.settingslib.R.string.choose_profile) - .setAdapter(adapter, this); + return createDialog(getContext(), mSelectedTile.userHandle, this); + } - return builder.create(); + /** + * Creates the profile select dialog. + */ + public static Dialog createDialog(Context context, List userProfiles, + UserAdapter.OnClickListener onClickListener) { + LayoutInflater layoutInflater = context.getSystemService(LayoutInflater.class); + + DialogTitle titleView = + (DialogTitle) layoutInflater.inflate(R.layout.user_select_title, null); + titleView.setText(com.android.settingslib.R.string.choose_profile); + + View contentView = layoutInflater.inflate(R.layout.user_select, null); + + RecyclerView listView = contentView.findViewById(R.id.list); + listView.setAdapter( + UserAdapter.createUserRecycleViewAdapter(context, userProfiles, onClickListener)); + listView.setLayoutManager( + new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)); + + return new AlertDialog.Builder(context) + .setCustomTitle(titleView) + .setView(contentView) + .create(); } @Override - public void onClick(DialogInterface dialog, int which) { - final UserHandle user = mSelectedTile.userHandle.get(which); + public void onClick(int position) { + final UserHandle user = mSelectedTile.userHandle.get(position); // Show menu on top level items. final Intent intent = new Intent(mSelectedTile.getIntent()); FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider() .logStartedIntentWithProfile(intent, mSourceMetricCategory, - which == 1 /* isWorkProfile */); + position == 1 /* isWorkProfile */); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); getActivity().startActivityAsUser(intent, user); + dismiss(); } @Override diff --git a/src/com/android/settings/dashboard/profileselector/UserAdapter.java b/src/com/android/settings/dashboard/profileselector/UserAdapter.java index ae5351e1390..2573d11ce19 100644 --- a/src/com/android/settings/dashboard/profileselector/UserAdapter.java +++ b/src/com/android/settings/dashboard/profileselector/UserAdapter.java @@ -18,188 +18,156 @@ package com.android.settings.dashboard.profileselector; import static android.app.admin.DevicePolicyResources.Strings.Settings.PERSONAL_CATEGORY_HEADER; import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_CATEGORY_HEADER; -import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_USER_LABEL; import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyResourcesManager; import android.content.Context; import android.content.pm.UserInfo; -import android.database.DataSetObserver; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.UserHandle; import android.os.UserManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.BaseAdapter; import android.widget.ImageView; -import android.widget.ListAdapter; -import android.widget.SpinnerAdapter; import android.widget.TextView; import com.android.internal.util.UserIcons; +import com.android.internal.widget.RecyclerView; import com.android.settingslib.R; -import com.android.settingslib.drawable.UserIconDrawable; +import com.android.settingslib.Utils; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * Adapter for a spinner that shows a list of users. */ -public class UserAdapter implements SpinnerAdapter, ListAdapter { +public class UserAdapter extends BaseAdapter { + /** Holder for user details */ public static class UserDetails { private final UserHandle mUserHandle; - private final String mName; private final Drawable mIcon; + private final String mTitle; public UserDetails(UserHandle userHandle, UserManager um, Context context) { mUserHandle = userHandle; UserInfo userInfo = um.getUserInfo(mUserHandle.getIdentifier()); - Drawable icon; + int tintColor = Utils.getColorAttrDefaultColor(context, + com.android.internal.R.attr.colorAccentPrimaryVariant); if (userInfo.isManagedProfile()) { - mName = context.getSystemService(DevicePolicyManager.class).getResources() - .getString(WORK_PROFILE_USER_LABEL, - () -> context.getString(R.string.managed_user_title)); - icon = context.getPackageManager().getUserBadgeForDensityNoBackground( + mIcon = context.getPackageManager().getUserBadgeForDensityNoBackground( userHandle, /* density= */ 0); + mIcon.setTint(tintColor); } else { - mName = userInfo.name; - final int userId = userInfo.id; - if (um.getUserIcon(userId) != null) { - icon = new BitmapDrawable(context.getResources(), um.getUserIcon(userId)); - } else { - icon = UserIcons.getDefaultUserIcon( - context.getResources(), userId, /* light= */ false); - } + mIcon = UserIcons.getDefaultUserIconInColor(context.getResources(), tintColor); } - this.mIcon = encircle(context, icon); + mTitle = getTitle(context); } - private static Drawable encircle(Context context, Drawable icon) { - return new UserIconDrawable(UserIconDrawable.getDefaultSize(context)) - .setIconDrawable(icon).bake(); + private String getTitle(Context context) { + DevicePolicyManager devicePolicyManager = + Objects.requireNonNull(context.getSystemService(DevicePolicyManager.class)); + DevicePolicyResourcesManager resources = devicePolicyManager.getResources(); + int userHandle = mUserHandle.getIdentifier(); + if (userHandle == UserHandle.USER_CURRENT + || userHandle == ActivityManager.getCurrentUser()) { + return resources.getString(PERSONAL_CATEGORY_HEADER, + () -> context.getString(R.string.category_personal)); + } else { + return resources.getString(WORK_CATEGORY_HEADER, + () -> context.getString(R.string.category_work)); + } } } - private ArrayList data; - private final Context mContext; + private final ArrayList mUserDetails; private final LayoutInflater mInflater; - private final DevicePolicyManager mDevicePolicyManager; public UserAdapter(Context context, ArrayList users) { if (users == null) { throw new IllegalArgumentException("A list of user details must be provided"); } - mContext = context; - this.data = users; - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class); + mUserDetails = users; + mInflater = context.getSystemService(LayoutInflater.class); } public UserHandle getUserHandle(int position) { - if (position < 0 || position >= data.size()) { + if (position < 0 || position >= mUserDetails.size()) { return null; } - return data.get(position).mUserHandle; - } - - @Override - public View getDropDownView(int position, View convertView, ViewGroup parent) { - final View row = convertView != null ? convertView : createUser(parent); - - UserDetails user = data.get(position); - ((ImageView) row.findViewById(android.R.id.icon)).setImageDrawable(user.mIcon); - ((TextView) row.findViewById(android.R.id.title)).setText(getTitle(user)); - return row; - } - - private String getTitle(UserDetails user) { - int userHandle = user.mUserHandle.getIdentifier(); - if (userHandle == UserHandle.USER_CURRENT - || userHandle == ActivityManager.getCurrentUser()) { - return mDevicePolicyManager.getResources().getString(PERSONAL_CATEGORY_HEADER, - () -> mContext.getString(R.string.category_personal)); - } else { - return mDevicePolicyManager.getResources().getString(WORK_CATEGORY_HEADER, - () -> mContext.getString(R.string.category_work)); - } - } - - private View createUser(ViewGroup parent) { - return mInflater.inflate(R.layout.user_preference, parent, false); - } - - @Override - public void registerDataSetObserver(DataSetObserver observer) { - // We don't support observers - } - - @Override - public void unregisterDataSetObserver(DataSetObserver observer) { - // We don't support observers - } - - @Override - public int getCount() { - return data.size(); - } - - @Override - public UserAdapter.UserDetails getItem(int position) { - return data.get(position); - } - - @Override - public long getItemId(int position) { - return data.get(position).mUserHandle.getIdentifier(); - } - - @Override - public boolean hasStableIds() { - return false; + return mUserDetails.get(position).mUserHandle; } @Override public View getView(int position, View convertView, ViewGroup parent) { - return getDropDownView(position, convertView, parent); + ViewHolder holder; + if (convertView != null) { + holder = (ViewHolder) convertView.getTag(); + } else { + convertView = mInflater.inflate(R.layout.user_preference, parent, false); + holder = new ViewHolder(convertView); + convertView.setTag(holder); + } + bindViewHolder(holder, position); + return convertView; + } + + private void bindViewHolder(ViewHolder holder, int position) { + UserDetails userDetails = getItem(position); + holder.getIconView().setImageDrawable(userDetails.mIcon); + holder.getTitleView().setText(userDetails.mTitle); } @Override - public int getItemViewType(int position) { - return 0; + public int getCount() { + return mUserDetails.size(); } @Override - public int getViewTypeCount() { - return 1; + public UserAdapter.UserDetails getItem(int position) { + return mUserDetails.get(position); } @Override - public boolean isEmpty() { - return data.isEmpty(); + public long getItemId(int position) { + return mUserDetails.get(position).mUserHandle.getIdentifier(); } - @Override - public boolean areAllItemsEnabled() { - return true; - } + private RecyclerView.Adapter createRecyclerViewAdapter( + OnClickListener onClickListener) { + return new RecyclerView.Adapter() { + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.user_select_item, parent, false); - @Override - public boolean isEnabled(int position) { - return true; + return new ViewHolder(view, onClickListener); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + UserAdapter.this.bindViewHolder(holder, position); + } + + @Override + public int getItemCount() { + return getCount(); + } + }; } /** - * Creates a {@link UserAdapter} if there is more than one - * profile on the device. + * Creates a {@link UserAdapter} if there is more than one profile on the device. * - *

The adapter can be used to populate a spinner that switches between the Settings - * app on the different profiles. + *

The adapter can be used to populate a spinner that switches between the different + * profiles. * - * @return a {@link UserAdapter} or null if there is only one - * profile. + * @return a {@link UserAdapter} or null if there is only one profile. */ public static UserAdapter createUserSpinnerAdapter(UserManager userManager, Context context) { List userProfiles = userManager.getUserProfiles(); @@ -215,13 +183,60 @@ public class UserAdapter implements SpinnerAdapter, ListAdapter { return createUserAdapter(userManager, context, userProfiles); } - public static UserAdapter createUserAdapter( + /** + * Creates a {@link RecyclerView} adapter which be used to populate a {@link RecyclerView} that + * select one of the different profiles. + */ + public static RecyclerView.Adapter createUserRecycleViewAdapter( + Context context, List userProfiles, OnClickListener onClickListener) { + UserManager systemService = context.getSystemService(UserManager.class); + return createUserAdapter(systemService, context, userProfiles) + .createRecyclerViewAdapter(onClickListener); + } + + private static UserAdapter createUserAdapter( UserManager userManager, Context context, List userProfiles) { ArrayList userDetails = new ArrayList<>(userProfiles.size()); - final int count = userProfiles.size(); - for (int i = 0; i < count; i++) { - userDetails.add(new UserDetails(userProfiles.get(i), userManager, context)); + for (UserHandle userProfile : userProfiles) { + userDetails.add(new UserDetails(userProfile, userManager, context)); } return new UserAdapter(context, userDetails); } + + static class ViewHolder extends RecyclerView.ViewHolder { + private final ImageView mIconView; + private final TextView mTitleView; + + private ViewHolder(View view) { + super(view); + mIconView = view.findViewById(android.R.id.icon); + mTitleView = view.findViewById(android.R.id.title); + } + + private ViewHolder(View view, OnClickListener onClickListener) { + this(view); + View button = view.findViewById(R.id.button); + if (button != null) { + button.setOnClickListener(v -> onClickListener.onClick(getAdapterPosition())); + } + } + + private ImageView getIconView() { + return mIconView; + } + + private TextView getTitleView() { + return mTitleView; + } + } + + /** + * Interface definition for a callback to be invoked when a user is clicked. + */ + public interface OnClickListener { + /** + * Called when a user has been clicked. + */ + void onClick(int position); + } } diff --git a/src/com/android/settings/privacy/EnableContentCaptureWithServiceSettingsPreferenceController.java b/src/com/android/settings/privacy/EnableContentCaptureWithServiceSettingsPreferenceController.java index c3878d51b62..fcb2347f5b0 100644 --- a/src/com/android/settings/privacy/EnableContentCaptureWithServiceSettingsPreferenceController.java +++ b/src/com/android/settings/privacy/EnableContentCaptureWithServiceSettingsPreferenceController.java @@ -19,19 +19,18 @@ package com.android.settings.privacy; import android.annotation.NonNull; import android.content.ComponentName; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.pm.UserInfo; import android.os.UserHandle; import android.os.UserManager; +import android.text.TextUtils; import android.util.Log; -import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; -import com.android.settings.dashboard.profileselector.UserAdapter; +import com.android.settings.dashboard.profileselector.ProfileSelectDialog; import com.android.settings.utils.ContentCaptureUtils; import java.util.ArrayList; @@ -42,13 +41,9 @@ public final class EnableContentCaptureWithServiceSettingsPreferenceController private static final String TAG = "ContentCaptureController"; - private final UserManager mUserManager; - public EnableContentCaptureWithServiceSettingsPreferenceController(@NonNull Context context, @NonNull String key) { super(context, key); - - mUserManager = UserManager.get(context); } @Override @@ -74,11 +69,6 @@ public final class EnableContentCaptureWithServiceSettingsPreferenceController Log.w(TAG, "No component name for custom service settings"); preference.setSelectable(false); } - - preference.setOnPreferenceClickListener((pref) -> { - ProfileSelectDialog.show(mContext, pref); - return true; - }); } @Override @@ -93,32 +83,30 @@ public final class EnableContentCaptureWithServiceSettingsPreferenceController return R.string.menu_key_privacy; } - private static final class ProfileSelectDialog { - public static void show(Context context, Preference pref) { - final UserManager userManager = UserManager.get(context); - final List userInfos = userManager.getUsers(); - final ArrayList userHandles = new ArrayList<>(userInfos.size()); - for (UserInfo info: userInfos) { - userHandles.add(info.getUserHandle()); - } - if (userHandles.size() == 1) { - final Intent intent = pref.getIntent().addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - context.startActivityAsUser(intent, userHandles.get(0)); - } else { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - UserAdapter adapter = UserAdapter.createUserAdapter(userManager, context, - userHandles); - builder.setTitle(com.android.settingslib.R.string.choose_profile) - .setAdapter(adapter, (DialogInterface dialog, int which) -> { - final UserHandle user = userHandles.get(which); - // Show menu on top level items. - final Intent intent = pref.getIntent() - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - context.startActivityAsUser(intent, user); - }) - .show(); - } + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { + return false; } + show(preference); + return true; } + private void show(Preference preference) { + final UserManager userManager = UserManager.get(mContext); + final List userInfos = userManager.getUsers(); + final ArrayList userHandles = new ArrayList<>(userInfos.size()); + for (UserInfo info : userInfos) { + userHandles.add(info.getUserHandle()); + } + final Intent intent = preference.getIntent().addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + if (userHandles.size() == 1) { + mContext.startActivityAsUser(intent, userHandles.get(0)); + return; + } + ProfileSelectDialog.createDialog(mContext, userHandles, (int position) -> { + // Show menu on top level items. + mContext.startActivityAsUser(intent, userHandles.get(position)); + }).show(); + } } diff --git a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java index 53cddb1b056..e1cf52b995b 100644 --- a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java +++ b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java @@ -23,21 +23,29 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.Dialog; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.UserInfo; import android.os.UserHandle; import android.os.UserManager; +import android.widget.TextView; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; import com.android.settingslib.drawer.ActivityTile; import com.android.settingslib.drawer.CategoryKey; import com.android.settingslib.drawer.Tile; +import com.google.android.collect.Lists; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) @@ -46,8 +54,9 @@ public class ProfileSelectDialogTest { private static final UserHandle NORMAL_USER = new UserHandle(1111); private static final UserHandle REMOVED_USER = new UserHandle(2222); - @Mock - private Context mContext; + @Spy + private Context mContext = ApplicationProvider.getApplicationContext(); + @Mock private UserManager mUserManager; @@ -91,4 +100,18 @@ public class ProfileSelectDialogTest { verify(mUserManager, times(1)).getUserInfo(NORMAL_USER.getIdentifier()); verify(mUserManager, times(2)).getUserInfo(REMOVED_USER.getIdentifier()); } + + @Test + public void createDialog_showsCorrectTitle() { + mContext.setTheme(R.style.Theme_AppCompat); + + Dialog dialog = ProfileSelectDialog.createDialog(mContext, Lists.newArrayList(NORMAL_USER), + (position) -> { + }); + dialog.show(); + + TextView titleView = dialog.findViewById(R.id.topPanel).findViewById(android.R.id.title); + assertThat(titleView.getText().toString()).isEqualTo( + mContext.getText(com.android.settingslib.R.string.choose_profile).toString()); + } } diff --git a/tests/robotests/src/com/android/settings/dashboard/profileselector/UserAdapterTest.java b/tests/robotests/src/com/android/settings/dashboard/profileselector/UserAdapterTest.java new file mode 100644 index 00000000000..aa7e30af809 --- /dev/null +++ b/tests/robotests/src/com/android/settings/dashboard/profileselector/UserAdapterTest.java @@ -0,0 +1,122 @@ +/* + * 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.dashboard.profileselector; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.UserInfo; +import android.os.UserHandle; +import android.os.UserManager; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.internal.widget.RecyclerView; +import com.android.settingslib.R; + +import com.google.android.collect.Lists; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +import java.util.ArrayList; + +@RunWith(RobolectricTestRunner.class) +public class UserAdapterTest { + @Rule + public MockitoRule mRule = MockitoJUnit.rule(); + + private final int mPersonalUserId = UserHandle.myUserId(); + private static final int WORK_USER_ID = 1; + + @Mock + private UserManager mUserManager; + + @Mock + private UserInfo mPersonalUserInfo; + + @Mock + private UserInfo mWorkUserInfo; + + @Mock + private UserAdapter.OnClickListener mOnClickListener; + + @Spy + private Context mContext = ApplicationProvider.getApplicationContext(); + + @Before + public void setUp() { + when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); + when(mUserManager.getUserInfo(mPersonalUserId)).thenReturn(mPersonalUserInfo); + when(mUserManager.getUserInfo(WORK_USER_ID)).thenReturn(mWorkUserInfo); + } + + @Test + public void createUserSpinnerAdapter_singleProfile_returnsNull() { + when(mUserManager.getUserProfiles()).thenReturn( + Lists.newArrayList(UserHandle.of(mPersonalUserId))); + + UserAdapter userSpinnerAdapter = + UserAdapter.createUserSpinnerAdapter(mUserManager, mContext); + + assertThat(userSpinnerAdapter).isNull(); + } + + @Test + public void createUserSpinnerAdapter_twoProfiles_succeed() { + when(mUserManager.getUserProfiles()).thenReturn( + Lists.newArrayList(UserHandle.of(mPersonalUserId), UserHandle.of(WORK_USER_ID))); + + UserAdapter userSpinnerAdapter = + UserAdapter.createUserSpinnerAdapter(mUserManager, mContext); + + assertThat(userSpinnerAdapter.getCount()).isEqualTo(2); + assertThat(userSpinnerAdapter.getUserHandle(0).getIdentifier()).isEqualTo(mPersonalUserId); + assertThat(userSpinnerAdapter.getUserHandle(1).getIdentifier()).isEqualTo(WORK_USER_ID); + } + + @Test + public void createUserRecycleViewAdapter_canBindViewHolderCorrectly() { + ArrayList userHandles = + Lists.newArrayList(UserHandle.of(mPersonalUserId), UserHandle.of(WORK_USER_ID)); + FrameLayout parent = new FrameLayout(mContext); + + RecyclerView.Adapter adapter = + UserAdapter.createUserRecycleViewAdapter(mContext, userHandles, mOnClickListener); + UserAdapter.ViewHolder holder = adapter.createViewHolder(parent, 0); + adapter.bindViewHolder(holder, 0); + holder.itemView.findViewById(R.id.button).performClick(); + + assertThat(adapter.getItemCount()).isEqualTo(2); + TextView textView = holder.itemView.findViewById(android.R.id.title); + assertThat(textView.getText().toString()).isEqualTo("Personal"); + verify(mOnClickListener).onClick(anyInt()); + } +} From e5fff060a28a98d77ef68ef19b8b9fab4473b2cc Mon Sep 17 00:00:00 2001 From: SongFerngWang Date: Mon, 25 Apr 2022 21:07:26 +0800 Subject: [PATCH 8/8] [MEP] show the DSDS mode selection dialog for user when user insert esim If device has esim, then user inserts esim in SS mode. Then, UI should show the DSDS mode selection dialog for user. Bug: 230050990 Test: build pass and manual test. Change-Id: Ia4e695ae2c4e86d6039e03738378989a0879cc71 --- .../ToggleSubscriptionDialogActivity.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/com/android/settings/network/telephony/ToggleSubscriptionDialogActivity.java b/src/com/android/settings/network/telephony/ToggleSubscriptionDialogActivity.java index 2616a69ba1d..d348b24e58f 100644 --- a/src/com/android/settings/network/telephony/ToggleSubscriptionDialogActivity.java +++ b/src/com/android/settings/network/telephony/ToggleSubscriptionDialogActivity.java @@ -326,7 +326,7 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc /* Handles the enabling SIM action. */ private void showEnableSubDialog() { - Log.i(TAG, "Handle subscription enabling."); + Log.d(TAG, "Handle subscription enabling."); if (isDsdsConditionSatisfied()) { showEnableDsdsConfirmDialog(); return; @@ -452,7 +452,7 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc } private void showMepSwitchSimConfirmDialog() { - Log.i(TAG, "showMepSwitchSimConfirmDialog"); + Log.d(TAG, "showMepSwitchSimConfirmDialog"); final CharSequence displayName = SubscriptionUtil.getUniqueSubscriptionDisplayName( mSubInfo, this); String title = getString(R.string.sim_action_switch_sub_dialog_mep_title, displayName); @@ -556,27 +556,35 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc private boolean isDsdsConditionSatisfied() { if (mTelMgr.isMultiSimEnabled()) { - Log.i(TAG, "DSDS is already enabled. Condition not satisfied."); + Log.d(TAG, "DSDS is already enabled. Condition not satisfied."); return false; } if (mTelMgr.isMultiSimSupported() != TelephonyManager.MULTISIM_ALLOWED) { - Log.i(TAG, "Hardware does not support DSDS."); + Log.d(TAG, "Hardware does not support DSDS."); return false; } + boolean isActiveSim = SubscriptionUtil.getActiveSubscriptions( + mSubscriptionManager).size() > 0; + if (isMultipleEnabledProfilesSupported() && isActiveSim) { + Log.d(TAG, + "Device supports MEP and eSIM operation and eSIM profile is enabled." + + " DSDS condition satisfied."); + return true; + } boolean isRemovableSimEnabled = isRemovableSimEnabled(); if (mIsEsimOperation && isRemovableSimEnabled) { - Log.i(TAG, "eSIM operation and removable SIM is enabled. DSDS condition satisfied."); + Log.d(TAG, "eSIM operation and removable SIM is enabled. DSDS condition satisfied."); return true; } boolean isEsimProfileEnabled = SubscriptionUtil.getActiveSubscriptions(mSubscriptionManager).stream() .anyMatch(SubscriptionInfo::isEmbedded); if (!mIsEsimOperation && isEsimProfileEnabled) { - Log.i(TAG, "Removable SIM operation and eSIM profile is enabled. DSDS condition" + Log.d(TAG, "Removable SIM operation and eSIM profile is enabled. DSDS condition" + " satisfied."); return true; } - Log.i(TAG, "DSDS condition not satisfied."); + Log.d(TAG, "DSDS condition not satisfied."); return false; }