From 0569121109cd129b38ba36169bc4d6f253c51a4b Mon Sep 17 00:00:00 2001 From: James Willcox Date: Tue, 11 Jun 2024 20:53:21 +0000 Subject: [PATCH 01/23] Add @RequiresPermission to ACTION_PREPARE_FACTORY_RESET Also make it public. This should allow it to be detected appropriately by the GMSCore permission registry machinery. Bug; 346611867 Test: builds Change-Id: I15aa99541f33124ab6aea284889351d86753fa56 --- .../settings/system/FactoryResetPreferenceController.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/system/FactoryResetPreferenceController.java b/src/com/android/settings/system/FactoryResetPreferenceController.java index df7cc3df3a2..54c97a389b8 100644 --- a/src/com/android/settings/system/FactoryResetPreferenceController.java +++ b/src/com/android/settings/system/FactoryResetPreferenceController.java @@ -16,6 +16,7 @@ package com.android.settings.system; import android.Manifest; +import android.annotation.RequiresPermission; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; @@ -37,8 +38,8 @@ public class FactoryResetPreferenceController extends BasePreferenceController { private static final String TAG = "FactoryResetPreference"; - @VisibleForTesting - static final String ACTION_PREPARE_FACTORY_RESET = + @RequiresPermission(Manifest.permission.PREPARE_FACTORY_RESET) + public static final String ACTION_PREPARE_FACTORY_RESET = "com.android.settings.ACTION_PREPARE_FACTORY_RESET"; private final UserManager mUm; From 40ec147d2b9fa046f4d10cbf199c1e3a12010cb8 Mon Sep 17 00:00:00 2001 From: Joshua McCloskey Date: Mon, 17 Jun 2024 17:48:03 +0000 Subject: [PATCH 02/23] Opt out of predictive back for biometrics Test: Verified that the screens are skipped if they are finished in activity#onCreate Fixes: 338348564 Change-Id: I5ea7fb865c49108705af877edcd09f9739b1c7a5 --- AndroidManifest.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f629a365099..5416aab1ecb 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2842,17 +2842,20 @@ @@ -2862,16 +2865,19 @@ @@ -2917,24 +2923,29 @@ Date: Mon, 25 Mar 2024 23:49:35 +0000 Subject: [PATCH 03/23] RESTRICT AUTOMERGE Restrict Settings Homepage prior to provisioning Bug: 327749022 Test: manual test 1. factory reset + launch Settings via ADB during Setup -> verify app closes 2. factory reset + bypass Setup + tap Settings icon in launcher -> verify app closes (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:70a5a0fd353cc6203d2926627de93786155ae5bc) Merged-In: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 Change-Id: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 --- .../settings/homepage/SettingsHomepageActivity.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 829a89c6f03..c399c6f3a9a 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -175,6 +175,16 @@ public class SettingsHomepageActivity extends FragmentActivity implements protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Ensure device is provisioned in order to access Settings home + // TODO(b/331254029): This should later be replaced in favor of an allowlist + boolean unprovisioned = android.provider.Settings.Global.getInt(getContentResolver(), + android.provider.Settings.Global.DEVICE_PROVISIONED, 0) == 0; + if (unprovisioned) { + Log.e(TAG, "Device is not provisioned, exiting Settings"); + finish(); + return; + } + mIsEmbeddingActivityEnabled = ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this); if (mIsEmbeddingActivityEnabled) { final UserManager um = getSystemService(UserManager.class); From 7f1c4df02d153cb380a6147e86194bec2a564ab7 Mon Sep 17 00:00:00 2001 From: Chris Antol Date: Mon, 25 Mar 2024 23:49:35 +0000 Subject: [PATCH 04/23] RESTRICT AUTOMERGE Restrict Settings Homepage prior to provisioning Bug: 327749022 Test: manual test 1. factory reset + launch Settings via ADB during Setup -> verify app closes 2. factory reset + bypass Setup + tap Settings icon in launcher -> verify app closes (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:70a5a0fd353cc6203d2926627de93786155ae5bc) Merged-In: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 Change-Id: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 --- .../settings/homepage/SettingsHomepageActivity.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 1d7b5dc14b9..f1c46dff44a 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -74,6 +74,17 @@ public class SettingsHomepageActivity extends FragmentActivity implements @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + // Ensure device is provisioned in order to access Settings home + // TODO(b/331254029): This should later be replaced in favor of an allowlist + boolean unprovisioned = android.provider.Settings.Global.getInt(getContentResolver(), + android.provider.Settings.Global.DEVICE_PROVISIONED, 0) == 0; + if (unprovisioned) { + Log.e(TAG, "Device is not provisioned, exiting Settings"); + finish(); + return; + } + setContentView(R.layout.settings_homepage_container); final View appBar = findViewById(R.id.app_bar_container); From d83f47397e61d5ec04866af20efcb935a58cbdff Mon Sep 17 00:00:00 2001 From: Chris Antol Date: Mon, 25 Mar 2024 23:49:35 +0000 Subject: [PATCH 05/23] RESTRICT AUTOMERGE Restrict Settings Homepage prior to provisioning Bug: 327749022 Test: manual test 1. factory reset + launch Settings via ADB during Setup -> verify app closes 2. factory reset + bypass Setup + tap Settings icon in launcher -> verify app closes (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:70a5a0fd353cc6203d2926627de93786155ae5bc) (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:477d4a8d6ba390ed0f9b150ca271966cd967820a) Merged-In: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 Change-Id: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 --- .../settings/homepage/SettingsHomepageActivity.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index b3f84d68e00..3d006132460 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -174,6 +174,16 @@ public class SettingsHomepageActivity extends FragmentActivity implements protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Ensure device is provisioned in order to access Settings home + // TODO(b/331254029): This should later be replaced in favor of an allowlist + boolean unprovisioned = android.provider.Settings.Global.getInt(getContentResolver(), + android.provider.Settings.Global.DEVICE_PROVISIONED, 0) == 0; + if (unprovisioned) { + Log.e(TAG, "Device is not provisioned, exiting Settings"); + finish(); + return; + } + mIsEmbeddingActivityEnabled = ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this); if (mIsEmbeddingActivityEnabled) { final UserManager um = getSystemService(UserManager.class); From b66997fc4e2bd8bf1318864d9a85d273748f24ca Mon Sep 17 00:00:00 2001 From: Chris Antol Date: Mon, 25 Mar 2024 23:49:35 +0000 Subject: [PATCH 06/23] RESTRICT AUTOMERGE Restrict Settings Homepage prior to provisioning Bug: 327749022 Test: manual test 1. factory reset + launch Settings via ADB during Setup -> verify app closes 2. factory reset + bypass Setup + tap Settings icon in launcher -> verify app closes (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:70a5a0fd353cc6203d2926627de93786155ae5bc) (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:477d4a8d6ba390ed0f9b150ca271966cd967820a) Merged-In: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 Change-Id: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 --- .../settings/homepage/SettingsHomepageActivity.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 1d7b5dc14b9..f1c46dff44a 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -74,6 +74,17 @@ public class SettingsHomepageActivity extends FragmentActivity implements @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + // Ensure device is provisioned in order to access Settings home + // TODO(b/331254029): This should later be replaced in favor of an allowlist + boolean unprovisioned = android.provider.Settings.Global.getInt(getContentResolver(), + android.provider.Settings.Global.DEVICE_PROVISIONED, 0) == 0; + if (unprovisioned) { + Log.e(TAG, "Device is not provisioned, exiting Settings"); + finish(); + return; + } + setContentView(R.layout.settings_homepage_container); final View appBar = findViewById(R.id.app_bar_container); From 2f0db305a6bb41d04ccab2ecd31a56bbff8e85fe Mon Sep 17 00:00:00 2001 From: Chris Antol Date: Mon, 25 Mar 2024 23:49:35 +0000 Subject: [PATCH 07/23] RESTRICT AUTOMERGE Restrict Settings Homepage prior to provisioning Bug: 327749022 Test: manual test 1. factory reset + launch Settings via ADB during Setup -> verify app closes 2. factory reset + bypass Setup + tap Settings icon in launcher -> verify app closes (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:70a5a0fd353cc6203d2926627de93786155ae5bc) (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:477d4a8d6ba390ed0f9b150ca271966cd967820a) Merged-In: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 Change-Id: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 --- .../settings/homepage/SettingsHomepageActivity.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 0311ea3fbe9..81556ef81f0 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -154,6 +154,17 @@ public class SettingsHomepageActivity extends FragmentActivity implements @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + // Ensure device is provisioned in order to access Settings home + // TODO(b/331254029): This should later be replaced in favor of an allowlist + boolean unprovisioned = android.provider.Settings.Global.getInt(getContentResolver(), + android.provider.Settings.Global.DEVICE_PROVISIONED, 0) == 0; + if (unprovisioned) { + Log.e(TAG, "Device is not provisioned, exiting Settings"); + finish(); + return; + } + setContentView(R.layout.settings_homepage_container); mIsEmbeddingActivityEnabled = ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this); mIsTwoPaneLastTime = ActivityEmbeddingUtils.isTwoPaneResolution(this); From 87ce283623ba8c408290ecf2bd7bbcbb8089f5d7 Mon Sep 17 00:00:00 2001 From: Chris Antol Date: Mon, 25 Mar 2024 23:49:35 +0000 Subject: [PATCH 08/23] RESTRICT AUTOMERGE Restrict Settings Homepage prior to provisioning Bug: 327749022 Test: manual test 1. factory reset + launch Settings via ADB during Setup -> verify app closes 2. factory reset + bypass Setup + tap Settings icon in launcher -> verify app closes (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:70a5a0fd353cc6203d2926627de93786155ae5bc) (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:c02f4ee1936ec5aea5231ebe5afcef27f4bd751c) Merged-In: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 Change-Id: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 --- .../settings/homepage/SettingsHomepageActivity.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 829a89c6f03..c399c6f3a9a 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -175,6 +175,16 @@ public class SettingsHomepageActivity extends FragmentActivity implements protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Ensure device is provisioned in order to access Settings home + // TODO(b/331254029): This should later be replaced in favor of an allowlist + boolean unprovisioned = android.provider.Settings.Global.getInt(getContentResolver(), + android.provider.Settings.Global.DEVICE_PROVISIONED, 0) == 0; + if (unprovisioned) { + Log.e(TAG, "Device is not provisioned, exiting Settings"); + finish(); + return; + } + mIsEmbeddingActivityEnabled = ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this); if (mIsEmbeddingActivityEnabled) { final UserManager um = getSystemService(UserManager.class); From f8dad38722f60b92ca3748950cb828d60afddb8d Mon Sep 17 00:00:00 2001 From: Chris Antol Date: Mon, 25 Mar 2024 23:49:35 +0000 Subject: [PATCH 09/23] RESTRICT AUTOMERGE Restrict Settings Homepage prior to provisioning Bug: 327749022 Test: manual test 1. factory reset + launch Settings via ADB during Setup -> verify app closes 2. factory reset + bypass Setup + tap Settings icon in launcher -> verify app closes (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:70a5a0fd353cc6203d2926627de93786155ae5bc) (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:477d4a8d6ba390ed0f9b150ca271966cd967820a) (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:d83f47397e61d5ec04866af20efcb935a58cbdff) Merged-In: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 Change-Id: I8cbe38109ebf88a0f68f3917e95468a81c6463c1 --- .../settings/homepage/SettingsHomepageActivity.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 60252fa8b95..98c743a6c75 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -175,6 +175,16 @@ public class SettingsHomepageActivity extends FragmentActivity implements protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Ensure device is provisioned in order to access Settings home + // TODO(b/331254029): This should later be replaced in favor of an allowlist + boolean unprovisioned = android.provider.Settings.Global.getInt(getContentResolver(), + android.provider.Settings.Global.DEVICE_PROVISIONED, 0) == 0; + if (unprovisioned) { + Log.e(TAG, "Device is not provisioned, exiting Settings"); + finish(); + return; + } + mIsEmbeddingActivityEnabled = ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this); if (mIsEmbeddingActivityEnabled) { final UserManager um = getSystemService(UserManager.class); From ad93026e3b0926be0fba08fb3c60ee1c601141e8 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Mon, 17 Jun 2024 11:48:53 +0800 Subject: [PATCH 10/23] Fix Apn network type tests Bug: 347114536 Test: unit test Change-Id: Ib5bd331e9a28e89e426bf423e6086d1c2d288961 Merged-In: Ib5bd331e9a28e89e426bf423e6086d1c2d288961 (cherry picked from commit 288b40ee4f50498ff320f927870410147922551a) --- .../network/apn/ApnEditPageProviderTest.kt | 48 ----------- .../network/apn/ApnNetworkTypeCheckBoxTest.kt | 85 +++++++++++++++++++ 2 files changed, 85 insertions(+), 48 deletions(-) create mode 100644 tests/spa_unit/src/com/android/settings/network/apn/ApnNetworkTypeCheckBoxTest.kt diff --git a/tests/spa_unit/src/com/android/settings/network/apn/ApnEditPageProviderTest.kt b/tests/spa_unit/src/com/android/settings/network/apn/ApnEditPageProviderTest.kt index 4dbe050c4d8..3621948c9fb 100644 --- a/tests/spa_unit/src/com/android/settings/network/apn/ApnEditPageProviderTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/apn/ApnEditPageProviderTest.kt @@ -58,7 +58,6 @@ class ApnEditPageProviderTest { private val apnEnable = context.resources.getString(R.string.carrier_enabled) private val apnProtocolOptions = context.resources.getStringArray(R.array.apn_protocol_entries).toList() - private val networkType = context.resources.getString(R.string.network_type) private val passwordTitle = context.resources.getString(R.string.apn_password) private val apnInit = ApnData( name = apnName, @@ -167,48 +166,6 @@ class ApnEditPageProviderTest { composeTestRule.onNodeWithText(apnEnable, true).assertIsOff() } - @Test - fun network_type_displayed() { - composeTestRule.setContent { - ApnPage(apnInit, remember { apnData }, uri) - } - composeTestRule.onRoot().onChild().onChildAt(0) - .performScrollToNode(hasText(networkType, true)) - composeTestRule.onNodeWithText(networkType, true).assertIsDisplayed() - } - - @Test - fun network_type_changed() { - composeTestRule.setContent { - ApnPage(apnInit, remember { apnData }, uri) - } - composeTestRule.onRoot().onChild().onChildAt(0) - .performScrollToNode(hasText(networkType, true)) - composeTestRule.onNodeWithText(networkType, true).performClick() - composeTestRule.onNodeWithText(NETWORK_TYPE_LTE, true).performClick() - composeTestRule.onNode(hasText(NETWORK_TYPE_UNSPECIFIED) and isFocused(), true) - .assertDoesNotExist() - composeTestRule.onNode(hasText(NETWORK_TYPE_LTE) and isFocused(), true).assertIsDisplayed() - } - - @Test - fun network_type_changed_back2Default() { - composeTestRule.setContent { - ApnPage(apnInit, remember { apnData }, uri) - } - composeTestRule.onRoot().onChild().onChildAt(0) - .performScrollToNode(hasText(networkType, true)) - composeTestRule.onNodeWithText(networkType, true).performClick() - composeTestRule.onNodeWithText(NETWORK_TYPE_LTE, true).performClick() - composeTestRule.onNode(hasText(NETWORK_TYPE_UNSPECIFIED) and isFocused(), true) - .assertDoesNotExist() - composeTestRule.onNode(hasText(NETWORK_TYPE_LTE) and isFocused(), true).assertIsDisplayed() - composeTestRule.onAllNodesWithText(NETWORK_TYPE_LTE, true).onLast().performClick() - composeTestRule.onNode(hasText(NETWORK_TYPE_UNSPECIFIED) and isFocused(), true) - .assertIsDisplayed() - composeTestRule.onNode(hasText(NETWORK_TYPE_LTE) and isFocused(), true).assertDoesNotExist() - } - @Test fun password_displayed() { composeTestRule.setContent { @@ -218,9 +175,4 @@ class ApnEditPageProviderTest { .performScrollToNode(hasText(passwordTitle, true)) composeTestRule.onNodeWithText(passwordTitle, true).assertIsDisplayed() } - - private companion object { - const val NETWORK_TYPE_UNSPECIFIED = "Unspecified" - const val NETWORK_TYPE_LTE = "LTE" - } } \ No newline at end of file diff --git a/tests/spa_unit/src/com/android/settings/network/apn/ApnNetworkTypeCheckBoxTest.kt b/tests/spa_unit/src/com/android/settings/network/apn/ApnNetworkTypeCheckBoxTest.kt new file mode 100644 index 00000000000..5a8e40af352 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/apn/ApnNetworkTypeCheckBoxTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network.apn + +import android.content.Context +import android.telephony.TelephonyManager +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isToggleable +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settingslib.spa.testutils.hasRole +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ApnNetworkTypeCheckBoxTest { + @get:Rule val composeTestRule = createComposeRule() + + private val context: Context = ApplicationProvider.getApplicationContext() + + private val apnData = ApnData() + + @Test + fun networkType_displayed() { + composeTestRule.setContent { ApnNetworkTypeCheckBox(apnData) {} } + + composeTestRule.onNodeWithText(context.getString(R.string.network_type)).assertIsDisplayed() + } + + @Test + fun networkType_changed() { + composeTestRule.setContent { ApnNetworkTypeCheckBox(apnData) {} } + + composeTestRule.onNodeWithText(context.getString(R.string.network_type)).performClick() + composeTestRule.onNode(hasText(LTE_TEXT) and isToggleable()).performClick() + + composeTestRule + .onDropdownListWithText(context.getString(R.string.network_type_unspecified)) + .assertDoesNotExist() + composeTestRule.onDropdownListWithText(LTE_TEXT).assertIsDisplayed() + } + + @Test + fun networkType_changed_back2Default() { + composeTestRule.setContent { ApnNetworkTypeCheckBox(apnData) {} } + + composeTestRule.onNodeWithText(context.getString(R.string.network_type)).performClick() + composeTestRule.onNode(hasText(LTE_TEXT) and isToggleable()).performClick() + composeTestRule.onNode(hasText(LTE_TEXT) and isToggleable()).performClick() + + composeTestRule + .onDropdownListWithText(context.getString(R.string.network_type_unspecified)) + .assertIsDisplayed() + composeTestRule.onDropdownListWithText(LTE_TEXT).assertDoesNotExist() + } + + private fun ComposeTestRule.onDropdownListWithText(text: String) = + onNode(hasText(text) and hasRole(Role.DropdownList)) + + private companion object { + val LTE_TEXT = TelephonyManager.getNetworkTypeName(TelephonyManager.NETWORK_TYPE_LTE) + } +} From f60b42acad662b106953787ceeb24df3ab756c7a Mon Sep 17 00:00:00 2001 From: tomhsu Date: Mon, 24 Jun 2024 13:00:11 +0000 Subject: [PATCH 11/23] When received Carrier Config Change, refresh UI. Flag: EXEMPT bug fix Fix: 340890663 Test: Manual test Change-Id: Ia78c2dfb7e567161bcbb8d1d622cdccb178778e0 --- .../telephony/MobileNetworkSettings.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java index 34d2fbd5436..d70ef25dd3a 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java +++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java @@ -20,11 +20,14 @@ import static com.android.settings.network.MobileNetworkListFragment.collectAirp import android.app.Activity; import android.app.settings.SettingsEnums; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.os.Bundle; import android.os.UserManager; import android.provider.Settings; +import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; @@ -106,6 +109,15 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme private SubscriptionInfoEntity mSubscriptionInfoEntity; private MobileNetworkInfoEntity mMobileNetworkInfoEntity; + private BroadcastReceiver mBrocastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) { + redrawPreferenceControllers(); + } + } + }; + public MobileNetworkSettings() { super(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS); } @@ -351,6 +363,10 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme mMobileNetworkRepository.updateEntity(); // TODO: remove log after fixing b/182326102 Log.d(LOG_TAG, "onResume() subId=" + mSubId); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED); + getContext().registerReceiver(mBrocastReceiver, intentFilter, Context.RECEIVER_EXPORTED); } private void onSubscriptionDetailChanged() { @@ -370,6 +386,7 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme @Override public void onPause() { mMobileNetworkRepository.removeRegister(this); + getContext().unregisterReceiver(mBrocastReceiver); super.onPause(); } From eb7285170b083bc101f931149772a76d568abbc4 Mon Sep 17 00:00:00 2001 From: Yuri Lin Date: Tue, 18 Jun 2024 14:09:39 -0400 Subject: [PATCH 12/23] Add "delete mode" option on mode configuration page Flag: android.app.modes_ui Bug: 346575126 Test: manual Change-Id: I0d085eb54ef5fa07f312bdddf0219855bf1c2ef4 --- res/values/strings.xml | 8 +++- .../notification/modes/ZenModeFragment.java | 46 +++++++++++++++++++ .../modes/ZenModeFragmentBase.java | 13 ++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 6560e5446f1..a76c02424af 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7955,9 +7955,15 @@ Delete schedules - + Delete + + Delete mode + + + Delete \"%1$s\" mode? + Edit diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index f2f47b99ceb..a6f49b71ecf 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -16,10 +16,14 @@ package com.android.settings.notification.modes; +import android.app.AlertDialog; import android.app.Application; import android.app.AutomaticZenRule; import android.app.settings.SettingsEnums; import android.content.Context; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import com.android.settings.R; import com.android.settingslib.applications.ApplicationsState; @@ -30,6 +34,9 @@ import java.util.List; public class ZenModeFragment extends ZenModeFragmentBase { + // for mode deletion menu + private static final int DELETE_MODE = 1; + @Override protected int getPreferenceScreenResId() { return R.xml.modes_rule_settings; @@ -76,4 +83,43 @@ public class ZenModeFragment extends ZenModeFragmentBase { // TODO: b/332937635 - make this the correct metrics category return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION; } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + menu.add(Menu.NONE, DELETE_MODE, Menu.NONE, R.string.zen_mode_menu_delete_mode); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + protected boolean onOptionsItemSelected(MenuItem item, ZenMode zenMode) { + switch (item.getItemId()) { + case DELETE_MODE: + new AlertDialog.Builder(mContext) + .setTitle(mContext.getString(R.string.zen_mode_delete_mode_confirmation, + zenMode.getRule().getName())) + .setPositiveButton(R.string.zen_mode_schedule_delete, + (dialog, which) -> { + // start finishing before calling removeMode() so that we don't + // try to update this activity with a nonexistent mode when the + // zen mode config is updated + finish(); + mBackend.removeMode(zenMode); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void updateZenModeState() { + // Because this fragment may be asked to finish by the delete menu but not be done doing + // so yet, ignore any attempts to update info in that case. + if (getActivity() != null && getActivity().isFinishing()) { + return; + } + super.updateZenModeState(); + } } diff --git a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java index 67cc13beb4a..9842866e4fc 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java +++ b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java @@ -20,6 +20,7 @@ import android.app.AutomaticZenRule; import android.content.Context; import android.os.Bundle; import android.util.Log; +import android.view.MenuItem; import android.widget.Toast; import androidx.annotation.NonNull; @@ -108,6 +109,18 @@ abstract class ZenModeFragmentBase extends ZenModesFragmentBase { updateControllers(); } + @Override + public final boolean onOptionsItemSelected(MenuItem item) { + if (mZenMode != null) { + return onOptionsItemSelected(item, mZenMode); + } + return super.onOptionsItemSelected(item); + } + + protected boolean onOptionsItemSelected(MenuItem item, @NonNull ZenMode zenMode) { + return true; + } + private void updateControllers() { if (getPreferenceControllers() == null || mZenMode == null) { return; From caef61b5eb7c11077c2de042667b9bc89f7a3213 Mon Sep 17 00:00:00 2001 From: Tetiana Meronyk Date: Wed, 1 May 2024 13:15:34 +0000 Subject: [PATCH 13/23] Refactor multiuser toggle to control user switch feature The name "Allow multiple users" is too ambiguous. It sounds like by toggling it off, the feature is completely disabled. In fact, it only hides user switcher. In conjunction with hiding other users from the list, it makes it appear as all the users get deleted when the toggle is off. On the contrary, users might be running in background when the toggle is off. After this change, the new name better represents the intention behind this toggle, as well as makes the UI more intuitive. The users are not being hidden anymore. But switching preference gets disabled. Since the toggle can only be enabled or disabled by owner (after this refactoring), it means that Owner has full control over multiuser settings and is able to perform actions on users without having to enable the toggle. Bug: 336762423 Test: atest UserSettingsTest && atest UserDetailsSettingsTest Flag: android.multiuser.new_multiuser_settings_ux Change-Id: Id9d507039b58d3df66fe78710409716fd4816890 --- res/values/strings.xml | 2 +- ...AddUserWhenLockedPreferenceController.java | 12 ++++- .../GuestTelephonyPreferenceController.java | 4 +- ...RemoveGuestOnExitPreferenceController.java | 12 ++++- .../settings/users/UserDetailsSettings.java | 13 ++++- .../android/settings/users/UserSettings.java | 39 +++++++++++--- .../users/UserDetailsSettingsTest.java | 20 ++++++++ .../settings/users/UserSettingsTest.java | 51 ++++++++++++++++--- 8 files changed, 132 insertions(+), 21 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 075056db64e..3180f67480b 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12643,7 +12643,7 @@ Use print service - Allow multiple users + Allow user switch allow, multiple, user, permit, many diff --git a/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java b/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java index ce5533e5c64..4830fd700f7 100644 --- a/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java +++ b/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java @@ -44,7 +44,11 @@ public class AddUserWhenLockedPreferenceController extends TogglePreferenceContr } else { restrictedSwitchPreference.setDisabledByAdmin( mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); - restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled); + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + restrictedSwitchPreference.setVisible(true); + } else { + restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled); + } } } @@ -55,7 +59,11 @@ public class AddUserWhenLockedPreferenceController extends TogglePreferenceContr } else if (mUserCaps.disallowAddUser() || mUserCaps.disallowAddUserSetByAdmin()) { return DISABLED_FOR_USER; } else { - return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + return AVAILABLE; + } else { + return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } } } diff --git a/src/com/android/settings/users/GuestTelephonyPreferenceController.java b/src/com/android/settings/users/GuestTelephonyPreferenceController.java index 4fbd4493bb7..64563df548d 100644 --- a/src/com/android/settings/users/GuestTelephonyPreferenceController.java +++ b/src/com/android/settings/users/GuestTelephonyPreferenceController.java @@ -45,6 +45,8 @@ public class GuestTelephonyPreferenceController extends TogglePreferenceControll public int getAvailabilityStatus() { if (!mUserCaps.isAdmin() || !mUserCaps.mCanAddGuest) { return DISABLED_FOR_USER; + } else if (android.multiuser.Flags.newMultiuserSettingsUx()) { + return AVAILABLE; } else { return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } @@ -74,7 +76,7 @@ public class GuestTelephonyPreferenceController extends TogglePreferenceControll public void updateState(Preference preference) { super.updateState(preference); mUserCaps.updateAddUserCapabilities(mContext); - preference.setVisible(isAvailable() && mUserCaps.mUserSwitcherEnabled + preference.setVisible(isAvailable() && mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY) && !UserManager.isHeadlessSystemUserMode()); } diff --git a/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java b/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java index 01df5fddc2e..94fad9782b3 100644 --- a/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java +++ b/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java @@ -72,7 +72,11 @@ public class RemoveGuestOnExitPreferenceController extends BasePreferenceControl } else { restrictedSwitchPreference.setDisabledByAdmin( mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); - restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled); + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + restrictedSwitchPreference.setVisible(true); + } else { + restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled); + } } } @@ -89,7 +93,11 @@ public class RemoveGuestOnExitPreferenceController extends BasePreferenceControl || mUserCaps.disallowAddUserSetByAdmin()) { return DISABLED_FOR_USER; } else { - return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + return AVAILABLE; + } else { + return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } } } diff --git a/src/com/android/settings/users/UserDetailsSettings.java b/src/com/android/settings/users/UserDetailsSettings.java index 71dd43f92a0..588f01aaa79 100644 --- a/src/com/android/settings/users/UserDetailsSettings.java +++ b/src/com/android/settings/users/UserDetailsSettings.java @@ -126,7 +126,11 @@ public class UserDetailsSettings extends SettingsPreferenceFragment @Override public void onResume() { super.onResume(); - mSwitchUserPref.setEnabled(canSwitchUserNow()); + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + mSwitchUserPref.setEnabled(canSwitchUserNow() && mUserCaps.mUserSwitcherEnabled); + } else { + mSwitchUserPref.setEnabled(canSwitchUserNow()); + } if (mUserInfo.isGuest() && mGuestUserAutoCreated) { mRemoveUserPref.setEnabled((mUserInfo.flags & UserInfo.FLAG_INITIALIZED) != 0); } @@ -358,7 +362,12 @@ public class UserDetailsSettings extends SettingsPreferenceFragment mSwitchUserPref.setDisabledByAdmin(RestrictedLockUtilsInternal.getDeviceOwner(context)); } else { mSwitchUserPref.setDisabledByAdmin(null); - mSwitchUserPref.setSelectable(true); + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + mSwitchUserPref.setEnabled(mUserCaps.mUserSwitcherEnabled); + mSwitchUserPref.setSelectable(mUserCaps.mUserSwitcherEnabled); + } else { + mSwitchUserPref.setSelectable(true); + } mSwitchUserPref.setOnPreferenceClickListener(this); } if (mUserInfo.isMain() || mUserInfo.isGuest() || !UserManager.isMultipleAdminEnabled() diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java index bf21c9b913a..6fe79da8f08 100644 --- a/src/com/android/settings/users/UserSettings.java +++ b/src/com/android/settings/users/UserSettings.java @@ -1198,15 +1198,23 @@ public class UserSettings extends SettingsPreferenceFragment } List users; - if (mUserCaps.mUserSwitcherEnabled) { + if (android.multiuser.Flags.newMultiuserSettingsUx()) { // Only users that can be switched to should show up here. // e.g. Managed profiles appear under Accounts Settings instead users = mUserManager.getAliveUsers().stream() .filter(UserInfo::supportsSwitchToByUser) .collect(Collectors.toList()); } else { - // Only current user will be displayed in case of multi-user switch is disabled - users = List.of(mUserManager.getUserInfo(context.getUserId())); + if (mUserCaps.mUserSwitcherEnabled) { + // Only users that can be switched to should show up here. + // e.g. Managed profiles appear under Accounts Settings instead + users = mUserManager.getAliveUsers().stream() + .filter(UserInfo::supportsSwitchToByUser) + .collect(Collectors.toList()); + } else { + // Only current user will be displayed in case of multi-user switch is disabled + users = List.of(mUserManager.getUserInfo(context.getUserId())); + } } final ArrayList missingIcons = new ArrayList<>(); @@ -1423,10 +1431,16 @@ public class UserSettings extends SettingsPreferenceFragment } else { pref.setDisabledByAdmin(null); } - if (mUserCaps.mUserSwitcherEnabled) { + if (android.multiuser.Flags.newMultiuserSettingsUx()) { mGuestUserCategory.addPreference(pref); // guest user preference is shown hence also make guest category visible mGuestUserCategory.setVisible(true); + } else { + if (mUserCaps.mUserSwitcherEnabled) { + mGuestUserCategory.addPreference(pref); + // guest user preference is shown hence also make guest category visible + mGuestUserCategory.setVisible(true); + } } isGuestAlreadyCreated = true; } @@ -1453,7 +1467,8 @@ public class UserSettings extends SettingsPreferenceFragment if (!isGuestAlreadyCreated && mUserCaps.mCanAddGuest && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_GUEST) && WizardManagerHelper.isDeviceProvisioned(context) - && mUserCaps.mUserSwitcherEnabled) { + && (mUserCaps.mUserSwitcherEnabled + || android.multiuser.Flags.newMultiuserSettingsUx())) { Drawable icon = context.getDrawable( com.android.settingslib.R.drawable.ic_account_circle); mAddGuest.setIcon(centerAndTint(icon)); @@ -1466,7 +1481,12 @@ public class UserSettings extends SettingsPreferenceFragment mAddGuest.setEnabled(false); } else { mAddGuest.setTitle(com.android.settingslib.R.string.guest_new_guest); - mAddGuest.setEnabled(canSwitchUserNow()); + if (android.multiuser.Flags.newMultiuserSettingsUx() + && mUserCaps.mDisallowAddUserSetByAdmin) { + mAddGuest.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); + } else { + mAddGuest.setEnabled(canSwitchUserNow()); + } } } else { mAddGuest.setVisible(false); @@ -1496,7 +1516,8 @@ public class UserSettings extends SettingsPreferenceFragment boolean canAddRestrictedProfile) { if ((mUserCaps.mCanAddUser && !mUserCaps.mDisallowAddUserSetByAdmin) && WizardManagerHelper.isDeviceProvisioned(context) - && mUserCaps.mUserSwitcherEnabled) { + && (mUserCaps.mUserSwitcherEnabled + || android.multiuser.Flags.newMultiuserSettingsUx())) { addUser.setVisible(true); addUser.setSelectable(true); final boolean canAddMoreUsers = @@ -1514,6 +1535,10 @@ public class UserSettings extends SettingsPreferenceFragment addUser.setDisabledByAdmin( mUserCaps.mDisallowAddUser ? mUserCaps.mEnforcedAdmin : null); } + } else if (android.multiuser.Flags.newMultiuserSettingsUx() + && mUserCaps.mDisallowAddUserSetByAdmin) { + addUser.setVisible(true); + addUser.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); } else { addUser.setVisible(false); } diff --git a/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java index 44e1cc6986c..e035274a5de 100644 --- a/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java +++ b/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java @@ -42,9 +42,13 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.UserInfo; +import android.multiuser.Flags; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.telephony.TelephonyManager; import androidx.fragment.app.FragmentActivity; @@ -63,6 +67,7 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 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; @@ -123,6 +128,8 @@ public class UserDetailsSettingsTest { private Bundle mArguments; private UserInfo mUserInfo; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -244,6 +251,19 @@ public class UserDetailsSettingsTest { verify(mSwitchUserPref).setEnabled(false); } + @Test + @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) + public void onResume_UserSwitcherDisabled_shouldDisableSwitchPref() { + setupSelectedUser(); + mUserCapabilities.mUserSwitcherEnabled = false; + mFragment.mSwitchUserPref = mSwitchUserPref; + mFragment.onAttach(mContext); + + mFragment.onResume(); + + verify(mSwitchUserPref).setEnabled(false); + } + @Test public void onResume_switchDisallowed_shouldDisableSwitchPref() { setupSelectedUser(); diff --git a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java index 5826ca25cc9..f35b64ecf04 100644 --- a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java +++ b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java @@ -46,10 +46,15 @@ import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; +import android.multiuser.Flags; import android.os.Bundle; import android.os.Looper; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; import android.text.SpannableStringBuilder; import android.view.Menu; @@ -75,6 +80,7 @@ import com.android.settingslib.search.SearchIndexableRaw; import org.junit.After; import org.junit.Before; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.AdditionalMatchers; @@ -142,6 +148,9 @@ public class UserSettingsTest { private UserSettings mFragment; private UserCapabilities mUserCapabilities; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -406,6 +415,7 @@ public class UserSettingsTest { } @Test + @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) public void updateUserList_addUserDisallowedByAdmin_shouldNotShowAddUser() { RestrictedLockUtils.EnforcedAdmin enforcedAdmin = mock( RestrictedLockUtils.EnforcedAdmin.class); @@ -420,6 +430,22 @@ public class UserSettingsTest { verify(mAddUserPreference).setVisible(false); } + @Test + @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) + public void updateUserList_addUserDisallowedByAdmin_shouldShowPrefDisabledByAdmin() { + RestrictedLockUtils.EnforcedAdmin enforcedAdmin = mock( + RestrictedLockUtils.EnforcedAdmin.class); + + mUserCapabilities.mEnforcedAdmin = enforcedAdmin; + mUserCapabilities.mCanAddUser = false; + mUserCapabilities.mDisallowAddUser = true; + mUserCapabilities.mDisallowAddUserSetByAdmin = true; + doReturn(true).when(mAddUserPreference).isEnabled(); + + mFragment.updateUserList(); + + verify(mAddUserPreference).setDisabledByAdmin(enforcedAdmin); + } @Test public void updateUserList_cannotAddUserButCanSwitchUser_shouldNotShowAddUser() { mUserCapabilities.mCanAddUser = false; @@ -461,18 +487,31 @@ public class UserSettingsTest { } @Test - public void updateUserList_userSwitcherDisabled_shouldNotShowAddUser() { + public void updateUserList_userSwitcherDisabled_shouldShowAddUser() { givenUsers(getAdminUser(true)); mUserCapabilities.mCanAddUser = true; mUserCapabilities.mUserSwitcherEnabled = false; mFragment.updateUserList(); - verify(mAddUserPreference).setVisible(false); + verify(mAddUserPreference).setVisible(true); } @Test - public void updateUserList_userSwitcherDisabled_shouldNotShowAddGuest() { + public void updateUserList_userSwitcherDisabled_shouldShowAddGuest() { + givenUsers(getAdminUser(true)); + mUserCapabilities.mCanAddGuest = true; + mUserCapabilities.mUserSwitcherEnabled = false; + doReturn(true) + .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_GUEST)); + + mFragment.updateUserList(); + + verify(mAddGuestPreference).setVisible(true); + } + + @Test + public void updateUserList_userSwitcherDisabledCannotAddMoreGuests_shouldNotShowAddGuest() { givenUsers(getAdminUser(true)); mUserCapabilities.mCanAddGuest = true; mUserCapabilities.mUserSwitcherEnabled = false; @@ -533,18 +572,18 @@ public class UserSettingsTest { } @Test - public void updateUserList_existingSecondaryUser_shouldAddOnlyCurrUser_MultiUserIsDisabled() { + public void updateUserList_existingSecondaryUser_shouldAddAllUsers_MultiUserIsDisabled() { givenUsers(getAdminUser(true), getSecondaryUser(false)); mUserCapabilities.mUserSwitcherEnabled = false; mFragment.updateUserList(); ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); - verify(mFragment.mUserListCategory, times(1)) + verify(mFragment.mUserListCategory, times(2)) .addPreference(captor.capture()); List userPrefs = captor.getAllValues(); - assertThat(userPrefs.size()).isEqualTo(1); + assertThat(userPrefs.size()).isEqualTo(2); assertThat(userPrefs.get(0)).isSameInstanceAs(mMePreference); } From 0f311e11959650c8490b85b185cbd955db399c32 Mon Sep 17 00:00:00 2001 From: Tetiana Meronyk Date: Tue, 18 Jun 2024 18:10:25 +0000 Subject: [PATCH 14/23] Add handling of admin enforced restrictions in Users Settings Before, when the restrictions were applied, the preferences that were restricted were hidden. After this change, if admin applies a restriction, the preference is displayed as disabled and Policy Transparency Dialog is displayed Bug: 338226475 Test: atest UserSettingsTest && atest UserDetailsSettingsTest Flag: android.multiuser.new_multiuser_settings_ux Change-Id: I1b5aeeeec7accde278ff3e46ea3d64c91d8400db --- ...AddUserWhenLockedPreferenceController.java | 15 ++++---- .../GuestTelephonyPreferenceController.java | 13 +++++-- .../users/MultiUserSwitchBarController.java | 5 --- ...RemoveGuestOnExitPreferenceController.java | 33 ++++++++++++----- .../android/settings/users/UserSettings.java | 32 +++++++++------- .../settings/users/UserSettingsTest.java | 37 +++++++++++++++++++ 6 files changed, 95 insertions(+), 40 deletions(-) diff --git a/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java b/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java index 4830fd700f7..861e69d08e8 100644 --- a/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java +++ b/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java @@ -42,11 +42,14 @@ public class AddUserWhenLockedPreferenceController extends TogglePreferenceContr if (!isAvailable()) { restrictedSwitchPreference.setVisible(false); } else { - restrictedSwitchPreference.setDisabledByAdmin( - mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); if (android.multiuser.Flags.newMultiuserSettingsUx()) { + if (mUserCaps.mDisallowAddUserSetByAdmin) { + restrictedSwitchPreference.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); + } restrictedSwitchPreference.setVisible(true); } else { + restrictedSwitchPreference.setDisabledByAdmin( + mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled); } } @@ -56,14 +59,12 @@ public class AddUserWhenLockedPreferenceController extends TogglePreferenceContr public int getAvailabilityStatus() { if (!mUserCaps.isAdmin()) { return DISABLED_FOR_USER; + } else if (android.multiuser.Flags.newMultiuserSettingsUx()) { + return AVAILABLE; } else if (mUserCaps.disallowAddUser() || mUserCaps.disallowAddUserSetByAdmin()) { return DISABLED_FOR_USER; } else { - if (android.multiuser.Flags.newMultiuserSettingsUx()) { - return AVAILABLE; - } else { - return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; - } + return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } } diff --git a/src/com/android/settings/users/GuestTelephonyPreferenceController.java b/src/com/android/settings/users/GuestTelephonyPreferenceController.java index 64563df548d..7cd8287547c 100644 --- a/src/com/android/settings/users/GuestTelephonyPreferenceController.java +++ b/src/com/android/settings/users/GuestTelephonyPreferenceController.java @@ -43,12 +43,17 @@ public class GuestTelephonyPreferenceController extends TogglePreferenceControll @Override public int getAvailabilityStatus() { - if (!mUserCaps.isAdmin() || !mUserCaps.mCanAddGuest) { - return DISABLED_FOR_USER; - } else if (android.multiuser.Flags.newMultiuserSettingsUx()) { + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + if (!mUserCaps.isAdmin()) { + return DISABLED_FOR_USER; + } return AVAILABLE; } else { - return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + if (!mUserCaps.isAdmin() || !mUserCaps.mCanAddGuest) { + return DISABLED_FOR_USER; + } else { + return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } } } diff --git a/src/com/android/settings/users/MultiUserSwitchBarController.java b/src/com/android/settings/users/MultiUserSwitchBarController.java index f57b7959c37..641ae51c9e3 100644 --- a/src/com/android/settings/users/MultiUserSwitchBarController.java +++ b/src/com/android/settings/users/MultiUserSwitchBarController.java @@ -57,11 +57,6 @@ public class MultiUserSwitchBarController implements SwitchWidgetController.OnSw mSwitchBar.setDisabledByAdmin(RestrictedLockUtilsInternal .checkIfRestrictionEnforced(mContext, UserManager.DISALLOW_USER_SWITCH, UserHandle.myUserId())); - - } else if (mUserCapabilities.mDisallowAddUser) { - mSwitchBar.setDisabledByAdmin(RestrictedLockUtilsInternal - .checkIfRestrictionEnforced(mContext, UserManager.DISALLOW_ADD_USER, - UserHandle.myUserId())); } else { mSwitchBar.setEnabled(mUserCapabilities.mIsMain); } diff --git a/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java b/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java index 94fad9782b3..263a51b8ae0 100644 --- a/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java +++ b/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java @@ -22,6 +22,7 @@ import android.content.DialogInterface; import android.content.pm.UserInfo; import android.os.Bundle; import android.os.Handler; +import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.util.Log; @@ -33,6 +34,8 @@ import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedSwitchPreference; /** @@ -70,11 +73,15 @@ public class RemoveGuestOnExitPreferenceController extends BasePreferenceControl if (!isAvailable()) { restrictedSwitchPreference.setVisible(false); } else { - restrictedSwitchPreference.setDisabledByAdmin( - mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); if (android.multiuser.Flags.newMultiuserSettingsUx()) { + final RestrictedLockUtils.EnforcedAdmin disallowRemoveUserAdmin = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, + UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId()); + restrictedSwitchPreference.setDisabledByAdmin(disallowRemoveUserAdmin); restrictedSwitchPreference.setVisible(true); } else { + restrictedSwitchPreference.setDisabledByAdmin( + mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled); } } @@ -86,15 +93,21 @@ public class RemoveGuestOnExitPreferenceController extends BasePreferenceControl // then disable this controller // also disable this controller for non-admin users // also disable when config_guestUserAllowEphemeralStateChange is false - if (mUserManager.isGuestUserAlwaysEphemeral() - || !UserManager.isGuestUserAllowEphemeralStateChange() - || !mUserCaps.isAdmin() - || mUserCaps.disallowAddUser() - || mUserCaps.disallowAddUserSetByAdmin()) { - return DISABLED_FOR_USER; - } else { - if (android.multiuser.Flags.newMultiuserSettingsUx()) { + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + if (mUserManager.isGuestUserAlwaysEphemeral() + || !UserManager.isGuestUserAllowEphemeralStateChange() + || !mUserCaps.isAdmin()) { + return DISABLED_FOR_USER; + } else { return AVAILABLE; + } + } else { + if (mUserManager.isGuestUserAlwaysEphemeral() + || !UserManager.isGuestUserAllowEphemeralStateChange() + || !mUserCaps.isAdmin() + || mUserCaps.disallowAddUser() + || mUserCaps.disallowAddUserSetByAdmin()) { + return DISABLED_FOR_USER; } else { return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java index 6fe79da8f08..39d9139cb2e 100644 --- a/src/com/android/settings/users/UserSettings.java +++ b/src/com/android/settings/users/UserSettings.java @@ -463,7 +463,8 @@ public class UserSettings extends SettingsPreferenceFragment @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { int pos = 0; - if (!isCurrentUserAdmin() && canSwitchUserNow() && !isCurrentUserGuest()) { + if (!isCurrentUserAdmin() && (canSwitchUserNow() || Flags.newMultiuserSettingsUx()) + && !isCurrentUserGuest()) { String nickname = mUserManager.getUserName(); MenuItem removeThisUser = menu.add(0, MENU_REMOVE_USER, pos++, getResources().getString(R.string.user_remove_user_menu, nickname)); @@ -1198,7 +1199,7 @@ public class UserSettings extends SettingsPreferenceFragment } List users; - if (android.multiuser.Flags.newMultiuserSettingsUx()) { + if (Flags.newMultiuserSettingsUx()) { // Only users that can be switched to should show up here. // e.g. Managed profiles appear under Accounts Settings instead users = mUserManager.getAliveUsers().stream() @@ -1265,7 +1266,10 @@ public class UserSettings extends SettingsPreferenceFragment pref.setSummary(R.string.user_summary_not_set_up); // Disallow setting up user which results in user switching when the // restriction is set. - pref.setEnabled(!mUserCaps.mDisallowSwitchUser && canSwitchUserNow()); + // If newMultiuserSettingsUx flag is enabled, allow opening user details page + // since switch to user will be disabled + pref.setEnabled((!mUserCaps.mDisallowSwitchUser && canSwitchUserNow()) + || Flags.newMultiuserSettingsUx()); } } else if (user.isRestricted()) { pref.setSummary(R.string.user_summary_restricted_profile); @@ -1425,13 +1429,13 @@ public class UserSettings extends SettingsPreferenceFragment getContext().getResources(), icon))); pref.setKey(KEY_USER_GUEST); pref.setOrder(Preference.DEFAULT_ORDER); - if (mUserCaps.mDisallowSwitchUser) { + if (mUserCaps.mDisallowSwitchUser && !Flags.newMultiuserSettingsUx()) { pref.setDisabledByAdmin( RestrictedLockUtilsInternal.getDeviceOwner(context)); } else { pref.setDisabledByAdmin(null); } - if (android.multiuser.Flags.newMultiuserSettingsUx()) { + if (Flags.newMultiuserSettingsUx()) { mGuestUserCategory.addPreference(pref); // guest user preference is shown hence also make guest category visible mGuestUserCategory.setVisible(true); @@ -1464,11 +1468,11 @@ public class UserSettings extends SettingsPreferenceFragment private boolean updateAddGuestPreference(Context context, boolean isGuestAlreadyCreated) { boolean isVisible = false; - if (!isGuestAlreadyCreated && mUserCaps.mCanAddGuest + if (!isGuestAlreadyCreated && (mUserCaps.mCanAddGuest + || (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUserSetByAdmin)) && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_GUEST) && WizardManagerHelper.isDeviceProvisioned(context) - && (mUserCaps.mUserSwitcherEnabled - || android.multiuser.Flags.newMultiuserSettingsUx())) { + && (mUserCaps.mUserSwitcherEnabled || Flags.newMultiuserSettingsUx())) { Drawable icon = context.getDrawable( com.android.settingslib.R.drawable.ic_account_circle); mAddGuest.setIcon(centerAndTint(icon)); @@ -1481,11 +1485,11 @@ public class UserSettings extends SettingsPreferenceFragment mAddGuest.setEnabled(false); } else { mAddGuest.setTitle(com.android.settingslib.R.string.guest_new_guest); - if (android.multiuser.Flags.newMultiuserSettingsUx() + if (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUserSetByAdmin) { mAddGuest.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); } else { - mAddGuest.setEnabled(canSwitchUserNow()); + mAddGuest.setEnabled(canSwitchUserNow() || Flags.newMultiuserSettingsUx()); } } } else { @@ -1516,15 +1520,15 @@ public class UserSettings extends SettingsPreferenceFragment boolean canAddRestrictedProfile) { if ((mUserCaps.mCanAddUser && !mUserCaps.mDisallowAddUserSetByAdmin) && WizardManagerHelper.isDeviceProvisioned(context) - && (mUserCaps.mUserSwitcherEnabled - || android.multiuser.Flags.newMultiuserSettingsUx())) { + && (mUserCaps.mUserSwitcherEnabled || Flags.newMultiuserSettingsUx())) { addUser.setVisible(true); addUser.setSelectable(true); final boolean canAddMoreUsers = mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY) || (canAddRestrictedProfile && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_RESTRICTED)); - addUser.setEnabled(canAddMoreUsers && !mAddingUser && canSwitchUserNow()); + addUser.setEnabled(canAddMoreUsers && !mAddingUser + && (canSwitchUserNow() || Flags.newMultiuserSettingsUx())); if (!canAddMoreUsers) { addUser.setSummary(getString(R.string.user_add_max_count)); @@ -1535,7 +1539,7 @@ public class UserSettings extends SettingsPreferenceFragment addUser.setDisabledByAdmin( mUserCaps.mDisallowAddUser ? mUserCaps.mEnforcedAdmin : null); } - } else if (android.multiuser.Flags.newMultiuserSettingsUx() + } else if (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUserSetByAdmin) { addUser.setVisible(true); addUser.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); diff --git a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java index f35b64ecf04..5bb84e12504 100644 --- a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java +++ b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java @@ -368,6 +368,7 @@ public class UserSettingsTest { } @Test + @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) public void updateUserList_cannotSwitchUser_shouldDisableAddUser() { mUserCapabilities.mCanAddUser = true; doReturn(true).when(mUserManager).canAddMoreUsers(anyString()); @@ -383,6 +384,20 @@ public class UserSettingsTest { verify(mAddUserPreference).setSelectable(true); } + @Test + @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) + public void updateUserList_disallowAddUser_shouldDisableAddUserAndAddGuest() { + mUserCapabilities.mDisallowAddUserSetByAdmin = true; + doReturn(true).when(mUserManager).canAddMoreUsers(anyString()); + doReturn(SWITCHABILITY_STATUS_OK) + .when(mUserManager).getUserSwitchability(); + + mFragment.updateUserList(); + + verify(mAddUserPreference).setVisible(true); + verify(mAddUserPreference).setDisabledByAdmin(any()); + } + @Test public void updateUserList_canNotAddMoreUsers_shouldDisableAddUserWithSummary() { mUserCapabilities.mCanAddUser = true; @@ -401,6 +416,7 @@ public class UserSettingsTest { } @Test + @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) public void updateUserList_cannotSwitchUser_shouldDisableAddGuest() { mUserCapabilities.mCanAddGuest = true; doReturn(true) @@ -414,6 +430,26 @@ public class UserSettingsTest { verify(mAddGuestPreference).setSelectable(true); } + @Test + @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) + public void updateUserList_cannotSwitchUser_shouldKeepPreferencesVisibleAndEnabled() { + givenUsers(getAdminUser(true)); + mUserCapabilities.mCanAddGuest = true; + mUserCapabilities.mCanAddUser = true; + mUserCapabilities.mDisallowSwitchUser = true; + doReturn(true) + .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_GUEST)); + doReturn(true) + .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_SECONDARY)); + + mFragment.updateUserList(); + + verify(mAddGuestPreference).setVisible(true); + verify(mAddGuestPreference).setEnabled(true); + verify(mAddUserPreference).setVisible(true); + verify(mAddUserPreference).setEnabled(true); + } + @Test @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) public void updateUserList_addUserDisallowedByAdmin_shouldNotShowAddUser() { @@ -670,6 +706,7 @@ public class UserSettingsTest { } @Test + @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) public void updateUserList_uninitializedUserAndCanNotSwitchUser_shouldDisablePref() { UserInfo uninitializedUser = getSecondaryUser(false); removeFlag(uninitializedUser, UserInfo.FLAG_INITIALIZED); From 25e1b4d88b6f7cc7b37d46e3b1f7054418f5ff7f Mon Sep 17 00:00:00 2001 From: Tetiana Meronyk Date: Mon, 24 Jun 2024 12:35:14 +0000 Subject: [PATCH 15/23] Show disabled add actions in COPE mode Before this change these actions were hidden. After this change, they are displayed but disabled which makes it more intuitive. Bug: 336762423 Test: atest UserSettingsTest && atest UserDetailsSettingsTest Flag: android.multiuser.new_multiuser_settings_ux Change-Id: Ie07816b7d3817d12e78e1ec2692fcddea9328933 --- ...AddUserWhenLockedPreferenceController.java | 4 +- .../GuestTelephonyPreferenceController.java | 37 +++++++++++++++++-- ...RemoveGuestOnExitPreferenceController.java | 11 +++++- .../android/settings/users/UserSettings.java | 35 ++++++++++++++++-- .../settings/users/UserSettingsTest.java | 28 ++++++++++++++ 5 files changed, 105 insertions(+), 10 deletions(-) diff --git a/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java b/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java index 861e69d08e8..fe90a2a25b8 100644 --- a/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java +++ b/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java @@ -43,10 +43,12 @@ public class AddUserWhenLockedPreferenceController extends TogglePreferenceContr restrictedSwitchPreference.setVisible(false); } else { if (android.multiuser.Flags.newMultiuserSettingsUx()) { + restrictedSwitchPreference.setVisible(true); if (mUserCaps.mDisallowAddUserSetByAdmin) { restrictedSwitchPreference.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); + } else if (mUserCaps.mDisallowAddUser) { + restrictedSwitchPreference.setVisible(false); } - restrictedSwitchPreference.setVisible(true); } else { restrictedSwitchPreference.setDisabledByAdmin( mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); diff --git a/src/com/android/settings/users/GuestTelephonyPreferenceController.java b/src/com/android/settings/users/GuestTelephonyPreferenceController.java index 7cd8287547c..e730cbf075f 100644 --- a/src/com/android/settings/users/GuestTelephonyPreferenceController.java +++ b/src/com/android/settings/users/GuestTelephonyPreferenceController.java @@ -19,12 +19,16 @@ package com.android.settings.users; import android.content.Context; import android.content.pm.PackageManager; import android.os.Bundle; +import android.os.UserHandle; import android.os.UserManager; import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.RestrictedSwitchPreference; /** * Controls the preference on the user settings screen which determines whether the guest user @@ -43,6 +47,10 @@ public class GuestTelephonyPreferenceController extends TogglePreferenceControll @Override public int getAvailabilityStatus() { + if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY) + || UserManager.isHeadlessSystemUserMode()) { + return DISABLED_FOR_USER; + } if (android.multiuser.Flags.newMultiuserSettingsUx()) { if (!mUserCaps.isAdmin()) { return DISABLED_FOR_USER; @@ -81,8 +89,31 @@ public class GuestTelephonyPreferenceController extends TogglePreferenceControll public void updateState(Preference preference) { super.updateState(preference); mUserCaps.updateAddUserCapabilities(mContext); - preference.setVisible(isAvailable() - && mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY) - && !UserManager.isHeadlessSystemUserMode()); + final RestrictedSwitchPreference restrictedSwitchPreference = + (RestrictedSwitchPreference) preference; + restrictedSwitchPreference.setChecked(isChecked()); + if (!isAvailable()) { + restrictedSwitchPreference.setVisible(false); + } else { + if (android.multiuser.Flags.newMultiuserSettingsUx()) { + restrictedSwitchPreference.setVisible(true); + final RestrictedLockUtils.EnforcedAdmin disallowRemoveUserAdmin = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, + UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId()); + if (disallowRemoveUserAdmin != null) { + restrictedSwitchPreference.setDisabledByAdmin(disallowRemoveUserAdmin); + } else if (mUserCaps.mDisallowAddUserSetByAdmin) { + restrictedSwitchPreference.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); + } else if (mUserCaps.mDisallowAddUser) { + // Adding user is restricted by system + restrictedSwitchPreference.setVisible(false); + } + } else { + restrictedSwitchPreference.setDisabledByAdmin( + mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); + restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled + && isAvailable()); + } + } } } diff --git a/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java b/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java index 263a51b8ae0..345b5068788 100644 --- a/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java +++ b/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java @@ -74,11 +74,18 @@ public class RemoveGuestOnExitPreferenceController extends BasePreferenceControl restrictedSwitchPreference.setVisible(false); } else { if (android.multiuser.Flags.newMultiuserSettingsUx()) { + restrictedSwitchPreference.setVisible(true); final RestrictedLockUtils.EnforcedAdmin disallowRemoveUserAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId()); - restrictedSwitchPreference.setDisabledByAdmin(disallowRemoveUserAdmin); - restrictedSwitchPreference.setVisible(true); + if (disallowRemoveUserAdmin != null) { + restrictedSwitchPreference.setDisabledByAdmin(disallowRemoveUserAdmin); + } else if (mUserCaps.mDisallowAddUserSetByAdmin) { + restrictedSwitchPreference.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); + } else if (mUserCaps.mDisallowAddUser) { + // Adding user is restricted by system + restrictedSwitchPreference.setVisible(false); + } } else { restrictedSwitchPreference.setDisabledByAdmin( mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java index 39d9139cb2e..0cf01e311e1 100644 --- a/src/com/android/settings/users/UserSettings.java +++ b/src/com/android/settings/users/UserSettings.java @@ -1469,7 +1469,7 @@ public class UserSettings extends SettingsPreferenceFragment private boolean updateAddGuestPreference(Context context, boolean isGuestAlreadyCreated) { boolean isVisible = false; if (!isGuestAlreadyCreated && (mUserCaps.mCanAddGuest - || (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUserSetByAdmin)) + || (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUser)) && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_GUEST) && WizardManagerHelper.isDeviceProvisioned(context) && (mUserCaps.mUserSwitcherEnabled || Flags.newMultiuserSettingsUx())) { @@ -1488,6 +1488,19 @@ public class UserSettings extends SettingsPreferenceFragment if (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUserSetByAdmin) { mAddGuest.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); + } else if (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUser) { + final List enforcingUsers = + mUserManager.getUserRestrictionSources(UserManager.DISALLOW_ADD_USER, + UserHandle.of(UserHandle.myUserId())); + if (!enforcingUsers.isEmpty()) { + final UserManager.EnforcingUser enforcingUser = enforcingUsers.get(0); + final int restrictionSource = enforcingUser.getUserRestrictionSource(); + if (restrictionSource == UserManager.RESTRICTION_SOURCE_SYSTEM) { + mAddGuest.setEnabled(false); + } else { + mAddGuest.setVisible(false); + } + } } else { mAddGuest.setEnabled(canSwitchUserNow() || Flags.newMultiuserSettingsUx()); } @@ -1518,7 +1531,8 @@ public class UserSettings extends SettingsPreferenceFragment private void updateAddUserCommon(Context context, RestrictedPreference addUser, boolean canAddRestrictedProfile) { - if ((mUserCaps.mCanAddUser && !mUserCaps.mDisallowAddUserSetByAdmin) + if ((mUserCaps.mCanAddUser + && !(mUserCaps.mDisallowAddUserSetByAdmin && Flags.newMultiuserSettingsUx())) && WizardManagerHelper.isDeviceProvisioned(context) && (mUserCaps.mUserSwitcherEnabled || Flags.newMultiuserSettingsUx())) { addUser.setVisible(true); @@ -1539,10 +1553,23 @@ public class UserSettings extends SettingsPreferenceFragment addUser.setDisabledByAdmin( mUserCaps.mDisallowAddUser ? mUserCaps.mEnforcedAdmin : null); } - } else if (Flags.newMultiuserSettingsUx() - && mUserCaps.mDisallowAddUserSetByAdmin) { + } else if (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUserSetByAdmin) { addUser.setVisible(true); addUser.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); + } else if (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUser) { + final List enforcingUsers = + mUserManager.getUserRestrictionSources(UserManager.DISALLOW_ADD_USER, + UserHandle.of(UserHandle.myUserId())); + if (!enforcingUsers.isEmpty()) { + final UserManager.EnforcingUser enforcingUser = enforcingUsers.get(0); + final int restrictionSource = enforcingUser.getUserRestrictionSource(); + if (restrictionSource == UserManager.RESTRICTION_SOURCE_SYSTEM) { + addUser.setVisible(true); + addUser.setEnabled(false); + } else { + addUser.setVisible(false); + } + } } else { addUser.setVisible(false); } diff --git a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java index 5bb84e12504..85db0bd88b9 100644 --- a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java +++ b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.app.settings.SettingsEnums; @@ -450,6 +451,33 @@ public class UserSettingsTest { verify(mAddUserPreference).setEnabled(true); } + @Test + @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) + public void updateUserList_disallowAddUser_shouldShowButDisableAddActions() { + givenUsers(getAdminUser(true)); + mUserCapabilities.mCanAddGuest = true; + mUserCapabilities.mCanAddUser = false; + mUserCapabilities.mDisallowAddUser = true; + mUserCapabilities.mDisallowAddUserSetByAdmin = false; + List enforcingUsers = new ArrayList<>(); + enforcingUsers.add(new UserManager.EnforcingUser(UserHandle.myUserId(), + UserManager.RESTRICTION_SOURCE_SYSTEM)); + when(mUserManager.getUserRestrictionSources(UserManager.DISALLOW_ADD_USER, + UserHandle.of(UserHandle.myUserId()))).thenReturn(enforcingUsers); + + doReturn(true) + .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_GUEST)); + doReturn(true) + .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_SECONDARY)); + + mFragment.updateUserList(); + + verify(mAddGuestPreference).setVisible(true); + verify(mAddGuestPreference).setEnabled(false); + verify(mAddUserPreference).setVisible(true); + verify(mAddUserPreference).setEnabled(false); + } + @Test @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX}) public void updateUserList_addUserDisallowedByAdmin_shouldNotShowAddUser() { From e4087bf29f98bf50bb45f193bf0363499e0d011d Mon Sep 17 00:00:00 2001 From: Julien Desprez Date: Mon, 24 Jun 2024 20:26:19 +0000 Subject: [PATCH 16/23] Enable 16kb tests on general-tests suite This doesn't need to be built on device side. Flag: TEST_ONLY Test: https://android-build.corp.google.com/builds/abtd/run/L50300030004790194 Bug: 342232954 Change-Id: I86493e8d197ca6acc24743e44b63a764bf4d36df --- tests/Enable16KbTests/Android.bp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Enable16KbTests/Android.bp b/tests/Enable16KbTests/Android.bp index 781ea8f7667..fa05d33954d 100644 --- a/tests/Enable16KbTests/Android.bp +++ b/tests/Enable16KbTests/Android.bp @@ -33,7 +33,6 @@ android_test { ], platform_apis: true, certificate: "platform", - test_suites: ["device-tests"], libs: [ "android.test.runner", "android.test.base", @@ -57,6 +56,6 @@ java_test_host { data: [ ":test_16kb_app", ], - test_suites: ["device-tests"], + test_suites: ["general-tests"], test_config: "AndroidTest.xml", } From b132ed91b1d0bd13c977d96727af305123d285df Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 25 Jun 2024 12:12:03 +0800 Subject: [PATCH 17/23] Clean up MobileNetworkRepository.DataRoamingObserver Bug: 348118234 Flag: EXEMPT clean up Test: manual - on Mobile Settings Change-Id: If9e7ed7cde0d7864ff00d135339a7725de179f1b --- .../network/MobileNetworkRepository.java | 55 +------------------ 1 file changed, 1 insertion(+), 54 deletions(-) diff --git a/src/com/android/settings/network/MobileNetworkRepository.java b/src/com/android/settings/network/MobileNetworkRepository.java index ce6f8842f9c..ebb341e0fec 100644 --- a/src/com/android/settings/network/MobileNetworkRepository.java +++ b/src/com/android/settings/network/MobileNetworkRepository.java @@ -85,7 +85,6 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions private List mActiveSubInfoEntityList = new ArrayList<>(); private Context mContext; private AirplaneModeObserver mAirplaneModeObserver; - private DataRoamingObserver mDataRoamingObserver; private MetricsFeatureProvider mMetricsFeatureProvider; private int mPhysicalSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX; private int mLogicalSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX; @@ -122,7 +121,6 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions mSubscriptionInfoDao = mMobileNetworkDatabase.mSubscriptionInfoDao(); mMobileNetworkInfoDao = mMobileNetworkDatabase.mMobileNetworkInfoDao(); mAirplaneModeObserver = new AirplaneModeObserver(new Handler(Looper.getMainLooper())); - mDataRoamingObserver = new DataRoamingObserver(new Handler(Looper.getMainLooper())); } private class AirplaneModeObserver extends ContentObserver { @@ -153,47 +151,6 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions } } - private class DataRoamingObserver extends ContentObserver { - private int mRegSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; - private String mBaseField = Settings.Global.DATA_ROAMING; - - DataRoamingObserver(Handler handler) { - super(handler); - } - - public void register(Context context, int subId) { - mRegSubId = subId; - String lastField = mBaseField; - createTelephonyManagerBySubId(subId); - TelephonyManager tm = mTelephonyManagerMap.get(subId); - if (tm.getSimCount() != 1) { - lastField += subId; - } - context.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(lastField), false, this); - } - - public void unRegister(Context context) { - context.getContentResolver().unregisterContentObserver(this); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - TelephonyManager tm = mTelephonyManagerMap.get(mRegSubId); - if (tm == null) { - return; - } - sExecutor.execute(() -> { - Log.d(TAG, "DataRoamingObserver changed"); - insertMobileNetworkInfo(mContext, mRegSubId, tm); - }); - boolean isDataRoamingEnabled = tm.isDataRoamingEnabled(); - for (MobileNetworkCallback callback : sCallbacks) { - callback.onDataRoamingChanged(mRegSubId, isDataRoamingEnabled); - } - } - } - /** * Register all callbacks and listener. * @@ -219,7 +176,6 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions observeAllMobileNetworkInfo(lifecycleOwner); if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { createTelephonyManagerBySubId(subId); - mDataRoamingObserver.register(mContext, subId); } // When one client registers callback first time, convey the cached results to the client // so that the client is aware of the content therein. @@ -283,7 +239,6 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions if (sCallbacks.isEmpty()) { mSubscriptionManager.removeOnSubscriptionsChangedListener(this); mAirplaneModeObserver.unRegister(mContext); - mDataRoamingObserver.unRegister(mContext); mTelephonyManagerMap.forEach((id, manager) -> { TelephonyCallback callback = mTelephonyCallbackMap.get(id); @@ -588,10 +543,8 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions private MobileNetworkInfoEntity convertToMobileNetworkInfoEntity(Context context, int subId, TelephonyManager telephonyManager) { boolean isDataEnabled = false; - boolean isDataRoamingEnabled = false; if (telephonyManager != null) { isDataEnabled = telephonyManager.isDataEnabled(); - isDataRoamingEnabled = telephonyManager.isDataRoamingEnabled(); } else { Log.d(TAG, "TelephonyManager is null, subId = " + subId); } @@ -607,7 +560,7 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions MobileNetworkUtils.isTdscdmaSupported(context, subId), MobileNetworkUtils.activeNetworkIsCellular(context), SubscriptionUtil.showToggleForPhysicalSim(mSubscriptionManager), - isDataRoamingEnabled + /* deprecated isDataRoamingEnabled = */ false ); } @@ -754,12 +707,6 @@ public class MobileNetworkRepository extends SubscriptionManager.OnSubscriptions default void onAirplaneModeChanged(boolean enabled) { } - - /** - * Notify clients data roaming changed of subscription. - */ - default void onDataRoamingChanged(int subId, boolean enabled) { - } } public void dump(IndentingPrintWriter printwriter) { From a23c53c9a94025ab3f43007db5294b353f416e4b Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 25 Jun 2024 13:26:12 +0800 Subject: [PATCH 18/23] Fix Talkback can focus on blank area By hiding radio button frame instead of just radio button. Fix: 347185801 Flag: EXEMPT bug fix Test: manual - on ApnSettings Change-Id: I7a2ba536f037a6cd014989bba1374d034169d90b --- src/com/android/settings/network/apn/ApnPreference.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/network/apn/ApnPreference.java b/src/com/android/settings/network/apn/ApnPreference.java index 879fcb602f1..55258c139c6 100644 --- a/src/com/android/settings/network/apn/ApnPreference.java +++ b/src/com/android/settings/network/apn/ApnPreference.java @@ -85,10 +85,11 @@ public class ApnPreference extends Preference final RelativeLayout textArea = (RelativeLayout) view.findViewById(R.id.text_layout); textArea.setOnClickListener(this); + final View radioButtonFrame = view.itemView.requireViewById(R.id.apn_radio_button_frame); final RadioButton rb = view.itemView.requireViewById(R.id.apn_radiobutton); mRadioButton = rb; if (mDefaultSelectable) { - view.itemView.requireViewById(R.id.apn_radio_button_frame).setOnClickListener((v) -> { + radioButtonFrame.setOnClickListener((v) -> { rb.performClick(); }); rb.setOnCheckedChangeListener(this); @@ -96,9 +97,9 @@ public class ApnPreference extends Preference mProtectFromCheckedChange = true; rb.setChecked(mIsChecked); mProtectFromCheckedChange = false; - rb.setVisibility(View.VISIBLE); + radioButtonFrame.setVisibility(View.VISIBLE); } else { - rb.setVisibility(View.GONE); + radioButtonFrame.setVisibility(View.GONE); } } From 954d0279179ad3febff9b405c4176aa289d7cbdd Mon Sep 17 00:00:00 2001 From: chelseahao Date: Tue, 25 Jun 2024 14:29:19 +0800 Subject: [PATCH 19/23] [Audiosharing] Test getInstance for states Test: atest -c com.android.settings.connecteddevice.audiosharing.audiostream Flag: com.android.settingslib.flags.enable_le_audio_qr_code_private_broadcast_sharing Bug: 345686602 Change-Id: Ic0f143b427966dfa039fca3bd0028e0c6f7382ab --- .../audiosharing/audiostreams/AddSourceBadCodeStateTest.java | 1 + .../audiosharing/audiostreams/AddSourceFailedStateTest.java | 1 + .../audiostreams/AddSourceWaitForResponseStateTest.java | 1 + .../audiosharing/audiostreams/SourceAddedStateTest.java | 1 + .../audiosharing/audiostreams/WaitForSyncStateTest.java | 1 + 5 files changed, 5 insertions(+) diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java index aba300e08bc..391a7b106b6 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java @@ -58,6 +58,7 @@ public class AddSourceBadCodeStateTest { @Test public void testGetInstance() { + mInstance = AddSourceBadCodeState.getInstance(); assertThat(mInstance).isNotNull(); assertThat(mInstance).isInstanceOf(SyncedState.class); } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java index 1bc9f9148f5..917d8dee99c 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java @@ -58,6 +58,7 @@ public class AddSourceFailedStateTest { @Test public void testGetInstance() { + mInstance = AddSourceFailedState.getInstance(); assertThat(mInstance).isNotNull(); assertThat(mInstance).isInstanceOf(SyncedState.class); } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java index 950ad38a64a..ce21658f64e 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java @@ -80,6 +80,7 @@ public class AddSourceWaitForResponseStateTest { @Test public void testGetInstance() { + mInstance = AddSourceWaitForResponseState.getInstance(); assertThat(mInstance).isNotNull(); assertThat(mInstance).isInstanceOf(AudioStreamStateHandler.class); } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java index 082735a31fc..59a42a1023d 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java @@ -85,6 +85,7 @@ public class SourceAddedStateTest { @Test public void testGetInstance() { + mInstance = SourceAddedState.getInstance(); assertThat(mInstance).isNotNull(); assertThat(mInstance).isInstanceOf(SourceAddedState.class); } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java index d97bf8fe58e..813ed2b4bd4 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java @@ -76,6 +76,7 @@ public class WaitForSyncStateTest { @Test public void testGetInstance() { + mInstance = WaitForSyncState.getInstance(); assertThat(mInstance).isNotNull(); assertThat(mInstance).isInstanceOf(AudioStreamStateHandler.class); } From a074f274991f6a172855e0253f49b0a6dd63c758 Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Tue, 25 Jun 2024 13:23:07 +0800 Subject: [PATCH 20/23] Add device to to cached device manager if it's not present Bug: 346923808 Test: atest BluetoothDeviceDetailsFragmentTest Flag: EXEMPT minor bug fix Change-Id: Ia4987bc7ec93cb6b54d188922b7232d83d528f2f --- .../bluetooth/BluetoothDeviceDetailsFragment.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index 44915fe2829..5f9957b9121 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -142,13 +142,23 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment } @VisibleForTesting + @Nullable CachedBluetoothDevice getCachedDevice(String deviceAddress) { if (sTestDataFactory != null) { return sTestDataFactory.getDevice(deviceAddress); } BluetoothDevice remoteDevice = mManager.getBluetoothAdapter().getRemoteDevice(deviceAddress); - return mManager.getCachedDeviceManager().findDevice(remoteDevice); + if (remoteDevice == null) { + return null; + } + CachedBluetoothDevice cachedDevice = + mManager.getCachedDeviceManager().findDevice(remoteDevice); + if (cachedDevice != null) { + return cachedDevice; + } + Log.i(TAG, "Add device to cached device manager: " + remoteDevice.getAnonymizedAddress()); + return mManager.getCachedDeviceManager().addDevice(remoteDevice); } @VisibleForTesting From a6016e655220539f9af3bf7d0e0ab66cb22f24bf Mon Sep 17 00:00:00 2001 From: Oleg Blinnikov Date: Tue, 1 Aug 2023 20:37:09 +0000 Subject: [PATCH 21/23] Added External Display settings page Settings page to show rotation, resolution, enable/disable display settings for external and overlay displays. In case persist.demo.userrotation.package_name sysprop is set, then the virtual display with this will also be shown. In case there is only one allowed display available, then this display will be shown right away. When there are more than 1 displays available, then the list of displays will be shown. Change-Id: I186667aaba94ed6befec3a98f4a87f2b2d1f1859 Test: atest ExternalDisplayUpdaterTest Test: atest ExternalDisplayPreferenceFragmentTest Test: atest ResolutionPreferenceFragmentTest Test: atest ConnectedDeviceGroupControllerTest Bug: 340218151 Bug: 294015706 Bug: 253296253 Flag: com.android.settings.flags.rotation_connected_display_setting Flag: com.android.settings.flags.resolution_and_enable_connected_display_setting --- ..._connecteddevice_flag_declarations.aconfig | 17 +- .../external_display_mirror_landscape.xml | 55 ++ .../external_display_mirror_portrait.xml | 55 ++ res/drawable/ic_external_display_32dp.xml | 32 ++ res/values/strings.xml | 33 ++ .../external_display_resolution_settings.xml | 20 + res/xml/external_display_settings.xml | 22 + .../SettingsPreferenceFragmentBase.java | 94 +++ .../ConnectedDeviceGroupController.java | 48 +- .../ExternalDisplayPreferenceFragment.java | 544 ++++++++++++++++++ .../ExternalDisplaySettingsConfiguration.java | 341 +++++++++++ .../display/ExternalDisplayUpdater.java | 181 ++++++ .../settings/connecteddevice/display/OWNERS | 7 + .../display/ResolutionPreferenceFragment.java | 343 +++++++++++ .../ConnectedDeviceGroupControllerTest.java | 57 +- tests/unit/Android.bp | 1 + ...ExternalDisplayPreferenceFragmentTest.java | 409 +++++++++++++ .../display/ExternalDisplayTestBase.java | 149 +++++ .../display/ExternalDisplayUpdaterTest.java | 121 ++++ .../ResolutionPreferenceFragmentTest.java | 195 +++++++ 20 files changed, 2708 insertions(+), 16 deletions(-) create mode 100644 res/drawable/external_display_mirror_landscape.xml create mode 100644 res/drawable/external_display_mirror_portrait.xml create mode 100644 res/drawable/ic_external_display_32dp.xml create mode 100644 res/xml/external_display_resolution_settings.xml create mode 100644 res/xml/external_display_settings.xml create mode 100644 src/com/android/settings/SettingsPreferenceFragmentBase.java create mode 100644 src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java create mode 100644 src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java create mode 100644 src/com/android/settings/connecteddevice/display/ExternalDisplayUpdater.java create mode 100644 src/com/android/settings/connecteddevice/display/OWNERS create mode 100644 src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragment.java create mode 100644 tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java create mode 100644 tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayTestBase.java create mode 100644 tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdaterTest.java create mode 100644 tests/unit/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragmentTest.java diff --git a/aconfig/settings_connecteddevice_flag_declarations.aconfig b/aconfig/settings_connecteddevice_flag_declarations.aconfig index 7942ccd1416..693e3985af8 100644 --- a/aconfig/settings_connecteddevice_flag_declarations.aconfig +++ b/aconfig/settings_connecteddevice_flag_declarations.aconfig @@ -8,6 +8,20 @@ flag { bug: "299405720" } +flag { + name: "rotation_connected_display_setting" + namespace: "display_manager" + description: "Allow changing rotation of the connected display." + bug: "294015706" +} + +flag { + name: "resolution_and_enable_connected_display_setting" + namespace: "display_manager" + description: "Allow enabling/disabling and changing resolution of the connected display." + bug: "253296253" +} + flag { name: "enable_auth_challenge_for_usb_preferences" namespace: "safety_center" @@ -15,7 +29,6 @@ flag { bug: "317367746" } - flag { name: "enable_bonded_bluetooth_device_searchable" namespace: "pixel_cross_device_control" @@ -24,4 +37,4 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -} \ No newline at end of file +} diff --git a/res/drawable/external_display_mirror_landscape.xml b/res/drawable/external_display_mirror_landscape.xml new file mode 100644 index 00000000000..4272ddbe7a8 --- /dev/null +++ b/res/drawable/external_display_mirror_landscape.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + diff --git a/res/drawable/external_display_mirror_portrait.xml b/res/drawable/external_display_mirror_portrait.xml new file mode 100644 index 00000000000..0fe7f93cdbc --- /dev/null +++ b/res/drawable/external_display_mirror_portrait.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + diff --git a/res/drawable/ic_external_display_32dp.xml b/res/drawable/ic_external_display_32dp.xml new file mode 100644 index 00000000000..3e1828255d2 --- /dev/null +++ b/res/drawable/ic_external_display_32dp.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index bfac793bc50..96d9d11b1f0 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1893,6 +1893,37 @@ Restart + + mirror, external display, connected display, usb display, resolution, rotation + + On + + Off + + External Display + + Use external display + + Display resolution + + External display is disconnected + + Rotation + + Standard + + 90° + + 180° + + 270° + + Changing rotation or resolution may stop any apps that are currently running + + Your device must be connected to an external display to mirror your screen + + More options + Cast @@ -7268,6 +7299,8 @@ + + diff --git a/res/xml/external_display_resolution_settings.xml b/res/xml/external_display_resolution_settings.xml new file mode 100644 index 00000000000..6ac6b1ad52e --- /dev/null +++ b/res/xml/external_display_resolution_settings.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/res/xml/external_display_settings.xml b/res/xml/external_display_settings.xml new file mode 100644 index 00000000000..00472115e0c --- /dev/null +++ b/res/xml/external_display_settings.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/src/com/android/settings/SettingsPreferenceFragmentBase.java b/src/com/android/settings/SettingsPreferenceFragmentBase.java new file mode 100644 index 00000000000..dd2e28784ac --- /dev/null +++ b/src/com/android/settings/SettingsPreferenceFragmentBase.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings; + +import android.app.Activity; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settingslib.search.Indexable; + +/** + * Base class for fragment suitable for unit testing. + */ +public abstract class SettingsPreferenceFragmentBase extends SettingsPreferenceFragment + implements Indexable { + @Override + @SuppressWarnings({"RequiresNullabilityAnnotation"}) + public void onCreate(final Bundle icicle) { + super.onCreate(icicle); + onCreateCallback(icicle); + } + + @Override + @SuppressWarnings({"RequiresNullabilityAnnotation"}) + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + onActivityCreatedCallback(savedInstanceState); + } + + @Override + public void onSaveInstanceState(@NonNull final Bundle outState) { + super.onSaveInstanceState(outState); + onSaveInstanceStateCallback(outState); + } + + @Override + public void onStart() { + super.onStart(); + onStartCallback(); + } + + @Override + public void onStop() { + super.onStop(); + onStopCallback(); + } + + protected Activity getCurrentActivity() { + return getActivity(); + } + + /** + * Callback called from {@link #onCreate} + */ + public abstract void onCreateCallback(@Nullable Bundle icicle); + + /** + * Callback called from {@link #onActivityCreated} + */ + public abstract void onActivityCreatedCallback(@Nullable Bundle savedInstanceState); + + /** + * Callback called from {@link #onStart} + */ + public abstract void onStartCallback(); + + /** + * Callback called from {@link #onStop} + */ + public abstract void onStopCallback(); + + /** + * Callback called from {@link #onSaveInstanceState} + */ + public void onSaveInstanceStateCallback(@NonNull final Bundle outState) { + // Do nothing. + } +} diff --git a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java index 56a3005f6dd..2548b95aac4 100644 --- a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java +++ b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java @@ -15,6 +15,8 @@ */ package com.android.settings.connecteddevice; +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isExternalDisplaySettingsPageEnabled; + import android.content.Context; import android.content.pm.PackageManager; import android.hardware.input.InputManager; @@ -22,6 +24,8 @@ import android.util.FeatureFlagUtils; import android.util.Log; import android.view.InputDevice; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; @@ -31,12 +35,15 @@ import com.android.settings.R; import com.android.settings.bluetooth.BluetoothDeviceUpdater; import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater; import com.android.settings.bluetooth.Utils; +import com.android.settings.connecteddevice.display.ExternalDisplayUpdater; import com.android.settings.connecteddevice.dock.DockUpdater; import com.android.settings.connecteddevice.stylus.StylusDeviceUpdater; import com.android.settings.connecteddevice.usb.ConnectedUsbDeviceUpdater; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.flags.FeatureFlags; +import com.android.settings.flags.FeatureFlagsImpl; import com.android.settings.flags.Flags; import com.android.settings.overlay.DockUpdaterFeatureProvider; import com.android.settings.overlay.FeatureFactory; @@ -64,6 +71,8 @@ public class ConnectedDeviceGroupController extends BasePreferenceController @VisibleForTesting PreferenceGroup mPreferenceGroup; + @Nullable + private ExternalDisplayUpdater mExternalDisplayUpdater; private BluetoothDeviceUpdater mBluetoothDeviceUpdater; private ConnectedUsbDeviceUpdater mConnectedUsbDeviceUpdater; private DockUpdater mConnectedDockUpdater; @@ -71,6 +80,8 @@ public class ConnectedDeviceGroupController extends BasePreferenceController private final PackageManager mPackageManager; private final InputManager mInputManager; private final LocalBluetoothManager mLocalBluetoothManager; + @NonNull + private final FeatureFlags mFeatureFlags = new FeatureFlagsImpl(); public ConnectedDeviceGroupController(Context context) { super(context, KEY); @@ -81,6 +92,10 @@ public class ConnectedDeviceGroupController extends BasePreferenceController @Override public void onStart() { + if (mExternalDisplayUpdater != null) { + mExternalDisplayUpdater.registerCallback(); + } + if (mBluetoothDeviceUpdater != null) { mBluetoothDeviceUpdater.registerCallback(); mBluetoothDeviceUpdater.refreshPreference(); @@ -101,6 +116,10 @@ public class ConnectedDeviceGroupController extends BasePreferenceController @Override public void onStop() { + if (mExternalDisplayUpdater != null) { + mExternalDisplayUpdater.unregisterCallback(); + } + if (mBluetoothDeviceUpdater != null) { mBluetoothDeviceUpdater.unregisterCallback(); } @@ -127,6 +146,10 @@ public class ConnectedDeviceGroupController extends BasePreferenceController if (isAvailable()) { final Context context = screen.getContext(); + if (mExternalDisplayUpdater != null) { + mExternalDisplayUpdater.initPreference(context); + } + if (mBluetoothDeviceUpdater != null) { mBluetoothDeviceUpdater.setPrefContext(context); mBluetoothDeviceUpdater.forceUpdate(); @@ -150,7 +173,8 @@ public class ConnectedDeviceGroupController extends BasePreferenceController @Override public int getAvailabilityStatus() { - return (hasBluetoothFeature() + return (hasExternalDisplayFeature() + || hasBluetoothFeature() || hasUsbFeature() || hasUsiStylusFeature() || mConnectedDockUpdater != null) @@ -180,11 +204,13 @@ public class ConnectedDeviceGroupController extends BasePreferenceController } @VisibleForTesting - void init(BluetoothDeviceUpdater bluetoothDeviceUpdater, + void init(@Nullable ExternalDisplayUpdater externalDisplayUpdater, + BluetoothDeviceUpdater bluetoothDeviceUpdater, ConnectedUsbDeviceUpdater connectedUsbDeviceUpdater, DockUpdater connectedDockUpdater, StylusDeviceUpdater connectedStylusDeviceUpdater) { + mExternalDisplayUpdater = externalDisplayUpdater; mBluetoothDeviceUpdater = bluetoothDeviceUpdater; mConnectedUsbDeviceUpdater = connectedUsbDeviceUpdater; mConnectedDockUpdater = connectedDockUpdater; @@ -197,7 +223,10 @@ public class ConnectedDeviceGroupController extends BasePreferenceController FeatureFactory.getFeatureFactory().getDockUpdaterFeatureProvider(); final DockUpdater connectedDockUpdater = dockUpdaterFeatureProvider.getConnectedDockUpdater(context, this); - init(hasBluetoothFeature() + init(hasExternalDisplayFeature() + ? new ExternalDisplayUpdater(this, fragment.getMetricsCategory()) + : null, + hasBluetoothFeature() ? new ConnectedBluetoothDeviceUpdater(context, this, fragment.getMetricsCategory()) : null, @@ -210,6 +239,19 @@ public class ConnectedDeviceGroupController extends BasePreferenceController : null); } + /** + * @return trunk stable feature flags. + */ + @VisibleForTesting + @NonNull + public FeatureFlags getFeatureFlags() { + return mFeatureFlags; + } + + private boolean hasExternalDisplayFeature() { + return isExternalDisplaySettingsPageEnabled(getFeatureFlags()); + } + private boolean hasBluetoothFeature() { return mPackageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); } diff --git a/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java b/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java new file mode 100644 index 00000000000..09f8e92ea49 --- /dev/null +++ b/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java @@ -0,0 +1,544 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.display; + + +import static android.view.Display.INVALID_DISPLAY; + +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_HELP_URL; +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DISPLAY_ID_ARG; +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE; +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplayAllowed; +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isUseDisplaySettingEnabled; +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isResolutionSettingEnabled; +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isRotationSettingEnabled; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.view.Display; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragmentBase; +import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener; +import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.Indexable; +import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.FooterPreference; +import com.android.settingslib.widget.IllustrationPreference; +import com.android.settingslib.widget.MainSwitchPreference; +import com.android.settingslib.widget.TwoTargetPreference; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * The Settings screen for External Displays configuration and connection management. + */ +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) +public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmentBase + implements Indexable { + static final int EXTERNAL_DISPLAY_SETTINGS_RESOURCE = R.xml.external_display_settings; + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(EXTERNAL_DISPLAY_SETTINGS_RESOURCE); + static final String DISPLAYS_LIST_PREFERENCE_KEY = "displays_list_preference"; + static final String EXTERNAL_DISPLAY_USE_PREFERENCE_KEY = "external_display_use_preference"; + static final String EXTERNAL_DISPLAY_ROTATION_KEY = "external_display_rotation"; + static final String EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY = "external_display_resolution"; + static final int EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE = + R.string.external_display_change_resolution_footer_title; + static final int EXTERNAL_DISPLAY_LANDSCAPE_DRAWABLE = + R.drawable.external_display_mirror_landscape; + static final int EXTERANAL_DISPLAY_TITLE_RESOURCE = + R.string.external_display_settings_title; + static final int EXTERNAL_DISPLAY_USE_TITLE_RESOURCE = + R.string.external_display_use_title; + static final int EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE = + R.string.external_display_not_found_footer_title; + static final int EXTERNAL_DISPLAY_PORTRAIT_DRAWABLE = + R.drawable.external_display_mirror_portrait; + static final int EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE = + R.string.external_display_rotation; + static final int EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE = + R.string.external_display_resolution_settings_title; + @VisibleForTesting + static final String PREVIOUSLY_SHOWN_LIST_KEY = "mPreviouslyShownListOfDisplays"; + private boolean mStarted; + @Nullable + private MainSwitchPreference mUseDisplayPref; + @Nullable + private IllustrationPreference mImagePreference; + @Nullable + private Preference mResolutionPreference; + @Nullable + private ListPreference mRotationPref; + @Nullable + private FooterPreference mFooterPreference; + @Nullable + private PreferenceCategory mDisplaysPreference; + @Nullable + private Injector mInjector; + @Nullable + private String[] mRotationEntries; + @Nullable + private String[] mRotationEntriesValues; + @NonNull + private final Runnable mUpdateRunnable = this::update; + private final DisplayListener mListener = new DisplayListener() { + @Override + public void update(int displayId) { + scheduleUpdate(); + } + }; + private boolean mPreviouslyShownListOfDisplays; + + public ExternalDisplayPreferenceFragment() {} + + @VisibleForTesting + ExternalDisplayPreferenceFragment(@NonNull Injector injector) { + mInjector = injector; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY; + } + + @Override + public int getHelpResource() { + return EXTERNAL_DISPLAY_HELP_URL; + } + + @Override + public void onSaveInstanceStateCallback(@NonNull Bundle outState) { + outState.putSerializable(PREVIOUSLY_SHOWN_LIST_KEY, + (Serializable) mPreviouslyShownListOfDisplays); + } + + @Override + public void onCreateCallback(@Nullable Bundle icicle) { + if (mInjector == null) { + mInjector = new Injector(getPrefContext()); + } + addPreferencesFromResource(EXTERNAL_DISPLAY_SETTINGS_RESOURCE); + } + + @Override + public void onActivityCreatedCallback(@Nullable Bundle savedInstanceState) { + restoreState(savedInstanceState); + View view = getView(); + TextView emptyView = null; + if (view != null) { + emptyView = (TextView) view.findViewById(android.R.id.empty); + } + if (emptyView != null) { + emptyView.setText(EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE); + setEmptyView(emptyView); + } + } + + @Override + public void onStartCallback() { + mStarted = true; + if (mInjector == null) { + return; + } + mInjector.registerDisplayListener(mListener); + scheduleUpdate(); + } + + @Override + public void onStopCallback() { + mStarted = false; + if (mInjector == null) { + return; + } + mInjector.unregisterDisplayListener(mListener); + unscheduleUpdate(); + } + + /** + * @return id of the preference. + */ + @Override + protected int getPreferenceScreenResId() { + return EXTERNAL_DISPLAY_SETTINGS_RESOURCE; + } + + @VisibleForTesting + protected void launchResolutionSelector(@NonNull final Context context, final int displayId) { + final Bundle args = new Bundle(); + args.putInt(DISPLAY_ID_ARG, displayId); + new SubSettingLauncher(context) + .setDestination(ResolutionPreferenceFragment.class.getName()) + .setArguments(args) + .setSourceMetricsCategory(getMetricsCategory()).launch(); + } + + @VisibleForTesting + protected void launchDisplaySettings(final int displayId) { + final Bundle args = new Bundle(); + var context = getPrefContext(); + args.putInt(DISPLAY_ID_ARG, displayId); + new SubSettingLauncher(context) + .setDestination(this.getClass().getName()) + .setArguments(args) + .setSourceMetricsCategory(getMetricsCategory()).launch(); + } + + /** + * Returns the preference for the footer. + */ + @NonNull + @VisibleForTesting + FooterPreference getFooterPreference(@NonNull Context context) { + if (mFooterPreference == null) { + mFooterPreference = new FooterPreference(context); + mFooterPreference.setPersistent(false); + } + return mFooterPreference; + } + + @NonNull + @VisibleForTesting + ListPreference getRotationPreference(@NonNull Context context) { + if (mRotationPref == null) { + mRotationPref = new ListPreference(context); + mRotationPref.setPersistent(false); + } + return mRotationPref; + } + + @NonNull + @VisibleForTesting + Preference getResolutionPreference(@NonNull Context context) { + if (mResolutionPreference == null) { + mResolutionPreference = new Preference(context); + mResolutionPreference.setPersistent(false); + } + return mResolutionPreference; + } + + @NonNull + @VisibleForTesting + MainSwitchPreference getUseDisplayPreference(@NonNull Context context) { + if (mUseDisplayPref == null) { + mUseDisplayPref = new MainSwitchPreference(context); + mUseDisplayPref.setPersistent(false); + } + return mUseDisplayPref; + } + + @NonNull + @VisibleForTesting + IllustrationPreference getIllustrationPreference(@NonNull Context context) { + if (mImagePreference == null) { + mImagePreference = new IllustrationPreference(context); + mImagePreference.setPersistent(false); + } + return mImagePreference; + } + + /** + * @return return display id argument of this settings page. + */ + @VisibleForTesting + protected int getDisplayIdArg() { + var args = getArguments(); + return args != null ? args.getInt(DISPLAY_ID_ARG, INVALID_DISPLAY) : INVALID_DISPLAY; + } + + @NonNull + private PreferenceCategory getDisplaysListPreference(@NonNull Context context) { + if (mDisplaysPreference == null) { + mDisplaysPreference = new PreferenceCategory(context); + mDisplaysPreference.setPersistent(false); + } + return mDisplaysPreference; + } + + private void restoreState(@Nullable Bundle savedInstanceState) { + if (savedInstanceState == null) { + return; + } + mPreviouslyShownListOfDisplays = Boolean.TRUE.equals(savedInstanceState.getSerializable( + PREVIOUSLY_SHOWN_LIST_KEY, Boolean.class)); + } + + private void update() { + final var screen = getPreferenceScreen(); + if (screen == null || mInjector == null || mInjector.getContext() == null) { + return; + } + screen.removeAll(); + updateScreenForDisplayId(getDisplayIdArg(), screen, mInjector.getContext()); + } + + private void updateScreenForDisplayId(final int displayId, + @NonNull final PreferenceScreen screen, @NonNull Context context) { + final var displaysToShow = getDisplaysToShow(displayId); + if (displaysToShow.isEmpty() && displayId == INVALID_DISPLAY) { + showTextWhenNoDisplaysToShow(screen, context); + } else if (displaysToShow.size() == 1 + && ((displayId == INVALID_DISPLAY && !mPreviouslyShownListOfDisplays) + || displaysToShow.get(0).getDisplayId() == displayId)) { + showDisplaySettings(displaysToShow.get(0), screen, context); + } else if (displayId == INVALID_DISPLAY) { + // If ever shown a list of displays - keep showing it for consistency after + // disconnecting one of the displays, and only one display is left. + mPreviouslyShownListOfDisplays = true; + showDisplaysList(displaysToShow, screen, context); + } + updateSettingsTitle(displaysToShow, displayId); + } + + private void updateSettingsTitle(@NonNull final List displaysToShow, int displayId) { + final Activity activity = getCurrentActivity(); + if (activity == null) { + return; + } + if (displaysToShow.size() == 1 && displaysToShow.get(0).getDisplayId() == displayId) { + var displayName = displaysToShow.get(0).getName(); + if (!displayName.isEmpty()) { + activity.setTitle(displayName.substring(0, Math.min(displayName.length(), 40))); + return; + } + } + activity.setTitle(EXTERANAL_DISPLAY_TITLE_RESOURCE); + } + + private void showTextWhenNoDisplaysToShow(@NonNull final PreferenceScreen screen, + @NonNull Context context) { + if (isUseDisplaySettingEnabled(mInjector)) { + screen.addPreference(updateUseDisplayPreferenceNoDisplaysFound(context)); + } + screen.addPreference(updateFooterPreference(context, + EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE)); + } + + private void showDisplaySettings(@NonNull Display display, @NonNull PreferenceScreen screen, + @NonNull Context context) { + final var isEnabled = mInjector != null && mInjector.isDisplayEnabled(display); + if (isUseDisplaySettingEnabled(mInjector)) { + screen.addPreference(updateUseDisplayPreference(context, display, isEnabled)); + } + if (!isEnabled) { + // Skip all other settings + return; + } + final var displayRotation = getDisplayRotation(display.getDisplayId()); + screen.addPreference(updateIllustrationImage(context, displayRotation)); + screen.addPreference(updateResolutionPreference(context, display)); + screen.addPreference(updateRotationPreference(context, display, displayRotation)); + if (isResolutionSettingEnabled(mInjector)) { + screen.addPreference(updateFooterPreference(context, + EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE)); + } + } + + private void showDisplaysList(@NonNull List displaysToShow, + @NonNull PreferenceScreen screen, @NonNull Context context) { + var pref = getDisplaysListPreference(context); + pref.setKey(DISPLAYS_LIST_PREFERENCE_KEY); + pref.removeAll(); + if (!displaysToShow.isEmpty()) { + screen.addPreference(pref); + } + for (var display : displaysToShow) { + pref.addPreference(new DisplayPreference(context, display)); + } + } + + private List getDisplaysToShow(int displayIdToShow) { + if (mInjector == null) { + return List.of(); + } + if (displayIdToShow != INVALID_DISPLAY) { + var display = mInjector.getDisplay(displayIdToShow); + if (display != null && isDisplayAllowed(display, mInjector)) { + return List.of(display); + } + } + var displaysToShow = new ArrayList(); + for (var display : mInjector.getAllDisplays()) { + if (display != null && isDisplayAllowed(display, mInjector)) { + displaysToShow.add(display); + } + } + return displaysToShow; + } + + private Preference updateUseDisplayPreferenceNoDisplaysFound(@NonNull Context context) { + final var pref = getUseDisplayPreference(context); + pref.setKey(EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); + pref.setTitle(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE); + pref.setChecked(false); + pref.setEnabled(false); + pref.setOnPreferenceChangeListener(null); + return pref; + } + + private Preference updateUseDisplayPreference(@NonNull final Context context, + @NonNull final Display display, boolean isEnabled) { + final var pref = getUseDisplayPreference(context); + pref.setKey(EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); + pref.setTitle(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE); + pref.setChecked(isEnabled); + pref.setEnabled(true); + pref.setOnPreferenceChangeListener((p, newValue) -> { + writePreferenceClickMetric(p); + final boolean result; + if (mInjector == null) { + return false; + } + if ((Boolean) newValue) { + result = mInjector.enableConnectedDisplay(display.getDisplayId()); + } else { + result = mInjector.disableConnectedDisplay(display.getDisplayId()); + } + if (result) { + pref.setChecked((Boolean) newValue); + } + return result; + }); + return pref; + } + + private Preference updateIllustrationImage(@NonNull final Context context, + final int displayRotation) { + var pref = getIllustrationPreference(context); + if (displayRotation % 2 == 0) { + pref.setLottieAnimationResId(EXTERNAL_DISPLAY_PORTRAIT_DRAWABLE); + } else { + pref.setLottieAnimationResId(EXTERNAL_DISPLAY_LANDSCAPE_DRAWABLE); + } + return pref; + } + + private Preference updateFooterPreference(@NonNull final Context context, final int title) { + var pref = getFooterPreference(context); + pref.setTitle(title); + return pref; + } + + private Preference updateRotationPreference(@NonNull final Context context, + @NonNull final Display display, final int displayRotation) { + var pref = getRotationPreference(context); + pref.setKey(EXTERNAL_DISPLAY_ROTATION_KEY); + pref.setTitle(EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE); + if (mRotationEntries == null || mRotationEntriesValues == null) { + mRotationEntries = new String[] { + context.getString(R.string.external_display_standard_rotation), + context.getString(R.string.external_display_rotation_90), + context.getString(R.string.external_display_rotation_180), + context.getString(R.string.external_display_rotation_270)}; + mRotationEntriesValues = new String[] {"0", "1", "2", "3"}; + } + pref.setEntries(mRotationEntries); + pref.setEntryValues(mRotationEntriesValues); + pref.setValueIndex(displayRotation); + pref.setSummary(mRotationEntries[displayRotation]); + pref.setOnPreferenceChangeListener((p, newValue) -> { + writePreferenceClickMetric(p); + var rotation = Integer.parseInt((String) newValue); + var displayId = display.getDisplayId(); + if (mInjector == null || !mInjector.freezeDisplayRotation(displayId, rotation)) { + return false; + } + pref.setValueIndex(rotation); + return true; + }); + pref.setEnabled(isRotationSettingEnabled(mInjector)); + return pref; + } + + private Preference updateResolutionPreference(@NonNull final Context context, + @NonNull final Display display) { + var pref = getResolutionPreference(context); + pref.setKey(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + pref.setTitle(EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE); + pref.setSummary(display.getMode().getPhysicalWidth() + " x " + + display.getMode().getPhysicalHeight()); + pref.setOnPreferenceClickListener((Preference p) -> { + writePreferenceClickMetric(p); + launchResolutionSelector(context, display.getDisplayId()); + return true; + }); + pref.setEnabled(isResolutionSettingEnabled(mInjector)); + return pref; + } + + private int getDisplayRotation(int displayId) { + if (mInjector == null) { + return 0; + } + return Math.min(3, Math.max(0, mInjector.getDisplayUserRotation(displayId))); + } + + private void scheduleUpdate() { + if (mInjector == null || !mStarted) { + return; + } + unscheduleUpdate(); + mInjector.getHandler().post(mUpdateRunnable); + } + + private void unscheduleUpdate() { + if (mInjector == null || !mStarted) { + return; + } + mInjector.getHandler().removeCallbacks(mUpdateRunnable); + } + + @VisibleForTesting + class DisplayPreference extends TwoTargetPreference + implements Preference.OnPreferenceClickListener { + private final int mDisplayId; + + DisplayPreference(@NonNull final Context context, @NonNull final Display display) { + super(context); + mDisplayId = display.getDisplayId(); + setPersistent(false); + setKey("display_id_" + mDisplayId); + setTitle(display.getName()); + setSummary(display.getMode().getPhysicalWidth() + " x " + + display.getMode().getPhysicalHeight()); + setOnPreferenceClickListener(this); + } + + @Override + public boolean onPreferenceClick(@NonNull Preference preference) { + launchDisplaySettings(mDisplayId); + writePreferenceClickMetric(preference); + return true; + } + } +} diff --git a/src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java b/src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java new file mode 100644 index 00000000000..89d464c9a4e --- /dev/null +++ b/src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.display; + +import static android.content.Context.DISPLAY_SERVICE; +import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED; +import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_ADDED; +import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED; +import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED; +import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_REMOVED; +import static android.view.Display.INVALID_DISPLAY; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManagerGlobal; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.view.Display; +import android.view.Display.Mode; +import android.view.IWindowManager; +import android.view.WindowManagerGlobal; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settings.R; +import com.android.settings.flags.FeatureFlags; +import com.android.settings.flags.FeatureFlagsImpl; + +public class ExternalDisplaySettingsConfiguration { + static final String VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY = + "persist.demo.userrotation.package_name"; + static final String DISPLAY_ID_ARG = "display_id"; + static final int EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE = R.string.external_display_not_found; + static final int EXTERNAL_DISPLAY_HELP_URL = R.string.help_url_external_display; + + public static class SystemServicesProvider { + @Nullable + private IWindowManager mWindowManager; + @Nullable + private DisplayManager mDisplayManager; + @Nullable + protected Context mContext; + /** + * @param name of a system property. + * @return the value of the system property. + */ + @NonNull + public String getSystemProperty(@NonNull String name) { + return SystemProperties.get(name); + } + + /** + * @return return public Display manager. + */ + @Nullable + public DisplayManager getDisplayManager() { + if (mDisplayManager == null && getContext() != null) { + mDisplayManager = (DisplayManager) getContext().getSystemService(DISPLAY_SERVICE); + } + return mDisplayManager; + } + + /** + * @return internal IWindowManager + */ + @Nullable + public IWindowManager getWindowManager() { + if (mWindowManager == null) { + mWindowManager = WindowManagerGlobal.getWindowManagerService(); + } + return mWindowManager; + } + + /** + * @return context. + */ + @Nullable + public Context getContext() { + return mContext; + } + } + + public static class Injector extends SystemServicesProvider { + @NonNull + private final FeatureFlags mFlags; + @NonNull + private final Handler mHandler; + + Injector(@Nullable Context context) { + this(context, new FeatureFlagsImpl(), new Handler(Looper.getMainLooper())); + } + + Injector(@Nullable Context context, @NonNull FeatureFlags flags, @NonNull Handler handler) { + mContext = context; + mFlags = flags; + mHandler = handler; + } + + /** + * @return all displays including disabled. + */ + @NonNull + public Display[] getAllDisplays() { + var dm = getDisplayManager(); + if (dm == null) { + return new Display[0]; + } + return dm.getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED); + } + + /** + * @return enabled displays only. + */ + @NonNull + public Display[] getEnabledDisplays() { + var dm = getDisplayManager(); + if (dm == null) { + return new Display[0]; + } + return dm.getDisplays(); + } + + /** + * @return true if the display is enabled + */ + public boolean isDisplayEnabled(@NonNull Display display) { + for (var enabledDisplay : getEnabledDisplays()) { + if (enabledDisplay.getDisplayId() == display.getDisplayId()) { + return true; + } + } + return false; + } + + /** + * Register display listener. + */ + public void registerDisplayListener(@NonNull DisplayManager.DisplayListener listener) { + var dm = getDisplayManager(); + if (dm == null) { + return; + } + dm.registerDisplayListener(listener, mHandler, EVENT_FLAG_DISPLAY_ADDED + | EVENT_FLAG_DISPLAY_CHANGED | EVENT_FLAG_DISPLAY_REMOVED + | EVENT_FLAG_DISPLAY_CONNECTION_CHANGED); + } + + /** + * Unregister display listener. + */ + public void unregisterDisplayListener(@NonNull DisplayManager.DisplayListener listener) { + var dm = getDisplayManager(); + if (dm == null) { + return; + } + dm.unregisterDisplayListener(listener); + } + + /** + * @return feature flags. + */ + @NonNull + public FeatureFlags getFlags() { + return mFlags; + } + + /** + * Enable connected display. + */ + public boolean enableConnectedDisplay(int displayId) { + var dm = getDisplayManager(); + if (dm == null) { + return false; + } + dm.enableConnectedDisplay(displayId); + return true; + } + + /** + * Disable connected display. + */ + public boolean disableConnectedDisplay(int displayId) { + var dm = getDisplayManager(); + if (dm == null) { + return false; + } + dm.disableConnectedDisplay(displayId); + return true; + } + + /** + * @param displayId which must be returned + * @return display object for the displayId + */ + @Nullable + public Display getDisplay(int displayId) { + if (displayId == INVALID_DISPLAY) { + return null; + } + var dm = getDisplayManager(); + if (dm == null) { + return null; + } + return dm.getDisplay(displayId); + } + + /** + * @return handler + */ + @NonNull + public Handler getHandler() { + return mHandler; + } + + /** + * Get display rotation + * @param displayId display identifier + * @return rotation + */ + public int getDisplayUserRotation(int displayId) { + var wm = getWindowManager(); + if (wm == null) { + return 0; + } + try { + return wm.getDisplayUserRotation(displayId); + } catch (RemoteException e) { + return 0; + } + } + + /** + * Freeze rotation of the display in the specified rotation. + * @param displayId display identifier + * @param rotation [0, 1, 2, 3] + * @return true if successful + */ + public boolean freezeDisplayRotation(int displayId, int rotation) { + var wm = getWindowManager(); + if (wm == null) { + return false; + } + try { + wm.freezeDisplayRotation(displayId, rotation, + "ExternalDisplayPreferenceFragment"); + return true; + } catch (RemoteException e) { + return false; + } + } + + /** + * Enforce display mode on the given display. + */ + public void setUserPreferredDisplayMode(int displayId, @NonNull Mode mode) { + DisplayManagerGlobal.getInstance().setUserPreferredDisplayMode(displayId, mode); + } + } + + public abstract static class DisplayListener implements DisplayManager.DisplayListener { + @Override + public void onDisplayAdded(int displayId) { + update(displayId); + } + + @Override + public void onDisplayRemoved(int displayId) { + update(displayId); + } + + @Override + public void onDisplayChanged(int displayId) { + update(displayId); + } + + @Override + public void onDisplayConnected(int displayId) { + update(displayId); + } + + @Override + public void onDisplayDisconnected(int displayId) { + update(displayId); + } + + /** + * Called from other listener methods to trigger update of the settings page. + */ + public abstract void update(int displayId); + } + + /** + * @return whether the settings page is enabled or not. + */ + public static boolean isExternalDisplaySettingsPageEnabled(@NonNull FeatureFlags flags) { + return flags.rotationConnectedDisplaySetting() + || flags.resolutionAndEnableConnectedDisplaySetting(); + } + + static boolean isDisplayAllowed(@NonNull Display display, + @NonNull SystemServicesProvider props) { + return display.getType() == Display.TYPE_EXTERNAL + || display.getType() == Display.TYPE_OVERLAY + || isVirtualDisplayAllowed(display, props); + } + + static boolean isVirtualDisplayAllowed(@NonNull Display display, + @NonNull SystemServicesProvider properties) { + var sysProp = properties.getSystemProperty(VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY); + return !sysProp.isEmpty() && display.getType() == Display.TYPE_VIRTUAL + && sysProp.equals(display.getOwnerPackageName()); + } + + static boolean isUseDisplaySettingEnabled(@Nullable Injector injector) { + return injector != null && injector.getFlags().resolutionAndEnableConnectedDisplaySetting(); + } + + static boolean isResolutionSettingEnabled(@Nullable Injector injector) { + return injector != null && injector.getFlags().resolutionAndEnableConnectedDisplaySetting(); + } + + static boolean isRotationSettingEnabled(@Nullable Injector injector) { + return injector != null && injector.getFlags().rotationConnectedDisplaySetting(); + } +} diff --git a/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdater.java b/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdater.java new file mode 100644 index 00000000000..64dd7bb2fdf --- /dev/null +++ b/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdater.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.display; + +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplayAllowed; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.connecteddevice.DevicePreferenceCallback; +import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener; +import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.RestrictedPreference; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +public class ExternalDisplayUpdater { + + private static final String PREF_KEY = "external_display_settings"; + private final int mMetricsCategory; + @NonNull + private final MetricsFeatureProvider mMetricsFeatureProvider; + @NonNull + private final Runnable mUpdateRunnable = this::update; + @NonNull + private final DevicePreferenceCallback mDevicePreferenceCallback; + @Nullable + private RestrictedPreference mPreference; + @Nullable + private Injector mInjector; + private final DisplayListener mListener = new DisplayListener() { + @Override + public void update(int displayId) { + scheduleUpdate(); + } + }; + + public ExternalDisplayUpdater(@NonNull DevicePreferenceCallback callback, int metricsCategory) { + mDevicePreferenceCallback = callback; + mMetricsCategory = metricsCategory; + mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); + } + + /** + * Set the context to generate the {@link Preference}, so it could get the correct theme. + */ + public void initPreference(@NonNull Context context) { + initPreference(context, new Injector(context)); + } + + @VisibleForTesting + void initPreference(@NonNull Context context, Injector injector) { + mInjector = injector; + mPreference = new RestrictedPreference(context, null /* AttributeSet */); + mPreference.setTitle(R.string.external_display_settings_title); + mPreference.setSummary(getSummary()); + mPreference.setIcon(getDrawable(context)); + mPreference.setKey(PREF_KEY); + mPreference.setDisabledByAdmin(checkIfUsbDataSignalingIsDisabled(context)); + mPreference.setOnPreferenceClickListener((Preference p) -> { + mMetricsFeatureProvider.logClickedPreference(p, mMetricsCategory); + // New version - uses a separate screen. + new SubSettingLauncher(context) + .setDestination(ExternalDisplayPreferenceFragment.class.getName()) + .setTitleRes(R.string.external_display_settings_title) + .setSourceMetricsCategory(mMetricsCategory) + .launch(); + return true; + }); + + scheduleUpdate(); + } + + /** + * Unregister the display listener. + */ + public void unregisterCallback() { + if (mInjector != null) { + mInjector.unregisterDisplayListener(mListener); + } + } + + /** + * Register the display listener. + */ + public void registerCallback() { + if (mInjector != null) { + mInjector.registerDisplayListener(mListener); + } + } + + @VisibleForTesting + @Nullable + protected RestrictedLockUtils.EnforcedAdmin checkIfUsbDataSignalingIsDisabled(Context context) { + return RestrictedLockUtilsInternal.checkIfUsbDataSignalingIsDisabled(context, + UserHandle.myUserId()); + } + + @VisibleForTesting + @Nullable + protected Drawable getDrawable(Context context) { + return context.getDrawable(R.drawable.ic_external_display_32dp); + } + + @Nullable + protected CharSequence getSummary() { + if (mInjector == null) { + return null; + } + var context = mInjector.getContext(); + if (context == null) { + return null; + } + + for (var display : mInjector.getEnabledDisplays()) { + if (display != null && isDisplayAllowed(display, mInjector)) { + return context.getString(R.string.external_display_on); + } + } + + for (var display : mInjector.getAllDisplays()) { + if (display != null && isDisplayAllowed(display, mInjector)) { + return context.getString(R.string.external_display_off); + } + } + + return null; + } + + private void scheduleUpdate() { + if (mInjector == null) { + return; + } + unscheduleUpdate(); + mInjector.getHandler().post(mUpdateRunnable); + } + + private void unscheduleUpdate() { + if (mInjector == null) { + return; + } + mInjector.getHandler().removeCallbacks(mUpdateRunnable); + } + + private void update() { + var summary = getSummary(); + if (mPreference == null) { + return; + } + mPreference.setSummary(summary); + if (summary != null) { + mDevicePreferenceCallback.onDeviceAdded(mPreference); + } else { + mDevicePreferenceCallback.onDeviceRemoved(mPreference); + } + } +} diff --git a/src/com/android/settings/connecteddevice/display/OWNERS b/src/com/android/settings/connecteddevice/display/OWNERS new file mode 100644 index 00000000000..78aecb9dcdf --- /dev/null +++ b/src/com/android/settings/connecteddevice/display/OWNERS @@ -0,0 +1,7 @@ +# Default reviewers for this and subdirectories. +santoscordon@google.com +petsjonkin@google.com +flc@google.com +wilczynskip@google.com +brup@google.com +olb@google.com diff --git a/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragment.java b/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragment.java new file mode 100644 index 00000000000..10314cb1e21 --- /dev/null +++ b/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragment.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.connecteddevice.display; + +import static android.view.Display.INVALID_DISPLAY; + +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DISPLAY_ID_ARG; +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_HELP_URL; +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE; +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplayAllowed; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.util.Log; +import android.util.Pair; +import android.view.Display; +import android.view.Display.Mode; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + +import com.android.internal.util.ToBooleanFunction; +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragmentBase; +import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener; +import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector; +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +import java.util.ArrayList; +import java.util.HashSet; + +public class ResolutionPreferenceFragment extends SettingsPreferenceFragmentBase { + private static final String TAG = "ResolutionPreferenceFragment"; + static final int DEFAULT_LOW_REFRESH_RATE = 60; + static final String MORE_OPTIONS_KEY = "more_options"; + static final String TOP_OPTIONS_KEY = "top_options"; + static final int MORE_OPTIONS_TITLE_RESOURCE = + R.string.external_display_more_options_title; + static final int EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE = + R.xml.external_display_resolution_settings; + @Nullable + private Injector mInjector; + @Nullable + private PreferenceCategory mTopOptionsPreference; + @Nullable + private PreferenceCategory mMoreOptionsPreference; + private boolean mStarted; + private final HashSet mResolutionPreferences = new HashSet<>(); + private int mExternalDisplayPeakWidth; + private int mExternalDisplayPeakHeight; + private int mExternalDisplayPeakRefreshRate; + private boolean mRefreshRateSynchronizationEnabled; + private boolean mMoreOptionsExpanded; + private final Runnable mUpdateRunnable = this::update; + private final DisplayListener mListener = new DisplayListener() { + @Override + public void update(int displayId) { + scheduleUpdate(); + } + }; + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY; + } + + @Override + public int getHelpResource() { + return EXTERNAL_DISPLAY_HELP_URL; + } + + @Override + public void onCreateCallback(@Nullable Bundle icicle) { + if (mInjector == null) { + mInjector = new Injector(getPrefContext()); + } + addPreferencesFromResource(EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE); + updateDisplayModeLimits(mInjector.getContext()); + } + + @Override + public void onActivityCreatedCallback(@Nullable Bundle savedInstanceState) { + View view = getView(); + TextView emptyView = null; + if (view != null) { + emptyView = (TextView) view.findViewById(android.R.id.empty); + } + if (emptyView != null) { + emptyView.setText(EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE); + setEmptyView(emptyView); + } + } + + @Override + public void onStartCallback() { + mStarted = true; + if (mInjector == null) { + return; + } + mInjector.registerDisplayListener(mListener); + scheduleUpdate(); + } + + @Override + public void onStopCallback() { + mStarted = false; + if (mInjector == null) { + return; + } + mInjector.unregisterDisplayListener(mListener); + unscheduleUpdate(); + } + + public ResolutionPreferenceFragment() {} + + @VisibleForTesting + ResolutionPreferenceFragment(@NonNull Injector injector) { + mInjector = injector; + } + + @VisibleForTesting + protected int getDisplayIdArg() { + var args = getArguments(); + return args != null ? args.getInt(DISPLAY_ID_ARG, INVALID_DISPLAY) : INVALID_DISPLAY; + } + + @VisibleForTesting + @NonNull + protected Resources getResources(@NonNull Context context) { + return context.getResources(); + } + + private void update() { + final PreferenceScreen screen = getPreferenceScreen(); + if (screen == null || mInjector == null) { + return; + } + var context = mInjector.getContext(); + if (context == null) { + return; + } + var display = mInjector.getDisplay(getDisplayIdArg()); + if (display == null || !isDisplayAllowed(display, mInjector)) { + screen.removeAll(); + mTopOptionsPreference = null; + mMoreOptionsPreference = null; + return; + } + mResolutionPreferences.clear(); + var remainingModes = addModePreferences(context, + getTopPreference(context, screen), + display.getSupportedModes(), this::isTopMode, display); + addRemainingPreferences(context, + getMorePreference(context, screen), + display, remainingModes.first, remainingModes.second); + } + + private PreferenceCategory getTopPreference(@NonNull Context context, + @NonNull PreferenceScreen screen) { + if (mTopOptionsPreference == null) { + mTopOptionsPreference = new PreferenceCategory(context); + mTopOptionsPreference.setPersistent(false); + mTopOptionsPreference.setKey(TOP_OPTIONS_KEY); + screen.addPreference(mTopOptionsPreference); + } else { + mTopOptionsPreference.removeAll(); + } + return mTopOptionsPreference; + } + + private PreferenceCategory getMorePreference(@NonNull Context context, + @NonNull PreferenceScreen screen) { + if (mMoreOptionsPreference == null) { + mMoreOptionsPreference = new PreferenceCategory(context); + mMoreOptionsPreference.setPersistent(false); + mMoreOptionsPreference.setTitle(MORE_OPTIONS_TITLE_RESOURCE); + mMoreOptionsPreference.setOnExpandButtonClickListener(() -> { + mMoreOptionsExpanded = true; + }); + mMoreOptionsPreference.setKey(MORE_OPTIONS_KEY); + screen.addPreference(mMoreOptionsPreference); + } else { + mMoreOptionsPreference.removeAll(); + } + return mMoreOptionsPreference; + } + + private void addRemainingPreferences(@NonNull Context context, + @NonNull PreferenceCategory group, @NonNull Display display, + boolean isSelectedModeFound, @NonNull Mode[] moreModes) { + if (moreModes.length == 0) { + return; + } + mMoreOptionsExpanded |= !isSelectedModeFound; + group.setInitialExpandedChildrenCount(mMoreOptionsExpanded ? Integer.MAX_VALUE : 0); + addModePreferences(context, group, moreModes, /*checkMode=*/ null, display); + } + + private Pair addModePreferences(@NonNull Context context, + @NonNull PreferenceGroup group, + @NonNull Mode[] modes, + @Nullable ToBooleanFunction checkMode, + @NonNull Display display) { + Display.Mode curMode = display.getMode(); + var currentResolution = curMode.getPhysicalWidth() + "x" + curMode.getPhysicalHeight(); + var rotatedResolution = curMode.getPhysicalHeight() + "x" + curMode.getPhysicalWidth(); + var skippedModes = new ArrayList(); + var isAnyOfModesSelected = false; + for (var mode : modes) { + var modeStr = mode.getPhysicalWidth() + "x" + mode.getPhysicalHeight(); + SelectorWithWidgetPreference pref = group.findPreference(modeStr); + if (pref != null) { + continue; + } + if (checkMode != null && !checkMode.apply(mode)) { + skippedModes.add(mode); + continue; + } + var isCurrentMode = + currentResolution.equals(modeStr) || rotatedResolution.equals(modeStr); + if (!isCurrentMode && !isAllowedMode(mode)) { + continue; + } + if (mResolutionPreferences.contains(modeStr)) { + // Added to "Top modes" already. + continue; + } + mResolutionPreferences.add(modeStr); + pref = new SelectorWithWidgetPreference(context); + pref.setPersistent(false); + pref.setKey(modeStr); + pref.setTitle(mode.getPhysicalWidth() + " x " + mode.getPhysicalHeight()); + pref.setSingleLineTitle(true); + pref.setOnClickListener(preference -> onDisplayModeClicked(preference, display)); + pref.setChecked(isCurrentMode); + isAnyOfModesSelected |= isCurrentMode; + group.addPreference(pref); + } + return new Pair<>(isAnyOfModesSelected, skippedModes.toArray(Mode.EMPTY_ARRAY)); + } + + private boolean isTopMode(@NonNull Mode mode) { + return mTopOptionsPreference != null + && mTopOptionsPreference.getPreferenceCount() < 3; + } + + private boolean isAllowedMode(@NonNull Mode mode) { + if (mRefreshRateSynchronizationEnabled + && (mode.getRefreshRate() < DEFAULT_LOW_REFRESH_RATE - 1 + || mode.getRefreshRate() > DEFAULT_LOW_REFRESH_RATE + 1)) { + Log.d(TAG, mode + " refresh rate is out of synchronization range"); + return false; + } + if (mExternalDisplayPeakHeight > 0 + && mode.getPhysicalHeight() > mExternalDisplayPeakHeight) { + Log.d(TAG, mode + " height is above the allowed limit"); + return false; + } + if (mExternalDisplayPeakWidth > 0 + && mode.getPhysicalWidth() > mExternalDisplayPeakWidth) { + Log.d(TAG, mode + " width is above the allowed limit"); + return false; + } + if (mExternalDisplayPeakRefreshRate > 0 + && mode.getRefreshRate() > mExternalDisplayPeakRefreshRate) { + Log.d(TAG, mode + " refresh rate is above the allowed limit"); + return false; + } + return true; + } + + private void scheduleUpdate() { + if (mInjector == null || !mStarted) { + return; + } + unscheduleUpdate(); + mInjector.getHandler().post(mUpdateRunnable); + } + + private void unscheduleUpdate() { + if (mInjector == null || !mStarted) { + return; + } + mInjector.getHandler().removeCallbacks(mUpdateRunnable); + } + + private void onDisplayModeClicked(@NonNull SelectorWithWidgetPreference preference, + @NonNull Display display) { + if (mInjector == null) { + return; + } + String[] modeResolution = preference.getKey().split("x"); + int width = Integer.parseInt(modeResolution[0]); + int height = Integer.parseInt(modeResolution[1]); + for (var mode : display.getSupportedModes()) { + if (mode.getPhysicalWidth() == width && mode.getPhysicalHeight() == height + && isAllowedMode(mode)) { + mInjector.setUserPreferredDisplayMode(display.getDisplayId(), mode); + return; + } + } + } + + private void updateDisplayModeLimits(@Nullable Context context) { + if (context == null) { + return; + } + mExternalDisplayPeakRefreshRate = getResources(context).getInteger( + com.android.internal.R.integer.config_externalDisplayPeakRefreshRate); + mExternalDisplayPeakWidth = getResources(context).getInteger( + com.android.internal.R.integer.config_externalDisplayPeakWidth); + mExternalDisplayPeakHeight = getResources(context).getInteger( + com.android.internal.R.integer.config_externalDisplayPeakHeight); + mRefreshRateSynchronizationEnabled = getResources(context).getBoolean( + com.android.internal.R.bool.config_refreshRateSynchronizationEnabled); + Log.d(TAG, "mExternalDisplayPeakRefreshRate=" + mExternalDisplayPeakRefreshRate); + Log.d(TAG, "mExternalDisplayPeakWidth=" + mExternalDisplayPeakWidth); + Log.d(TAG, "mExternalDisplayPeakHeight=" + mExternalDisplayPeakHeight); + Log.d(TAG, "mRefreshRateSynchronizationEnabled=" + mRefreshRateSynchronizationEnabled); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java index d28ab3b928b..5a9f2bc8692 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java @@ -17,6 +17,8 @@ package com.android.settings.connecteddevice; import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE; import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; +import static com.android.settings.flags.Flags.FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING; +import static com.android.settings.flags.Flags.FLAG_ROTATION_CONNECTED_DISPLAY_SETTING; import static com.google.common.truth.Truth.assertThat; @@ -30,6 +32,7 @@ import static org.mockito.Mockito.when; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.hardware.input.InputManager; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; @@ -40,13 +43,16 @@ import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater; import com.android.settings.bluetooth.Utils; +import com.android.settings.connecteddevice.display.ExternalDisplayUpdater; import com.android.settings.connecteddevice.dock.DockUpdater; import com.android.settings.connecteddevice.stylus.StylusDeviceUpdater; import com.android.settings.connecteddevice.usb.ConnectedUsbDeviceUpdater; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.flags.FakeFeatureFlagsImpl; import com.android.settings.flags.Flags; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.testutils.shadow.ShadowBluetoothUtils; @@ -65,7 +71,6 @@ import org.mockito.Answers; 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; import org.robolectric.shadows.ShadowApplicationPackageManager; @@ -84,6 +89,8 @@ public class ConnectedDeviceGroupControllerTest { @Mock private DashboardFragment mDashboardFragment; @Mock + private ExternalDisplayUpdater mExternalDisplayUpdater; + @Mock private ConnectedBluetoothDeviceUpdater mConnectedBluetoothDeviceUpdater; @Mock private ConnectedUsbDeviceUpdater mConnectedUsbDeviceUpdater; @@ -105,6 +112,9 @@ public class ConnectedDeviceGroupControllerTest { private CachedBluetoothDevice mCachedDevice; @Mock private BluetoothDevice mDevice; + @Mock + private Resources mResources; + private final FakeFeatureFlagsImpl mFakeFeatureFlags = new FakeFeatureFlagsImpl(); private ShadowApplicationPackageManager mPackageManager; private PreferenceGroup mPreferenceGroup; @@ -118,8 +128,10 @@ public class ConnectedDeviceGroupControllerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); + mFakeFeatureFlags.setFlag(FLAG_ROTATION_CONNECTED_DISPLAY_SETTING, true); + mFakeFeatureFlags.setFlag(FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING, true); - mContext = spy(RuntimeEnvironment.application); + mContext = spy(ApplicationProvider.getApplicationContext()); mPreference = new Preference(mContext); mPreference.setKey(PREFERENCE_KEY_1); mPackageManager = (ShadowApplicationPackageManager) Shadows.shadowOf( @@ -129,15 +141,19 @@ public class ConnectedDeviceGroupControllerTest { doReturn(mContext).when(mDashboardFragment).getContext(); mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true); when(mContext.getSystemService(InputManager.class)).thenReturn(mInputManager); + when(mContext.getResources()).thenReturn(mResources); when(mInputManager.getInputDeviceIds()).thenReturn(new int[]{}); ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager; mLocalBluetoothManager = Utils.getLocalBtManager(mContext); when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager); - mConnectedDeviceGroupController = new ConnectedDeviceGroupController(mContext); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, - mConnectedUsbDeviceUpdater, mConnectedDockUpdater, mStylusDeviceUpdater); + mConnectedDeviceGroupController = spy(new ConnectedDeviceGroupController(mContext)); + when(mConnectedDeviceGroupController.getFeatureFlags()).thenReturn(mFakeFeatureFlags); + + mConnectedDeviceGroupController.init(mExternalDisplayUpdater, + mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, mConnectedDockUpdater, + mStylusDeviceUpdater); mConnectedDeviceGroupController.mPreferenceGroup = mPreferenceGroup; when(mCachedDevice.getName()).thenReturn(DEVICE_NAME); @@ -147,6 +163,7 @@ public class ConnectedDeviceGroupControllerTest { FeatureFlagUtils.setEnabled(mContext, FeatureFlagUtils.SETTINGS_SHOW_STYLUS_PREFERENCES, true); + when(mPreferenceScreen.getContext()).thenReturn(mContext); } @Test @@ -193,6 +210,7 @@ public class ConnectedDeviceGroupControllerTest { // register the callback in onStart() mConnectedDeviceGroupController.onStart(); + verify(mExternalDisplayUpdater).registerCallback(); verify(mConnectedBluetoothDeviceUpdater).registerCallback(); verify(mConnectedUsbDeviceUpdater).registerCallback(); verify(mConnectedDockUpdater).registerCallback(); @@ -204,6 +222,7 @@ public class ConnectedDeviceGroupControllerTest { public void onStop_shouldUnregisterUpdaters() { // unregister the callback in onStop() mConnectedDeviceGroupController.onStop(); + verify(mExternalDisplayUpdater).unregisterCallback(); verify(mConnectedBluetoothDeviceUpdater).unregisterCallback(); verify(mConnectedUsbDeviceUpdater).unregisterCallback(); verify(mConnectedDockUpdater).unregisterCallback(); @@ -212,22 +231,36 @@ public class ConnectedDeviceGroupControllerTest { @Test public void getAvailabilityStatus_noBluetoothUsbDockFeature_returnUnSupported() { + mFakeFeatureFlags.setFlag(FLAG_ROTATION_CONNECTED_DISPLAY_SETTING, false); + mFakeFeatureFlags.setFlag(FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, null, null); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( UNSUPPORTED_ON_DEVICE); } + @Test + public void getAvailabilityStatus_connectedDisplay_returnSupported() { + mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); + mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); + mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, + mConnectedUsbDeviceUpdater, null, null); + + assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( + AVAILABLE_UNSEARCHABLE); + } + @Test public void getAvailabilityStatus_BluetoothFeature_returnSupported() { mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, null, null); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( @@ -239,7 +272,7 @@ public class ConnectedDeviceGroupControllerTest { mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, true); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, null, null); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( @@ -251,7 +284,7 @@ public class ConnectedDeviceGroupControllerTest { mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, mConnectedDockUpdater, null); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( @@ -261,6 +294,8 @@ public class ConnectedDeviceGroupControllerTest { @Test public void getAvailabilityStatus_noUsiStylusFeature_returnUnSupported() { + mFakeFeatureFlags.setFlag(FLAG_ROTATION_CONNECTED_DISPLAY_SETTING, false); + mFakeFeatureFlags.setFlag(FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); @@ -268,7 +303,7 @@ public class ConnectedDeviceGroupControllerTest { when(mInputManager.getInputDevice(0)).thenReturn(new InputDevice.Builder().setSources( InputDevice.SOURCE_DPAD).setExternal(false).build()); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, null, mStylusDeviceUpdater); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( @@ -284,7 +319,7 @@ public class ConnectedDeviceGroupControllerTest { when(mInputManager.getInputDevice(0)).thenReturn(new InputDevice.Builder().setSources( InputDevice.SOURCE_STYLUS).setExternal(false).build()); - mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, mConnectedDockUpdater, mStylusDeviceUpdater); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp index bc5824f6cb2..55df480e787 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp @@ -33,6 +33,7 @@ android_test { "kotlinx_coroutines_test", "Settings-testutils2", "MediaDrmSettingsFlagsLib", + "servicestests-utils", // Don't add SettingsLib libraries here - you can use them directly as they are in the // instrumented Settings app. ], diff --git a/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java new file mode 100644 index 00000000000..019ade7ae58 --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java @@ -0,0 +1,409 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.display; + + +import static android.view.Display.INVALID_DISPLAY; + +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.PREVIOUSLY_SHOWN_LIST_KEY; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.DISPLAYS_LIST_PREFERENCE_KEY; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_ROTATION_KEY; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_SETTINGS_RESOURCE; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_USE_PREFERENCE_KEY; +import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_USE_TITLE_RESOURCE; +import static com.android.settingslib.widget.FooterPreference.KEY_FOOTER; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.view.Display; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.DisplayPreference; +import com.android.settingslib.widget.FooterPreference; +import com.android.settingslib.widget.MainSwitchPreference; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** Unit tests for {@link ExternalDisplayPreferenceFragment}. */ +@RunWith(AndroidJUnit4.class) +public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBase { + @Nullable + private ExternalDisplayPreferenceFragment mFragment; + private int mPreferenceIdFromResource; + private int mDisplayIdArg = INVALID_DISPLAY; + private int mResolutionSelectorDisplayId = INVALID_DISPLAY; + @Mock + private MetricsLogger mMockedMetricsLogger; + + @Test + @UiThreadTest + public void testCreateAndStart() { + initFragment(); + assertThat(mPreferenceIdFromResource).isEqualTo(EXTERNAL_DISPLAY_SETTINGS_RESOURCE); + } + + @Test + @UiThreadTest + public void testShowDisplayList() { + var fragment = initFragment(); + var outState = new Bundle(); + fragment.onSaveInstanceStateCallback(outState); + assertThat(outState.getBoolean(PREVIOUSLY_SHOWN_LIST_KEY)).isFalse(); + assertThat(mHandler.getPendingMessages().size()).isEqualTo(1); + PreferenceCategory pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + assertThat(pref).isNull(); + verify(mMockedInjector, never()).getAllDisplays(); + mHandler.flush(); + assertThat(mHandler.getPendingMessages().size()).isEqualTo(0); + verify(mMockedInjector).getAllDisplays(); + pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + assertThat(pref).isNotNull(); + assertThat(pref.getPreferenceCount()).isEqualTo(2); + fragment.onSaveInstanceStateCallback(outState); + assertThat(outState.getBoolean(PREVIOUSLY_SHOWN_LIST_KEY)).isTrue(); + } + + @Test + @UiThreadTest + public void testLaunchDisplaySettingFromList() { + initFragment(); + mHandler.flush(); + PreferenceCategory pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + assertThat(pref).isNotNull(); + DisplayPreference display1Pref = (DisplayPreference) pref.getPreference(0); + DisplayPreference display2Pref = (DisplayPreference) pref.getPreference(1); + assertThat(display1Pref.getKey()).isEqualTo("display_id_" + 1); + assertThat("" + display1Pref.getTitle()).isEqualTo("HDMI"); + assertThat("" + display1Pref.getSummary()).isEqualTo("1920 x 1080"); + display1Pref.onPreferenceClick(display1Pref); + assertThat(mDisplayIdArg).isEqualTo(1); + verify(mMockedMetricsLogger).writePreferenceClickMetric(display1Pref); + assertThat(display2Pref.getKey()).isEqualTo("display_id_" + 2); + assertThat("" + display2Pref.getTitle()).isEqualTo("Overlay #1"); + assertThat("" + display2Pref.getSummary()).isEqualTo("1240 x 780"); + display2Pref.onPreferenceClick(display2Pref); + assertThat(mDisplayIdArg).isEqualTo(2); + verify(mMockedMetricsLogger).writePreferenceClickMetric(display2Pref); + } + + @Test + @UiThreadTest + public void testShowDisplayListForOnlyOneDisplay_PreviouslyShownList() { + var fragment = initFragment(); + // Previously shown list of displays + fragment.onActivityCreatedCallback(createBundleForPreviouslyShownList()); + // Only one display available + doReturn(new Display[] {mDisplays[1]}).when(mMockedInjector).getAllDisplays(); + mHandler.flush(); + PreferenceCategory pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + assertThat(pref).isNotNull(); + assertThat(pref.getPreferenceCount()).isEqualTo(1); + } + + @Test + @UiThreadTest + public void testShowEnabledDisplay_OnlyOneDisplayAvailable() { + doReturn(true).when(mMockedInjector).isDisplayEnabled(any()); + // Only one display available + doReturn(new Display[] {mDisplays[1]}).when(mMockedInjector).getAllDisplays(); + // Init + initFragment(); + mHandler.flush(); + PreferenceCategory list = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY); + assertThat(list).isNull(); + var pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + assertThat(pref).isNotNull(); + pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_ROTATION_KEY); + assertThat(pref).isNotNull(); + var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER); + assertThat(footerPref).isNotNull(); + verify(footerPref).setTitle(EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE); + } + + @Test + @UiThreadTest + public void testShowOneEnabledDisplay_FewAvailable() { + mDisplayIdArg = 1; + doReturn(true).when(mMockedInjector).isDisplayEnabled(any()); + initFragment(); + verify(mMockedInjector, never()).getDisplay(anyInt()); + mHandler.flush(); + verify(mMockedInjector).getDisplay(mDisplayIdArg); + var pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + assertThat(pref).isNotNull(); + pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_ROTATION_KEY); + assertThat(pref).isNotNull(); + var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER); + assertThat(footerPref).isNotNull(); + verify(footerPref).setTitle(EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE); + } + + @Test + @UiThreadTest + public void testShowDisabledDisplay() { + mDisplayIdArg = 1; + initFragment(); + verify(mMockedInjector, never()).getDisplay(anyInt()); + mHandler.flush(); + verify(mMockedInjector).getDisplay(mDisplayIdArg); + var mainPref = (MainSwitchPreference) mPreferenceScreen.findPreference( + EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); + assertThat(mainPref).isNotNull(); + assertThat("" + mainPref.getTitle()).isEqualTo( + getText(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE)); + assertThat(mainPref.isChecked()).isFalse(); + assertThat(mainPref.isEnabled()).isTrue(); + assertThat(mainPref.getOnPreferenceChangeListener()).isNotNull(); + var pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + assertThat(pref).isNull(); + pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_ROTATION_KEY); + assertThat(pref).isNull(); + var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER); + assertThat(footerPref).isNull(); + } + + @Test + @UiThreadTest + public void testNoDisplays() { + doReturn(new Display[0]).when(mMockedInjector).getAllDisplays(); + initFragment(); + mHandler.flush(); + var mainPref = (MainSwitchPreference) mPreferenceScreen.findPreference( + EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); + assertThat(mainPref).isNotNull(); + assertThat("" + mainPref.getTitle()).isEqualTo( + getText(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE)); + assertThat(mainPref.isChecked()).isFalse(); + assertThat(mainPref.isEnabled()).isFalse(); + assertThat(mainPref.getOnPreferenceChangeListener()).isNull(); + var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER); + assertThat(footerPref).isNotNull(); + verify(footerPref).setTitle(EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE); + } + + @Test + @UiThreadTest + public void testDisplayRotationPreference() { + mDisplayIdArg = 1; + doReturn(true).when(mMockedInjector).isDisplayEnabled(any()); + var fragment = initFragment(); + mHandler.flush(); + var pref = fragment.getRotationPreference(mContext); + assertThat(pref.getKey()).isEqualTo(EXTERNAL_DISPLAY_ROTATION_KEY); + assertThat("" + pref.getTitle()).isEqualTo( + getText(EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE)); + assertThat(pref.getEntries().length).isEqualTo(4); + assertThat(pref.getEntryValues().length).isEqualTo(4); + assertThat(pref.getEntryValues()[0].toString()).isEqualTo("0"); + assertThat(pref.getEntryValues()[1].toString()).isEqualTo("1"); + assertThat(pref.getEntryValues()[2].toString()).isEqualTo("2"); + assertThat(pref.getEntryValues()[3].toString()).isEqualTo("3"); + assertThat(pref.getEntries()[0].length()).isGreaterThan(0); + assertThat(pref.getEntries()[1].length()).isGreaterThan(0); + assertThat("" + pref.getSummary()).isEqualTo(pref.getEntries()[0].toString()); + assertThat(pref.getValue()).isEqualTo("0"); + assertThat(pref.getOnPreferenceChangeListener()).isNotNull(); + assertThat(pref.isEnabled()).isTrue(); + var rotation = 1; + doReturn(true).when(mMockedInjector).freezeDisplayRotation(mDisplayIdArg, rotation); + assertThat(pref.getOnPreferenceChangeListener().onPreferenceChange(pref, rotation + "")) + .isTrue(); + verify(mMockedInjector).freezeDisplayRotation(mDisplayIdArg, rotation); + assertThat(pref.getValue()).isEqualTo(rotation + ""); + verify(mMockedMetricsLogger).writePreferenceClickMetric(pref); + } + + @Test + @UiThreadTest + public void testDisplayResolutionPreference() { + mDisplayIdArg = 1; + doReturn(true).when(mMockedInjector).isDisplayEnabled(any()); + var fragment = initFragment(); + mHandler.flush(); + var pref = fragment.getResolutionPreference(mContext); + assertThat(pref.getKey()).isEqualTo(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY); + assertThat("" + pref.getTitle()).isEqualTo( + getText(EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE)); + assertThat("" + pref.getSummary()).isEqualTo("1920 x 1080"); + assertThat(pref.isEnabled()).isTrue(); + assertThat(pref.getOnPreferenceClickListener()).isNotNull(); + assertThat(pref.getOnPreferenceClickListener().onPreferenceClick(pref)).isTrue(); + assertThat(mResolutionSelectorDisplayId).isEqualTo(mDisplayIdArg); + verify(mMockedMetricsLogger).writePreferenceClickMetric(pref); + } + + @Test + @UiThreadTest + public void testUseDisplayPreference_EnabledDisplay() { + mDisplayIdArg = 1; + doReturn(true).when(mMockedInjector).isDisplayEnabled(any()); + doReturn(true).when(mMockedInjector).enableConnectedDisplay(mDisplayIdArg); + doReturn(true).when(mMockedInjector).disableConnectedDisplay(mDisplayIdArg); + var fragment = initFragment(); + mHandler.flush(); + var pref = fragment.getUseDisplayPreference(mContext); + assertThat(pref.getKey()).isEqualTo(EXTERNAL_DISPLAY_USE_PREFERENCE_KEY); + assertThat("" + pref.getTitle()).isEqualTo(getText(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE)); + assertThat(pref.isEnabled()).isTrue(); + assertThat(pref.isChecked()).isTrue(); + assertThat(pref.getOnPreferenceChangeListener()).isNotNull(); + assertThat(pref.getOnPreferenceChangeListener().onPreferenceChange(pref, false)).isTrue(); + verify(mMockedInjector).disableConnectedDisplay(mDisplayIdArg); + assertThat(pref.isChecked()).isFalse(); + assertThat(pref.getOnPreferenceChangeListener().onPreferenceChange(pref, true)).isTrue(); + verify(mMockedInjector).enableConnectedDisplay(mDisplayIdArg); + assertThat(pref.isChecked()).isTrue(); + verify(mMockedMetricsLogger, times(2)).writePreferenceClickMetric(pref); + } + + @NonNull + private ExternalDisplayPreferenceFragment initFragment() { + if (mFragment != null) { + return mFragment; + } + mFragment = new TestableExternalDisplayPreferenceFragment(); + mFragment.onCreateCallback(null); + mFragment.onActivityCreatedCallback(null); + mFragment.onStartCallback(); + return mFragment; + } + + @NonNull + private Bundle createBundleForPreviouslyShownList() { + var state = new Bundle(); + state.putBoolean(PREVIOUSLY_SHOWN_LIST_KEY, true); + return state; + } + + @NonNull + private String getText(int id) { + return mContext.getResources().getText(id).toString(); + } + + private class TestableExternalDisplayPreferenceFragment extends + ExternalDisplayPreferenceFragment { + private final View mMockedRootView; + private final TextView mEmptyView; + private final Activity mMockedActivity; + private final FooterPreference mMockedFooterPreference; + private final MetricsLogger mLogger; + + TestableExternalDisplayPreferenceFragment() { + super(mMockedInjector); + mMockedActivity = mock(Activity.class); + mMockedRootView = mock(View.class); + mMockedFooterPreference = mock(FooterPreference.class); + doReturn(KEY_FOOTER).when(mMockedFooterPreference).getKey(); + mEmptyView = new TextView(mContext); + doReturn(mEmptyView).when(mMockedRootView).findViewById(android.R.id.empty); + mLogger = mMockedMetricsLogger; + } + + @Override + public PreferenceScreen getPreferenceScreen() { + return mPreferenceScreen; + } + + @Override + protected Activity getCurrentActivity() { + return mMockedActivity; + } + + @Override + public View getView() { + return mMockedRootView; + } + + @Override + public void setEmptyView(View view) { + assertThat(view).isEqualTo(mEmptyView); + } + + @Override + public View getEmptyView() { + return mEmptyView; + } + + @Override + public void addPreferencesFromResource(int resource) { + mPreferenceIdFromResource = resource; + } + + @Override + @NonNull + FooterPreference getFooterPreference(@NonNull Context context) { + return mMockedFooterPreference; + } + + @Override + protected int getDisplayIdArg() { + return mDisplayIdArg; + } + + @Override + protected void launchResolutionSelector(@NonNull Context context, int displayId) { + mResolutionSelectorDisplayId = displayId; + } + + @Override + protected void launchDisplaySettings(final int displayId) { + mDisplayIdArg = displayId; + } + + @Override + protected void writePreferenceClickMetric(Preference preference) { + mLogger.writePreferenceClickMetric(preference); + } + } + + /** + * Interface allowing to mock and spy on log events. + */ + public interface MetricsLogger { + + /** + * On preference click metric + */ + void writePreferenceClickMetric(Preference preference); + } +} diff --git a/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayTestBase.java b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayTestBase.java new file mode 100644 index 00000000000..60b034288a0 --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayTestBase.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.display; + +import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY; +import static com.android.settings.flags.Flags.FLAG_ROTATION_CONNECTED_DISPLAY_SETTING; +import static com.android.settings.flags.Flags.FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.content.res.Resources; +import android.hardware.display.DisplayManagerGlobal; +import android.hardware.display.IDisplayManager; +import android.os.RemoteException; +import android.view.Display; +import android.view.DisplayAdjustments; +import android.view.DisplayInfo; + +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.server.testutils.TestHandler; +import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener; +import com.android.settings.flags.FakeFeatureFlagsImpl; + +import org.junit.Before; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class ExternalDisplayTestBase { + @Mock + ExternalDisplaySettingsConfiguration.Injector mMockedInjector; + @Mock + IDisplayManager mMockedIDisplayManager; + Resources mResources; + DisplayManagerGlobal mDisplayManagerGlobal; + FakeFeatureFlagsImpl mFlags = new FakeFeatureFlagsImpl(); + Context mContext; + DisplayListener mListener; + TestHandler mHandler = new TestHandler(null); + PreferenceManager mPreferenceManager; + PreferenceScreen mPreferenceScreen; + Display[] mDisplays; + + /** + * Setup. + */ + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + mContext = spy(ApplicationProvider.getApplicationContext()); + mResources = spy(mContext.getResources()); + doReturn(mResources).when(mContext).getResources(); + mPreferenceManager = new PreferenceManager(mContext); + mPreferenceScreen = mPreferenceManager.createPreferenceScreen(mContext); + doReturn(0).when(mMockedIDisplayManager).getPreferredWideGamutColorSpaceId(); + mDisplayManagerGlobal = new DisplayManagerGlobal(mMockedIDisplayManager); + mFlags.setFlag(FLAG_ROTATION_CONNECTED_DISPLAY_SETTING, true); + mFlags.setFlag(FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING, true); + mDisplays = new Display[] { + createDefaultDisplay(), createExternalDisplay(), createOverlayDisplay()}; + doReturn(mDisplays).when(mMockedInjector).getAllDisplays(); + doReturn(mDisplays).when(mMockedInjector).getEnabledDisplays(); + for (var display : mDisplays) { + doReturn(display).when(mMockedInjector).getDisplay(display.getDisplayId()); + } + doReturn(mFlags).when(mMockedInjector).getFlags(); + doReturn(mHandler).when(mMockedInjector).getHandler(); + doReturn("").when(mMockedInjector).getSystemProperty( + VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY); + doAnswer((arg) -> { + mListener = arg.getArgument(0); + return null; + }).when(mMockedInjector).registerDisplayListener(any()); + doReturn(0).when(mMockedInjector).getDisplayUserRotation(anyInt()); + doReturn(mContext).when(mMockedInjector).getContext(); + } + + Display createDefaultDisplay() throws RemoteException { + int displayId = 0; + var displayInfo = new DisplayInfo(); + doReturn(displayInfo).when(mMockedIDisplayManager).getDisplayInfo(displayId); + displayInfo.displayId = displayId; + displayInfo.name = "Built-in"; + displayInfo.type = Display.TYPE_INTERNAL; + displayInfo.supportedModes = new Display.Mode[]{ + new Display.Mode(0, 2048, 1024, 60, 60, new float[0], + new int[0])}; + displayInfo.appsSupportedModes = displayInfo.supportedModes; + return createDisplay(displayInfo); + } + + Display createExternalDisplay() throws RemoteException { + int displayId = 1; + var displayInfo = new DisplayInfo(); + doReturn(displayInfo).when(mMockedIDisplayManager).getDisplayInfo(displayId); + displayInfo.displayId = displayId; + displayInfo.name = "HDMI"; + displayInfo.type = Display.TYPE_EXTERNAL; + displayInfo.supportedModes = new Display.Mode[]{ + new Display.Mode(0, 1920, 1080, 60, 60, new float[0], new int[0]), + new Display.Mode(1, 800, 600, 60, 60, new float[0], new int[0]), + new Display.Mode(2, 320, 240, 70, 70, new float[0], new int[0]), + new Display.Mode(3, 640, 480, 60, 60, new float[0], new int[0]), + new Display.Mode(4, 640, 480, 50, 60, new float[0], new int[0]), + new Display.Mode(5, 2048, 1024, 60, 60, new float[0], new int[0]), + new Display.Mode(6, 720, 480, 60, 60, new float[0], new int[0])}; + displayInfo.appsSupportedModes = displayInfo.supportedModes; + return createDisplay(displayInfo); + } + + Display createOverlayDisplay() throws RemoteException { + int displayId = 2; + var displayInfo = new DisplayInfo(); + doReturn(displayInfo).when(mMockedIDisplayManager).getDisplayInfo(displayId); + displayInfo.displayId = displayId; + displayInfo.name = "Overlay #1"; + displayInfo.type = Display.TYPE_OVERLAY; + displayInfo.supportedModes = new Display.Mode[]{ + new Display.Mode(0, 1240, 780, 60, 60, new float[0], + new int[0])}; + displayInfo.appsSupportedModes = displayInfo.supportedModes; + return createDisplay(displayInfo); + } + + Display createDisplay(DisplayInfo displayInfo) { + return new Display(mDisplayManagerGlobal, displayInfo.displayId, displayInfo, + (DisplayAdjustments) null); + } +} diff --git a/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdaterTest.java b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdaterTest.java new file mode 100644 index 00000000000..824974ad854 --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdaterTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.display; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.RemoteException; +import android.view.Display; + +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.connecteddevice.DevicePreferenceCallback; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedPreference; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** Unit tests for {@link ExternalDisplayUpdater}. */ +@RunWith(AndroidJUnit4.class) +public class ExternalDisplayUpdaterTest extends ExternalDisplayTestBase { + + private ExternalDisplayUpdater mUpdater; + @Mock + private DevicePreferenceCallback mMockedCallback; + @Mock + private Drawable mMockedDrawable; + private RestrictedPreference mPreferenceAdded; + private RestrictedPreference mPreferenceRemoved; + + @Before + public void setUp() throws RemoteException { + super.setUp(); + mUpdater = new TestableExternalDisplayUpdater(mMockedCallback, /*metricsCategory=*/ 0); + } + + @Test + public void testPreferenceAdded() { + doAnswer((v) -> { + mPreferenceAdded = v.getArgument(0); + return null; + }).when(mMockedCallback).onDeviceAdded(any()); + mUpdater.initPreference(mContext, mMockedInjector); + mUpdater.registerCallback(); + mHandler.flush(); + assertThat(mPreferenceAdded).isNotNull(); + var summary = mPreferenceAdded.getSummary(); + assertThat(summary).isNotNull(); + assertThat(summary.length()).isGreaterThan(0); + var title = mPreferenceAdded.getTitle(); + assertThat(title).isNotNull(); + assertThat(title.length()).isGreaterThan(0); + } + + @Test + public void testPreferenceRemoved() { + doAnswer((v) -> { + mPreferenceAdded = v.getArgument(0); + return null; + }).when(mMockedCallback).onDeviceAdded(any()); + doAnswer((v) -> { + mPreferenceRemoved = v.getArgument(0); + return null; + }).when(mMockedCallback).onDeviceRemoved(any()); + mUpdater.initPreference(mContext, mMockedInjector); + mUpdater.registerCallback(); + mHandler.flush(); + assertThat(mPreferenceAdded).isNotNull(); + assertThat(mPreferenceRemoved).isNull(); + // Remove display + doReturn(new Display[0]).when(mMockedInjector).getAllDisplays(); + doReturn(new Display[0]).when(mMockedInjector).getEnabledDisplays(); + mListener.onDisplayRemoved(1); + mHandler.flush(); + assertThat(mPreferenceRemoved).isEqualTo(mPreferenceAdded); + } + + class TestableExternalDisplayUpdater extends ExternalDisplayUpdater { + TestableExternalDisplayUpdater( + DevicePreferenceCallback callback, + int metricsCategory) { + super(callback, metricsCategory); + } + + @Override + @Nullable + protected RestrictedLockUtils.EnforcedAdmin checkIfUsbDataSignalingIsDisabled( + Context context) { + // if null is returned - usb signalling is enabled + return null; + } + + @Override + @Nullable + protected Drawable getDrawable(Context context) { + return mMockedDrawable; + } + } +} diff --git a/tests/unit/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragmentTest.java b/tests/unit/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragmentTest.java new file mode 100644 index 00000000000..ee38a1cbae2 --- /dev/null +++ b/tests/unit/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragmentTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.display; + +import static android.view.Display.INVALID_DISPLAY; + +import static com.android.settings.connecteddevice.display.ResolutionPreferenceFragment.EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE; +import static com.android.settings.connecteddevice.display.ResolutionPreferenceFragment.MORE_OPTIONS_KEY; +import static com.android.settings.connecteddevice.display.ResolutionPreferenceFragment.TOP_OPTIONS_KEY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.content.res.Resources; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** Unit tests for {@link ResolutionPreferenceFragment}. */ +@RunWith(AndroidJUnit4.class) +public class ResolutionPreferenceFragmentTest extends ExternalDisplayTestBase { + @Nullable + private ResolutionPreferenceFragment mFragment; + private int mPreferenceIdFromResource; + private int mDisplayIdArg = INVALID_DISPLAY; + @Mock + private MetricsLogger mMockedMetricsLogger; + + @Test + @UiThreadTest + public void testCreateAndStart() { + initFragment(); + mHandler.flush(); + assertThat(mPreferenceIdFromResource).isEqualTo( + EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE); + var pref = mPreferenceScreen.findPreference(TOP_OPTIONS_KEY); + assertThat(pref).isNull(); + pref = mPreferenceScreen.findPreference(MORE_OPTIONS_KEY); + assertThat(pref).isNull(); + } + + @Test + @UiThreadTest + public void testCreateAndStartDefaultDisplayNotAllowed() { + mDisplayIdArg = 0; + initFragment(); + mHandler.flush(); + var pref = mPreferenceScreen.findPreference(TOP_OPTIONS_KEY); + assertThat(pref).isNull(); + pref = mPreferenceScreen.findPreference(MORE_OPTIONS_KEY); + assertThat(pref).isNull(); + } + + @Test + @UiThreadTest + public void testModePreferences() { + mDisplayIdArg = 1; + initFragment(); + mHandler.flush(); + PreferenceCategory topPref = mPreferenceScreen.findPreference(TOP_OPTIONS_KEY); + assertThat(topPref).isNotNull(); + PreferenceCategory morePref = mPreferenceScreen.findPreference(MORE_OPTIONS_KEY); + assertThat(morePref).isNotNull(); + assertThat(topPref.getPreferenceCount()).isEqualTo(3); + assertThat(morePref.getPreferenceCount()).isEqualTo(1); + } + + @Test + @UiThreadTest + public void testModeChange() { + mDisplayIdArg = 1; + initFragment(); + mHandler.flush(); + PreferenceCategory topPref = mPreferenceScreen.findPreference(TOP_OPTIONS_KEY); + assertThat(topPref).isNotNull(); + var modePref = (SelectorWithWidgetPreference) topPref.getPreference(1); + modePref.onClick(); + var mode = mDisplays[mDisplayIdArg].getSupportedModes()[1]; + verify(mMockedInjector).setUserPreferredDisplayMode(mDisplayIdArg, mode); + } + + private void initFragment() { + if (mFragment != null) { + return; + } + mFragment = new TestableResolutionPreferenceFragment(); + mFragment.onCreateCallback(null); + mFragment.onActivityCreatedCallback(null); + mFragment.onStartCallback(); + } + + private class TestableResolutionPreferenceFragment extends ResolutionPreferenceFragment { + private final View mMockedRootView; + private final TextView mEmptyView; + private final Resources mMockedResources; + private final MetricsLogger mLogger; + TestableResolutionPreferenceFragment() { + super(mMockedInjector); + mMockedResources = mock(Resources.class); + doReturn(61).when(mMockedResources).getInteger( + com.android.internal.R.integer.config_externalDisplayPeakRefreshRate); + doReturn(1920).when(mMockedResources).getInteger( + com.android.internal.R.integer.config_externalDisplayPeakWidth); + doReturn(1080).when(mMockedResources).getInteger( + com.android.internal.R.integer.config_externalDisplayPeakHeight); + doReturn(true).when(mMockedResources).getBoolean( + com.android.internal.R.bool.config_refreshRateSynchronizationEnabled); + mMockedRootView = mock(View.class); + mEmptyView = new TextView(mContext); + doReturn(mEmptyView).when(mMockedRootView).findViewById(android.R.id.empty); + mLogger = mMockedMetricsLogger; + } + + @Override + public PreferenceScreen getPreferenceScreen() { + return mPreferenceScreen; + } + + @Override + public View getView() { + return mMockedRootView; + } + + @Override + public void setEmptyView(View view) { + assertThat(view).isEqualTo(mEmptyView); + } + + @Override + public View getEmptyView() { + return mEmptyView; + } + + @Override + public void addPreferencesFromResource(int resource) { + mPreferenceIdFromResource = resource; + } + + @Override + protected int getDisplayIdArg() { + return mDisplayIdArg; + } + + @Override + protected void writePreferenceClickMetric(Preference preference) { + mLogger.writePreferenceClickMetric(preference); + } + + @Override + @NonNull + protected Resources getResources(@NonNull Context context) { + return mMockedResources; + } + } + + /** + * Interface allowing to mock and spy on log events. + */ + public interface MetricsLogger { + /** + * On preference click metric + */ + void writePreferenceClickMetric(Preference preference); + } +} From d303afbbd758bdab21b7fbd1d9af278f4758a690 Mon Sep 17 00:00:00 2001 From: Pat Manning Date: Fri, 8 Mar 2024 16:56:03 +0000 Subject: [PATCH 22/23] Add Settings for vector-specific PointerIcon scale. Bug: 305193969 Test: PointerScaleSeekBarControllerTest Flag: com.android.systemui.enable_vector_cursor_a11y_settings Change-Id: I2f344f1d5fc6d5c8eb7159570287b8ce6f9ac070 --- res/values/dimens.xml | 3 + res/values/integers.xml | 4 + res/values/strings.xml | 6 ++ res/xml/trackpad_settings.xml | 12 ++- .../PointerScaleSeekBarController.java | 95 +++++++++++++++++++ .../PointerScaleSeekBarControllerTest.java | 94 ++++++++++++++++++ 6 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/com/android/settings/inputmethod/PointerScaleSeekBarController.java create mode 100644 tests/robotests/src/com/android/settings/inputmethod/PointerScaleSeekBarControllerTest.java diff --git a/res/values/dimens.xml b/res/values/dimens.xml index d972e138eec..d34647449df 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -179,6 +179,9 @@ 8dp 1dp 3dp + 8dp + 1.0 + 2.5 40dp diff --git a/res/values/integers.xml b/res/values/integers.xml index f62ccae576e..5427cdd0d35 100644 --- a/res/values/integers.xml +++ b/res/values/integers.xml @@ -36,4 +36,8 @@ 0 0 + + + 0 + 3 diff --git a/res/values/strings.xml b/res/values/strings.xml index 21c750f31eb..d76b8435c85 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4571,6 +4571,12 @@ Pointer speed + + Pointer scale + + Decrease pointer scale + + Increase pointer scale Game Controller diff --git a/res/xml/trackpad_settings.xml b/res/xml/trackpad_settings.xml index 1eb16b73156..04422dd5df9 100644 --- a/res/xml/trackpad_settings.xml +++ b/res/xml/trackpad_settings.xml @@ -66,9 +66,19 @@ android:key="pointer_fill_style" android:title="@string/pointer_fill_style" android:order="50" - android:dialogTitle="@string/pointer_fill_style" settings:controller="com.android.settings.inputmethod.PointerFillStylePreferenceController"/> + + Date: Tue, 25 Jun 2024 16:48:31 +0200 Subject: [PATCH 23/23] Adjust strings in modes list to match latest mocks Bug: 346575288 Test: atest ZenModesListItemPreferenceTest Flag: android.app.modes_ui Change-Id: Ibe9c049ed0fb3b1838866b923a6227d67885c75c --- res/values/strings.xml | 6 +++--- .../notification/modes/ZenModesListItemPreference.java | 2 +- .../notification/modes/ZenModesListItemPreferenceTest.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index ed9341d6a23..b231927dc3c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7946,11 +7946,11 @@ %1$s%2$s - - Tap to set up + + Not set - Paused + Disabled Limit interruptions diff --git a/src/com/android/settings/notification/modes/ZenModesListItemPreference.java b/src/com/android/settings/notification/modes/ZenModesListItemPreference.java index 1bc6e55acce..261ab1d60c5 100644 --- a/src/com/android/settings/notification/modes/ZenModesListItemPreference.java +++ b/src/com/android/settings/notification/modes/ZenModesListItemPreference.java @@ -79,7 +79,7 @@ class ZenModesListItemPreference extends RestrictedPreference { mZenMode.getRule().getTriggerDescription()); case ENABLED -> mZenMode.getRule().getTriggerDescription(); case DISABLED_BY_USER -> mContext.getString(R.string.zen_mode_disabled_by_user); - case DISABLED_BY_OTHER -> mContext.getString(R.string.zen_mode_disabled_tap_to_set_up); + case DISABLED_BY_OTHER -> mContext.getString(R.string.zen_mode_disabled_needs_setup); }; setSummary(statusText); diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesListItemPreferenceTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListItemPreferenceTest.java index 495a24c8c34..aaf70595132 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModesListItemPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListItemPreferenceTest.java @@ -100,7 +100,7 @@ public class ZenModesListItemPreferenceTest { ShadowLooper.idleMainLooper(); assertThat(preference.getTitle()).isEqualTo("Mode disabled by app"); - assertThat(preference.getSummary()).isEqualTo("Tap to set up"); + assertThat(preference.getSummary()).isEqualTo("Not set"); assertThat(preference.getIcon()).isNotNull(); } @@ -120,7 +120,7 @@ public class ZenModesListItemPreferenceTest { ShadowLooper.idleMainLooper(); assertThat(preference.getTitle()).isEqualTo("Mode disabled by user"); - assertThat(preference.getSummary()).isEqualTo("Paused"); + assertThat(preference.getSummary()).isEqualTo("Disabled"); assertThat(preference.getIcon()).isNotNull(); } }