From eb1344959306fce370858273cc45a21b70349f11 Mon Sep 17 00:00:00 2001 From: Joshua McCloskey Date: Tue, 15 Oct 2024 03:33:57 +0000 Subject: [PATCH 01/11] Updated interactors to use repos Test: Verified flows still work as expected Flag: EXEMPT bugfix Bug: 379113250 Change-Id: I548b98f9e9121e7c0b4e613559817895c33af989 --- .../fingerprint2/BiometricsEnvironment.kt | 10 ++-- .../interactor/AuthenticateInteractorImpl.kt | 10 ++-- .../EnrollFingerprintInteractorImpl.kt | 5 +- .../GenerateChallengeInteractorImpl.kt | 10 ++-- .../RemoveFingerprintsInteractorImpl.kt | 49 ++++++++++--------- .../RenameFingerprintsInteractorImpl.kt | 5 +- .../FingerprintManagerInteractorTest.kt | 10 ++-- 7 files changed, 59 insertions(+), 40 deletions(-) diff --git a/src/com/android/settings/biometrics/fingerprint2/BiometricsEnvironment.kt b/src/com/android/settings/biometrics/fingerprint2/BiometricsEnvironment.kt index 030aadb2ea5..790b14af743 100644 --- a/src/com/android/settings/biometrics/fingerprint2/BiometricsEnvironment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/BiometricsEnvironment.kt @@ -122,24 +122,24 @@ class BiometricsEnvironment( FingerprintEnrollStageThresholdInteractor(fingerprintEnrollmentRepository) fun createGenerateChallengeInteractor(): GenerateChallengeInteractor = - GenerateChallengeInteractorImpl(fingerprintManager, context.userId, gateKeeperPasswordProvider) + GenerateChallengeInteractorImpl(fingerprintManager, userRepo, gateKeeperPasswordProvider) fun createFingerprintEnrollInteractor(): EnrollFingerprintInteractor = - EnrollFingerprintInteractorImpl(context.userId, fingerprintManager, Settings) + EnrollFingerprintInteractorImpl(userRepo, fingerprintManager, Settings) fun createFingerprintsEnrolledInteractor(): EnrolledFingerprintsInteractorImpl = EnrolledFingerprintsInteractorImpl(fingerprintEnrollmentRepository) fun createAuthenticateInteractor(): AuthenitcateInteractor = - AuthenticateInteractorImpl(fingerprintManager, context.userId) + AuthenticateInteractorImpl(fingerprintManager, userRepo) fun createUserInteractor(): UserInteractor = UserInteractorImpl(userRepo) fun createRemoveFingerprintInteractor(): RemoveFingerprintInteractor = - RemoveFingerprintsInteractorImpl(fingerprintManager, context.userId) + RemoveFingerprintsInteractorImpl(fingerprintManager, userRepo) fun createRenameFingerprintInteractor(): RenameFingerprintInteractor = - RenameFingerprintsInteractorImpl(fingerprintManager, context.userId, backgroundDispatcher) + RenameFingerprintsInteractorImpl(fingerprintManager, userRepo, backgroundDispatcher) fun createAccessibilityInteractor(): AccessibilityInteractor { return AccessibilityInteractorImpl(context.getSystemService(AccessibilityManager::class.java)!!) diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/AuthenticateInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/AuthenticateInteractorImpl.kt index df93092619f..8640177f0e2 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/AuthenticateInteractorImpl.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/AuthenticateInteractorImpl.kt @@ -19,19 +19,22 @@ package com.android.settings.biometrics.fingerprint2.domain.interactor import android.hardware.fingerprint.FingerprintManager import android.os.CancellationSignal import android.util.Log +import com.android.settings.biometrics.fingerprint2.data.repository.UserRepo import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.AuthenitcateInteractor import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel import kotlin.coroutines.resume import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.flow.first import kotlinx.coroutines.suspendCancellableCoroutine class AuthenticateInteractorImpl( private val fingerprintManager: FingerprintManager, - private val userId: Int, + private val userRepo: UserRepo, ) : AuthenitcateInteractor { - override suspend fun authenticate(): FingerprintAuthAttemptModel = - suspendCancellableCoroutine { c: CancellableContinuation -> + override suspend fun authenticate(): FingerprintAuthAttemptModel { + val userId = userRepo.currentUser.first() + return suspendCancellableCoroutine { c: CancellableContinuation -> val authenticationCallback = object : FingerprintManager.AuthenticationCallback() { @@ -64,6 +67,7 @@ class AuthenticateInteractorImpl( userId, ) } + } companion object { private const val TAG = "AuthenticateInteractor" diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollFingerprintInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollFingerprintInteractorImpl.kt index 3e14a640278..2c1cacab992 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollFingerprintInteractorImpl.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollFingerprintInteractorImpl.kt @@ -22,6 +22,7 @@ import android.os.CancellationSignal import android.util.Log import com.android.settings.biometrics.fingerprint2.conversion.Util.toEnrollError import com.android.settings.biometrics.fingerprint2.conversion.Util.toOriginalReason +import com.android.settings.biometrics.fingerprint2.data.repository.UserRepo import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.EnrollFingerprintInteractor import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState @@ -33,10 +34,11 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update class EnrollFingerprintInteractorImpl( - private val userId: Int, + private val userRepo: UserRepo, private val fingerprintManager: FingerprintManager, private val fingerprintFlow: FingerprintFlow, ) : EnrollFingerprintInteractor { @@ -47,6 +49,7 @@ class EnrollFingerprintInteractorImpl( enrollReason: EnrollReason, fingerprintEnrollOptions: FingerprintEnrollOptions, ): Flow = callbackFlow { + val userId = userRepo.currentUser.first() // TODO (b/308456120) Improve this logic if (enrollRequestOutstanding.value) { Log.d(TAG, "Outstanding enroll request, waiting 150ms") diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/GenerateChallengeInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/GenerateChallengeInteractorImpl.kt index a2080fbb74f..365e644afdf 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/GenerateChallengeInteractorImpl.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/GenerateChallengeInteractorImpl.kt @@ -19,19 +19,22 @@ package com.android.settings.biometrics.fingerprint2.domain.interactor import android.content.Intent import android.hardware.fingerprint.FingerprintManager import com.android.settings.biometrics.GatekeeperPasswordProvider +import com.android.settings.biometrics.fingerprint2.data.repository.UserRepo import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.GenerateChallengeInteractor import com.android.settings.password.ChooseLockSettingsHelper +import kotlinx.coroutines.flow.first import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine class GenerateChallengeInteractorImpl( private val fingerprintManager: FingerprintManager, - private val userId: Int, + private val userRepo: UserRepo, private val gatekeeperPasswordProvider: GatekeeperPasswordProvider, ) : GenerateChallengeInteractor { - override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair = - suspendCoroutine { + override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair { + val userId = userRepo.currentUser.first() + return suspendCoroutine { val callback = FingerprintManager.GenerateChallengeCallback { _, userId, challenge -> val intent = Intent() @@ -45,4 +48,5 @@ class GenerateChallengeInteractorImpl( } fingerprintManager.generateChallenge(userId, callback) } + } } diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RemoveFingerprintsInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RemoveFingerprintsInteractorImpl.kt index 4232963ad55..cb33ff412aa 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RemoveFingerprintsInteractorImpl.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RemoveFingerprintsInteractorImpl.kt @@ -18,38 +18,43 @@ package com.android.settings.biometrics.fingerprint2.domain.interactor import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.FingerprintManager.RemovalCallback +import com.android.settings.biometrics.fingerprint2.data.repository.UserRepo import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RemoveFingerprintInteractor import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData +import kotlinx.coroutines.flow.first import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine class RemoveFingerprintsInteractorImpl( private val fingerprintManager: FingerprintManager, - private val userId: Int, + private val userRepo: UserRepo, ) : RemoveFingerprintInteractor { - override suspend fun removeFingerprint(fp: FingerprintData): Boolean = suspendCoroutine { - val callback = - object : RemovalCallback() { - override fun onRemovalError( - fp: android.hardware.fingerprint.Fingerprint, - errMsgId: Int, - errString: CharSequence, - ) { - it.resume(false) - } + override suspend fun removeFingerprint(fp: FingerprintData): Boolean { + val userId = userRepo.currentUser.first() + return suspendCoroutine { + val callback = + object : RemovalCallback() { + override fun onRemovalError( + fp: android.hardware.fingerprint.Fingerprint, + errMsgId: Int, + errString: CharSequence, + ) { + it.resume(false) + } - override fun onRemovalSucceeded( - fp: android.hardware.fingerprint.Fingerprint?, - remaining: Int, - ) { - it.resume(true) + override fun onRemovalSucceeded( + fp: android.hardware.fingerprint.Fingerprint?, + remaining: Int, + ) { + it.resume(true) + } } - } - fingerprintManager.remove( - android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId), - userId, - callback, - ) + fingerprintManager.remove( + android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId), + userId, + callback, + ) + } } } diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RenameFingerprintsInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RenameFingerprintsInteractorImpl.kt index f238e7c4f2e..5ab702afb9a 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RenameFingerprintsInteractorImpl.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RenameFingerprintsInteractorImpl.kt @@ -17,18 +17,21 @@ package com.android.settings.biometrics.fingerprint2.domain.interactor import android.hardware.fingerprint.FingerprintManager +import com.android.settings.biometrics.fingerprint2.data.repository.UserRepo import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RenameFingerprintInteractor import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext class RenameFingerprintsInteractorImpl( private val fingerprintManager: FingerprintManager, - private val userId: Int, + private val userRepo: UserRepo, private val backgroundDispatcher: CoroutineDispatcher, ) : RenameFingerprintInteractor { override suspend fun renameFingerprint(fp: FingerprintData, newName: String) { + val userId = userRepo.currentUser.first() withContext(backgroundDispatcher) { fingerprintManager.rename(fp.fingerId, userId, newName) } } } diff --git a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt index 2623206cddd..f479fa31784 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt @@ -152,17 +152,17 @@ class FingerprintManagerInteractorTest { enrolledFingerprintsInteractorUnderTest = EnrolledFingerprintsInteractorImpl(fingerprintEnrollRepo) generateChallengeInteractorUnderTest = - GenerateChallengeInteractorImpl(fingerprintManager, userId, gateKeeperPasswordProvider) + GenerateChallengeInteractorImpl(fingerprintManager, userRepo, gateKeeperPasswordProvider) removeFingerprintsInteractorUnderTest = - RemoveFingerprintsInteractorImpl(fingerprintManager, userId) + RemoveFingerprintsInteractorImpl(fingerprintManager, userRepo) renameFingerprintsInteractorUnderTest = - RenameFingerprintsInteractorImpl(fingerprintManager, userId, backgroundDispatcher) - authenticateInteractorImplUnderTest = AuthenticateInteractorImpl(fingerprintManager, userId) + RenameFingerprintsInteractorImpl(fingerprintManager, userRepo, backgroundDispatcher) + authenticateInteractorImplUnderTest = AuthenticateInteractorImpl(fingerprintManager, userRepo) canEnrollFingerprintsInteractorUnderTest = CanEnrollFingerprintsInteractorImpl(fingerprintEnrollRepo) - enrollInteractorUnderTest = EnrollFingerprintInteractorImpl(userId, fingerprintManager, flow) + enrollInteractorUnderTest = EnrollFingerprintInteractorImpl(userRepo, fingerprintManager, flow) } @Test From a6334bc0a3a66ee6194991eb1c941ab7dbea365a Mon Sep 17 00:00:00 2001 From: Jaewan Kim Date: Fri, 15 Nov 2024 00:40:21 +0900 Subject: [PATCH 02/11] Hide Linux terminal if device doesn't meet minimum requirement Bug: 366374130 Test: Manually, atest Change-Id: Id0ef47099b3fab41175bfae3d0eaf61037ba82ff --- .../LinuxTerminalPreferenceController.java | 30 +++++++++++-- ...LinuxTerminalPreferenceControllerTest.java | 44 +++++++++++++++++-- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java b/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java index b3a0f801f61..f21f2b5700b 100644 --- a/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java +++ b/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java @@ -18,7 +18,10 @@ package com.android.settings.development.linuxterminal; import android.content.Context; import android.content.pm.PackageManager; +import android.os.Process; +import android.os.storage.StorageManager; import android.text.TextUtils; +import android.util.DataUnit; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -28,30 +31,45 @@ import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; +import java.util.Objects; + /** Preference controller for Linux terminal option in developers option */ public class LinuxTerminalPreferenceController extends DeveloperOptionsPreferenceController implements PreferenceControllerMixin { @VisibleForTesting static final int TERMINAL_PACKAGE_NAME_RESID = R.string.config_linux_terminal_app_package_name; + @VisibleForTesting + static final long MEMORY_MIN_BYTES = DataUnit.GIGABYTES.toBytes(4); // 4_000_000_000 + + @VisibleForTesting + static final long STORAGE_MIN_BYTES = DataUnit.GIBIBYTES.toBytes(128); // 128 * 2^30 + private static final String LINUX_TERMINAL_KEY = "linux_terminal"; @Nullable private final String mTerminalPackageName; + private final boolean mIsDeviceCapable; public LinuxTerminalPreferenceController(@NonNull Context context) { super(context); String packageName = context.getString(TERMINAL_PACKAGE_NAME_RESID); mTerminalPackageName = isPackageInstalled(context.getPackageManager(), packageName) ? packageName : null; + + StorageManager storageManager = + Objects.requireNonNull(context.getSystemService(StorageManager.class)); + mIsDeviceCapable = + getTotalMemory() >= MEMORY_MIN_BYTES + && storageManager.getPrimaryStorageSize() >= STORAGE_MIN_BYTES; } // Avoid lazy initialization because this may be called before displayPreference(). @Override public boolean isAvailable() { - // Returns true only if the terminal app is installed which only happens when the build flag - // RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES is true. + // Check build flag RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES indirectly + // by checking whether the terminal app is installed. // TODO(b/343795511): Add explicitly check for the flag when it's accessible from Java code. - return mTerminalPackageName != null; + return mTerminalPackageName != null && mIsDeviceCapable; } @Override @@ -73,4 +91,10 @@ public class LinuxTerminalPreferenceController extends DeveloperOptionsPreferenc return false; } } + + // Can be overridden for test + @VisibleForTesting + long getTotalMemory() { + return Process.getTotalMemory(); + } } diff --git a/tests/robotests/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceControllerTest.java index 17c34356297..fffe6b10a1c 100644 --- a/tests/robotests/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceControllerTest.java @@ -16,6 +16,8 @@ package com.android.settings.development.linuxterminal; +import static com.android.settings.development.linuxterminal.LinuxTerminalPreferenceController.MEMORY_MIN_BYTES; +import static com.android.settings.development.linuxterminal.LinuxTerminalPreferenceController.STORAGE_MIN_BYTES; import static com.android.settings.development.linuxterminal.LinuxTerminalPreferenceController.TERMINAL_PACKAGE_NAME_RESID; import static com.google.common.truth.Truth.assertThat; @@ -29,6 +31,7 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.os.storage.StorageManager; import org.junit.Before; import org.junit.Test; @@ -43,6 +46,7 @@ public class LinuxTerminalPreferenceControllerTest { @Mock private Context mContext; @Mock private PackageManager mPackageManager; + @Mock private StorageManager mStorageManager; @Mock private PackageInfo mPackageInfo; private String mTerminalPackageName = "com.android.virtualization.terminal"; @@ -57,11 +61,30 @@ public class LinuxTerminalPreferenceControllerTest { doReturn(mPackageInfo) .when(mPackageManager) .getPackageInfo(eq(mTerminalPackageName), anyInt()); + + doReturn(mStorageManager).when(mContext).getSystemService(StorageManager.class); + doReturn(STORAGE_MIN_BYTES).when(mStorageManager).getPrimaryStorageSize(); } @Test - public void isAvailable_whenPackageExists_returnsTrue() throws NameNotFoundException { - mController = new LinuxTerminalPreferenceController(mContext); + public void isAvailable_whenMemoryInsufficient_returnFalse() { + mController = createController(mContext, MEMORY_MIN_BYTES / 2); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_whenDeviceStorageInsufficient_returnFalse() { + doReturn(STORAGE_MIN_BYTES / 2).when(mStorageManager).getPrimaryStorageSize(); + + mController = createController(mContext); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_whenPackageExists_returnsTrue() { + mController = createController(mContext); assertThat(mController.isAvailable()).isTrue(); } @@ -70,7 +93,7 @@ public class LinuxTerminalPreferenceControllerTest { public void isAvailable_whenPackageNameIsNull_returnsFalse() { doReturn(null).when(mContext).getString(TERMINAL_PACKAGE_NAME_RESID); - mController = new LinuxTerminalPreferenceController(mContext); + mController = createController(mContext); assertThat(mController.isAvailable()).isFalse(); } @@ -81,8 +104,21 @@ public class LinuxTerminalPreferenceControllerTest { .when(mPackageManager) .getPackageInfo(eq(mTerminalPackageName), anyInt()); - mController = new LinuxTerminalPreferenceController(mContext); + mController = createController(mContext); assertThat(mController.isAvailable()).isFalse(); } + + private LinuxTerminalPreferenceController createController(Context context) { + return createController(context, MEMORY_MIN_BYTES); + } + + private LinuxTerminalPreferenceController createController(Context context, long totalMemory) { + return new LinuxTerminalPreferenceController(context) { + @Override + public long getTotalMemory() { + return totalMemory; + } + }; + } } From 49045fb3617eda6a65808c929637eb38effd6a5c Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Mon, 18 Nov 2024 13:08:13 +0800 Subject: [PATCH 03/11] Remove the extra space above Spatial audio toggle BUG: 343317785 Test: local tested Flag: com.android.settings.flags.enable_bluetooth_device_details_polish Change-Id: I56bd6438e9f6589f388c0355460e6c5ba9cb2823 --- .../bluetooth/BluetoothDetailsSpatialAudioController.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java b/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java index 398edb6b991..b878612fa9f 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java @@ -150,6 +150,9 @@ public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsCont @Override protected void init(PreferenceScreen screen) { mProfilesContainer = screen.findPreference(getPreferenceKey()); + if (com.android.settings.flags.Flags.enableBluetoothDeviceDetailsPolish()) { + mProfilesContainer.setLayoutResource(R.layout.preference_category_bluetooth_no_padding); + } refresh(); } From d5e121d582cd0041848e13b2ddbd165679726ca4 Mon Sep 17 00:00:00 2001 From: George Chang Date: Mon, 18 Nov 2024 11:29:49 +0000 Subject: [PATCH 04/11] Revert^2 "Add intent filters for NfcTagAppsSettings" This reverts commit 581b9b43462f3a3ff6df2c84ed235b08c1cabca8. Reason for revert: CarDeveloperOptionsUnitTests fixed Change-Id: I65a477b1db837e7e9fd0bce6fb1d2af84a747408 Test: adb shell am start -a android.nfc.action.CHANGE_TAG_INTENT_PREFERENCE Test: atest CarDeveloperOptionsUnitTests Bug: 379618293 Bug: 335916336 --- AndroidManifest.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 5cd2bb394f8..382791631fe 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -5372,6 +5372,23 @@ + + + + + + + + + + + From ab0213a7e3a21f3b3ac68860e337d6567a04d0ca Mon Sep 17 00:00:00 2001 From: Zoey Chen Date: Thu, 14 Nov 2024 05:23:23 +0000 Subject: [PATCH 05/11] [Settings] Refactor: Add LocalePickerBaseListPreferenceController Bug: 377664066 Flag: EXEMPT refactor Change-Id: I2b0c0e9e121d7ab09cb8f627afa34a4f58ec9f9b --- .../localepicker/LocaleListEditor.java | 1 + .../LocaleListSearchCallback.java | 2 +- ...alePickerBaseListPreferenceController.java | 111 +++++++++++++----- ...egionAndNumberingSystemPickerFragment.java | 37 +++++- ...stemLocaleAllListPreferenceController.java | 87 ++++++++++++++ .../SystemLocalePickerFragment.java | 67 +++++++++-- ...caleSuggestedListPreferenceController.java | 81 +++++++++++++ 7 files changed, 337 insertions(+), 49 deletions(-) create mode 100644 src/com/android/settings/localepicker/SystemLocaleAllListPreferenceController.java create mode 100644 src/com/android/settings/localepicker/SystemLocaleSuggestedListPreferenceController.java diff --git a/src/com/android/settings/localepicker/LocaleListEditor.java b/src/com/android/settings/localepicker/LocaleListEditor.java index b09e22cc698..f9c50b79bd7 100644 --- a/src/com/android/settings/localepicker/LocaleListEditor.java +++ b/src/com/android/settings/localepicker/LocaleListEditor.java @@ -71,6 +71,7 @@ import java.util.Locale; public class LocaleListEditor extends RestrictedSettingsFragment implements View.OnTouchListener { protected static final String INTENT_LOCALE_KEY = "localeInfo"; protected static final String EXTRA_SYSTEM_LOCALE_DIALOG_TYPE = "system_locale_dialog_type"; + protected static final String EXTRA_RESULT_LOCALE = "result_locale"; protected static final String LOCALE_SUGGESTION = "locale_suggestion"; private static final String TAG = LocaleListEditor.class.getSimpleName(); diff --git a/src/com/android/settings/localepicker/LocaleListSearchCallback.java b/src/com/android/settings/localepicker/LocaleListSearchCallback.java index b80bbccccdd..e24e9bd3fa1 100644 --- a/src/com/android/settings/localepicker/LocaleListSearchCallback.java +++ b/src/com/android/settings/localepicker/LocaleListSearchCallback.java @@ -21,7 +21,7 @@ import com.android.internal.app.LocaleStore; import java.util.List; -/** Interface for when locale list changes in SearchView . */ +/** Interface for when locale list changes in SearchView. */ public interface LocaleListSearchCallback { /** Callback method for searching changes. */ diff --git a/src/com/android/settings/localepicker/LocalePickerBaseListPreferenceController.java b/src/com/android/settings/localepicker/LocalePickerBaseListPreferenceController.java index 8c71abf7248..4b0e9fa4fd4 100644 --- a/src/com/android/settings/localepicker/LocalePickerBaseListPreferenceController.java +++ b/src/com/android/settings/localepicker/LocalePickerBaseListPreferenceController.java @@ -16,13 +16,17 @@ package com.android.settings.localepicker; +import static com.android.settings.localepicker.LocaleListEditor.EXTRA_RESULT_LOCALE; +import static com.android.settings.localepicker.RegionAndNumberingSystemPickerFragment.EXTRA_IS_NUMBERING_SYSTEM; + import android.content.Context; +import android.os.Bundle; import android.os.LocaleList; -import android.provider.Settings; import android.util.ArrayMap; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; @@ -30,7 +34,10 @@ import androidx.preference.PreferenceScreen; import com.android.internal.app.LocaleCollectorBase; import com.android.internal.app.LocaleHelper; import com.android.internal.app.LocaleStore; +import com.android.settings.R; import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.core.instrumentation.Instrumentable; import java.util.ArrayList; import java.util.Collections; @@ -43,40 +50,27 @@ import java.util.stream.Collectors; /** A base controller for handling locale controllers. */ public abstract class LocalePickerBaseListPreferenceController extends BasePreferenceController implements LocaleListSearchCallback { - private static final String TAG = "LocalePickerBaseListPreference"; private static final String KEY_SUGGESTED = "suggested"; + private static final String KEY_SUPPORTED = "supported"; private PreferenceCategory mPreferenceCategory; - private LocaleList mExplicitLocales; private Set mLocaleList; private List mLocaleOptions; private Map mPreferences; private String mPackageName; - private boolean mCountryMode; + private boolean mIsCountryMode; + @Nullable private LocaleStore.LocaleInfo mParentLocale; public LocalePickerBaseListPreferenceController(@NonNull Context context, @NonNull String preferenceKey) { super(context, preferenceKey); - // TODO: Should get extra from fragment. -// if (isDeviceDemoMode()) { -// Bundle bundle = preference.getExtras(); -// mExplicitLocales = bundle == null -// ? null -// : bundle.getParcelable(Settings.EXTRA_EXPLICIT_LOCALES, LocaleList.class); -// Log.d(TAG, "Has explicit locales : " + mExplicitLocales); -// } mLocaleList = getLocaleCollectorController(context).getSupportedLocaleList(null, false, false); mLocaleOptions = new ArrayList<>(mLocaleList.size()); mPreferences = new ArrayMap<>(); } - private boolean isDeviceDemoMode() { - return Settings.Global.getInt( - mContext.getContentResolver(), Settings.Global.DEVICE_DEMO_MODE, 0) == 1; - } - @Override public void displayPreference(@NonNull PreferenceScreen screen) { super.displayPreference(screen); @@ -91,8 +85,21 @@ public abstract class LocalePickerBaseListPreferenceController extends } List result; + mParentLocale = getParentLocale(); + if (mParentLocale != null) { + mIsCountryMode = true; + mLocaleList = getLocaleCollectorController(mContext).getSupportedLocaleList( + mParentLocale, false, mIsCountryMode); + mLocaleOptions = new ArrayList<>(mLocaleList.size()); + if (!getPreferenceCategoryKey().contains(KEY_SUGGESTED)) { + mPreferenceCategory.setTitle( + mContext.getString(R.string.all_supported_locales_regions_title)); + } + } + result = getSortedLocaleList( - getPreferenceCategoryKey().contains(KEY_SUGGESTED) ? getSuggestedLocaleList() + getPreferenceCategoryKey().contains(KEY_SUGGESTED) + ? getSuggestedLocaleList() : getSupportedLocaleList()); final Map existingPreferences = mPreferences; @@ -113,12 +120,7 @@ public abstract class LocalePickerBaseListPreferenceController extends newList = getSortedSuggestedLocaleFromSearchList( newList, getSuggestedLocaleList()); } - if (!newList.isEmpty()) { - mPreferenceCategory.setVisible(true); - setupPreference(newList, existingPreferences); - } else { - mPreferenceCategory.setVisible(false); - } + setupPreference(newList, existingPreferences); } private List getSortedSuggestedLocaleFromSearchList( @@ -138,6 +140,12 @@ public abstract class LocalePickerBaseListPreferenceController extends private void setupPreference(List localeInfoList, Map existingPreferences) { + Log.d(TAG, "setupPreference: isNumberingMode = " + isNumberingMode()); + if (isNumberingMode() && getPreferenceCategoryKey().contains(KEY_SUPPORTED)) { + mPreferenceCategory.setTitle( + mContext.getString(R.string.all_supported_numbering_system_title)); + } + localeInfoList.stream().forEach(locale -> { Preference pref = existingPreferences.remove(locale.getId()); @@ -146,15 +154,16 @@ public abstract class LocalePickerBaseListPreferenceController extends mPreferenceCategory.addPreference(pref); } String localeName = - mCountryMode ? locale.getFullCountryNameNative() : locale.getFullNameNative(); + mIsCountryMode ? locale.getFullCountryNameNative() : locale.getFullNameNative(); pref.setTitle(localeName); pref.setKey(locale.toString()); pref.setOnPreferenceClickListener(clickedPref -> { - // TODO: Click locale to show region or numbering system page if needed. + switchFragment(locale); return true; }); mPreferences.put(locale.getId(), pref); }); + mPreferenceCategory.setVisible(mPreferenceCategory.getPreferenceCount() > 0); } @Override @@ -166,14 +175,16 @@ public abstract class LocalePickerBaseListPreferenceController extends protected abstract LocaleCollectorBase getLocaleCollectorController(Context context); + @Nullable protected abstract LocaleStore.LocaleInfo getParentLocale(); + + protected abstract boolean isNumberingMode(); + + @Nullable protected abstract LocaleList getExplicitLocaleList(); + protected String getPackageName() { return mPackageName; } - protected LocaleList getExplicitLocaleList() { - return mExplicitLocales; - } - protected List getSuggestedLocaleList() { mLocaleOptions.clear(); if (mLocaleList != null && !mLocaleList.isEmpty()) { @@ -203,8 +214,46 @@ public abstract class LocalePickerBaseListPreferenceController extends List localeInfos) { final Locale sortingLocale = Locale.getDefault(); final LocaleHelper.LocaleInfoComparator comp = - new LocaleHelper.LocaleInfoComparator(sortingLocale, mCountryMode); + new LocaleHelper.LocaleInfoComparator(sortingLocale, mIsCountryMode); Collections.sort(localeInfos, comp); return localeInfos; } + + private void switchFragment(LocaleStore.LocaleInfo localeInfo) { + boolean shouldShowLocaleEditor = shouldShowLocaleEditor(localeInfo); + String extraKey = shouldShowLocaleEditor ? LocaleListEditor.INTENT_LOCALE_KEY + : RegionAndNumberingSystemPickerFragment.EXTRA_TARGET_LOCALE; + String fragmentName = shouldShowLocaleEditor ? LocaleListEditor.class.getCanonicalName() + : RegionAndNumberingSystemPickerFragment.class.getCanonicalName(); + + final Bundle extra = new Bundle(); + extra.putSerializable(extraKey, localeInfo); + extra.putBoolean(EXTRA_IS_NUMBERING_SYSTEM, localeInfo.hasNumberingSystems()); + if (shouldShowLocaleEditor) { + extra.putBoolean(EXTRA_RESULT_LOCALE, true); + } + + new SubSettingLauncher(mContext) + .setDestination(fragmentName) + .setSourceMetricsCategory(Instrumentable.METRICS_CATEGORY_UNKNOWN) + .setArguments(extra) + .launch(); + } + + private boolean shouldShowLocaleEditor(LocaleStore.LocaleInfo localeInfo) { + boolean isSystemLocale = localeInfo.isSystemLocale(); + boolean isRegionLocale = localeInfo.getParent() != null; + boolean mayHaveDifferentNumberingSystem = localeInfo.hasNumberingSystems(); + mLocaleList = getLocaleCollectorController(mContext).getSupportedLocaleList(localeInfo, + false, localeInfo != null); + Log.d(TAG, + "shouldShowLocaleEditor: isSystemLocale = " + isSystemLocale + ", isRegionLocale = " + + isRegionLocale + ", mayHaveDifferentNumberingSystem = " + + mayHaveDifferentNumberingSystem + ", isSuggested = " + + localeInfo.isSuggested() + ", isNumberingMode = " + isNumberingMode()); + + return mLocaleList.size() == 1 || isSystemLocale || localeInfo.isSuggested() + || (isRegionLocale && !mayHaveDifferentNumberingSystem) + || isNumberingMode(); + } } diff --git a/src/com/android/settings/localepicker/RegionAndNumberingSystemPickerFragment.java b/src/com/android/settings/localepicker/RegionAndNumberingSystemPickerFragment.java index 171df6a3d39..83c87b03499 100644 --- a/src/com/android/settings/localepicker/RegionAndNumberingSystemPickerFragment.java +++ b/src/com/android/settings/localepicker/RegionAndNumberingSystemPickerFragment.java @@ -55,19 +55,41 @@ import java.util.Set; * default locale.

*/ public class RegionAndNumberingSystemPickerFragment extends DashboardFragment { - private static final String TAG = "RegionAndNumberingSystemPickerFragment"; + public static final String EXTRA_TARGET_LOCALE = "extra_target_locale"; + public static final String EXTRA_IS_NUMBERING_SYSTEM = "extra_is_numbering_system"; + + private static final String TAG = "RegionAndNumberingSystemPickerFragment"; + private static final String KEY_PREFERENCE_SYSTEM_LOCALE_LIST = "system_locale_list"; + private static final String KEY_PREFERENCE_SYSTEM_LOCALE_SUGGESTED_LIST = + "system_locale_suggested_list"; + + @Nullable + private SystemLocaleAllListPreferenceController mSystemLocaleAllListPreferenceController; + @Nullable + private SystemLocaleSuggestedListPreferenceController mSuggestedListPreferenceController; + @Nullable + private LocaleStore.LocaleInfo mLocaleInfo; private RecyclerView mRecyclerView; private AppBarLayout mAppBarLayout; private Activity mActivity; + private boolean mIsNumberingMode; @Override public void onCreate(@NonNull Bundle icicle) { super.onCreate(icicle); mActivity = getActivity(); - if (mActivity.isFinishing()) { + if (mActivity == null || mActivity.isFinishing()) { + Log.d(TAG, "onCreate, no activity or activity is finishing"); return; } + + if (mLocaleInfo == null) { + Log.d(TAG, "onCreate, can not get localeInfo"); + return; + } + + mActivity.setTitle(mLocaleInfo.getFullNameNative()); } @Override @@ -102,8 +124,15 @@ public class RegionAndNumberingSystemPickerFragment extends DashboardFragment { private List buildPreferenceControllers( @NonNull Context context, @Nullable Lifecycle lifecycle) { final List controllers = new ArrayList<>(); - - // TODO: b/30358431 - Add preference of region locales. + mLocaleInfo = (LocaleStore.LocaleInfo) getArguments().getSerializable(EXTRA_TARGET_LOCALE); + mIsNumberingMode = getArguments().getBoolean(EXTRA_IS_NUMBERING_SYSTEM); + mSuggestedListPreferenceController = new SystemLocaleSuggestedListPreferenceController( + context, KEY_PREFERENCE_SYSTEM_LOCALE_SUGGESTED_LIST, mLocaleInfo, + mIsNumberingMode); + mSystemLocaleAllListPreferenceController = new SystemLocaleAllListPreferenceController( + context, KEY_PREFERENCE_SYSTEM_LOCALE_LIST, mLocaleInfo, mIsNumberingMode); + controllers.add(mSuggestedListPreferenceController); + controllers.add(mSystemLocaleAllListPreferenceController); return controllers; } diff --git a/src/com/android/settings/localepicker/SystemLocaleAllListPreferenceController.java b/src/com/android/settings/localepicker/SystemLocaleAllListPreferenceController.java new file mode 100644 index 00000000000..d4d87fa2c2b --- /dev/null +++ b/src/com/android/settings/localepicker/SystemLocaleAllListPreferenceController.java @@ -0,0 +1,87 @@ +/** + * 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.localepicker; + +import android.content.Context; +import android.os.LocaleList; + +import com.android.internal.app.LocaleCollectorBase; +import com.android.internal.app.LocaleStore; +import com.android.internal.app.SystemLocaleCollector; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class SystemLocaleAllListPreferenceController extends + LocalePickerBaseListPreferenceController { + private static final String KEY_PREFERENCE_CATEGORY_ADD_LANGUAGE_ALL_SUPPORTED = + "system_language_all_supported_category"; + private static final String KEY_PREFERENCE_SYSTEM_LOCALE_LIST = "system_locale_list"; + + private boolean mIsNumberingSystemMode; + @Nullable private LocaleStore.LocaleInfo mLocaleInfo; + @Nullable private LocaleList mExplicitLocales; + + public SystemLocaleAllListPreferenceController(@NonNull Context context, + @NonNull String preferenceKey) { + super(context, preferenceKey); + } + + public SystemLocaleAllListPreferenceController(@NonNull Context context, + @NonNull String preferenceKey, @NonNull LocaleStore.LocaleInfo parentLocale, + boolean isNumberingSystemMode) { + super(context, preferenceKey); + mLocaleInfo = parentLocale; + mIsNumberingSystemMode = isNumberingSystemMode; + } + + public SystemLocaleAllListPreferenceController(@NonNull Context context, + @NonNull String preferenceKey, @Nullable LocaleList explicitLocales) { + super(context, preferenceKey); + mExplicitLocales = explicitLocales; + } + + @Override + protected String getPreferenceCategoryKey() { + return KEY_PREFERENCE_CATEGORY_ADD_LANGUAGE_ALL_SUPPORTED; + } + + @Override + public @NonNull String getPreferenceKey() { + return KEY_PREFERENCE_SYSTEM_LOCALE_LIST; + } + + @Override + protected LocaleCollectorBase getLocaleCollectorController(Context context) { + return new SystemLocaleCollector(context, getExplicitLocaleList()); + } + + @Override + protected @Nullable LocaleStore.LocaleInfo getParentLocale() { + return mLocaleInfo; + } + + @Override + protected boolean isNumberingMode() { + return mIsNumberingSystemMode; + } + + @Override + protected @Nullable LocaleList getExplicitLocaleList() { + return mExplicitLocales; + } +} diff --git a/src/com/android/settings/localepicker/SystemLocalePickerFragment.java b/src/com/android/settings/localepicker/SystemLocalePickerFragment.java index df3ae8454f4..5a007318a55 100644 --- a/src/com/android/settings/localepicker/SystemLocalePickerFragment.java +++ b/src/com/android/settings/localepicker/SystemLocalePickerFragment.java @@ -20,6 +20,8 @@ import android.app.Activity; import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; +import android.os.LocaleList; +import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; @@ -48,8 +50,6 @@ import com.android.settingslib.core.lifecycle.Lifecycle; import com.google.android.material.appbar.AppBarLayout; -import org.jetbrains.annotations.NotNull; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -68,12 +68,24 @@ public class SystemLocalePickerFragment extends DashboardFragment implements private static final String TAG = "SystemLocalePickerFragment"; private static final String EXTRA_EXPAND_SEARCH_VIEW = "expand_search_view"; + private static final String KEY_PREFERENCE_SYSTEM_LOCALE_LIST = "system_locale_list"; + private static final String KEY_PREFERENCE_SYSTEM_LOCALE_SUGGESTED_LIST = + "system_locale_suggested_list"; - @Nullable private SearchView mSearchView = null; - @Nullable private SearchFilter mSearchFilter = null; - @Nullable private Set mLocaleList; - @Nullable private List mLocaleOptions; - @Nullable private List mOriginalLocaleInfos; + @Nullable + private SearchView mSearchView = null; + @Nullable + private SearchFilter mSearchFilter = null; + @Nullable + private Set mLocaleList; + @Nullable + private List mLocaleOptions; + @Nullable + private List mOriginalLocaleInfos; + @Nullable + private SystemLocaleAllListPreferenceController mSystemLocaleAllListPreferenceController; + @Nullable + private SystemLocaleSuggestedListPreferenceController mSuggestedListPreferenceController; private AppBarLayout mAppBarLayout; private RecyclerView mRecyclerView; private Activity mActivity; @@ -138,12 +150,16 @@ public class SystemLocalePickerFragment extends DashboardFragment implements } private void filterSearch(@Nullable String query) { + if (mSystemLocaleAllListPreferenceController == null) { + Log.d(TAG, "filterSearch(), can not get preference."); + return; + } + if (mSearchFilter == null) { mSearchFilter = new SearchFilter(); } - // TODO: b/30358431 - Add preference of system locales. - // mOriginalLocaleInfos = mSystemLocaleAllListPreferenceController.getSupportedLocaleList(); + mOriginalLocaleInfos = mSystemLocaleAllListPreferenceController.getSupportedLocaleList(); // If we haven't load apps list completely, don't filter anything. if (mOriginalLocaleInfos == null) { Log.w(TAG, "Locales haven't loaded completely yet, so nothing can be filtered"); @@ -195,14 +211,19 @@ public class SystemLocalePickerFragment extends DashboardFragment implements @Override protected void publishResults(CharSequence constraint, FilterResults results) { + if (mSystemLocaleAllListPreferenceController == null + || mSuggestedListPreferenceController == null) { + Log.d(TAG, "publishResults(), can not get preference."); + return; + } + mLocaleOptions = (ArrayList) results.values; // Need to scroll to first preference when searching. if (mRecyclerView != null) { mRecyclerView.post(() -> mRecyclerView.scrollToPosition(0)); } - // TODO: b/30358431 - Add preference of system locales. - // mSystemLocaleAllListPreferenceController.onSearchListChanged(mLocaleOptions); - // mSuggestedListPreferenceController.onSearchListChanged(mLocaleOptions); + mSystemLocaleAllListPreferenceController.onSearchListChanged(mLocaleOptions); + mSuggestedListPreferenceController.onSearchListChanged(mLocaleOptions); } // TODO: decide if this is enough, or we want to use a BreakIterator... @@ -271,11 +292,31 @@ public class SystemLocalePickerFragment extends DashboardFragment implements private List buildPreferenceControllers( @NonNull Context context, @Nullable Lifecycle lifecycle) { + LocaleList explicitLocales = null; + if (isDeviceDemoMode()) { + Bundle bundle = getIntent().getExtras(); + explicitLocales = bundle == null + ? null + : bundle.getParcelable(Settings.EXTRA_EXPLICIT_LOCALES, LocaleList.class); + Log.i(TAG, "Has explicit locales : " + explicitLocales); + } + mSuggestedListPreferenceController = + new SystemLocaleSuggestedListPreferenceController(context, + KEY_PREFERENCE_SYSTEM_LOCALE_SUGGESTED_LIST); + mSystemLocaleAllListPreferenceController = new SystemLocaleAllListPreferenceController( + context, KEY_PREFERENCE_SYSTEM_LOCALE_LIST, explicitLocales); final List controllers = new ArrayList<>(); - // TODO: b/30358431 - Add preference of system locales. + controllers.add(mSuggestedListPreferenceController); + controllers.add(mSystemLocaleAllListPreferenceController); + return controllers; } + private boolean isDeviceDemoMode() { + return Settings.Global.getInt( + getContentResolver(), Settings.Global.DEVICE_DEMO_MODE, 0) == 1; + } + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider(R.xml.system_language_picker); } diff --git a/src/com/android/settings/localepicker/SystemLocaleSuggestedListPreferenceController.java b/src/com/android/settings/localepicker/SystemLocaleSuggestedListPreferenceController.java new file mode 100644 index 00000000000..d082bc09b2e --- /dev/null +++ b/src/com/android/settings/localepicker/SystemLocaleSuggestedListPreferenceController.java @@ -0,0 +1,81 @@ +/** + * 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.localepicker; + +import android.content.Context; +import android.os.LocaleList; + +import com.android.internal.app.LocaleCollectorBase; +import com.android.internal.app.LocaleStore; +import com.android.internal.app.SystemLocaleCollector; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class SystemLocaleSuggestedListPreferenceController extends + LocalePickerBaseListPreferenceController { + private static final String KEY_PREFERENCE_CATEGORY_ADD_A_LANGUAGE_SUGGESTED = + "system_language_suggested_category"; + private static final String KEY_PREFERENCE_SYSTEM_LOCALE_SUGGESTED_LIST = + "system_locale_suggested_list"; + + @Nullable private LocaleStore.LocaleInfo mLocaleInfo; + private boolean mIsNumberingSystemMode; + + public SystemLocaleSuggestedListPreferenceController(@NonNull Context context, + @NonNull String preferenceKey) { + super(context, preferenceKey); + } + + public SystemLocaleSuggestedListPreferenceController(@NonNull Context context, + @NonNull String preferenceKey, + @NonNull LocaleStore.LocaleInfo parentLocale, boolean isNumberingSystemMode) { + super(context, preferenceKey); + mLocaleInfo = parentLocale; + mIsNumberingSystemMode = isNumberingSystemMode; + } + + @Override + protected @NonNull String getPreferenceCategoryKey() { + return KEY_PREFERENCE_CATEGORY_ADD_A_LANGUAGE_SUGGESTED; + } + + @Override + public @NonNull String getPreferenceKey() { + return KEY_PREFERENCE_SYSTEM_LOCALE_SUGGESTED_LIST; + } + + @Override + protected LocaleCollectorBase getLocaleCollectorController(Context context) { + return new SystemLocaleCollector(context, getExplicitLocaleList()); + } + + @Override + protected @Nullable LocaleStore.LocaleInfo getParentLocale() { + return mLocaleInfo; + } + + @Override + protected boolean isNumberingMode() { + return mIsNumberingSystemMode; + } + + @Override + protected @Nullable LocaleList getExplicitLocaleList() { + return null; + } +} From a1d5f2544adde9f20679cbe2e250b70fcb43b94e Mon Sep 17 00:00:00 2001 From: David Liu Date: Mon, 18 Nov 2024 23:40:11 +0000 Subject: [PATCH 06/11] [Expressive design] SettingsMainSwitchPreference should not in the group - SettingsMainSwitchPreference already have rounded corner itself. - Don't need to have rounded background. Bug: 377266201 Test: visual test Flag: EXEMPT update library Change-Id: I3e336257bcbdb83a8e397bb006a00a33b543aae0 --- .../android/settings/widget/SettingsMainSwitchPreference.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/widget/SettingsMainSwitchPreference.java b/src/com/android/settings/widget/SettingsMainSwitchPreference.java index 17d5fc88915..5e0a5a722e7 100644 --- a/src/com/android/settings/widget/SettingsMainSwitchPreference.java +++ b/src/com/android/settings/widget/SettingsMainSwitchPreference.java @@ -32,6 +32,7 @@ import com.android.settings.widget.SettingsMainSwitchBar.OnBeforeCheckedChangeLi import com.android.settingslib.RestrictedPreferenceHelper; import com.android.settingslib.RestrictedPreferenceHelperProvider; import com.android.settingslib.core.instrumentation.SettingsJankMonitor; +import com.android.settingslib.widget.GroupSectionDividerMixin; import java.util.ArrayList; import java.util.List; @@ -42,7 +43,7 @@ import java.util.List; * to enable or disable the preferences on the page. */ public class SettingsMainSwitchPreference extends TwoStatePreference implements - OnCheckedChangeListener, RestrictedPreferenceHelperProvider { + OnCheckedChangeListener, RestrictedPreferenceHelperProvider, GroupSectionDividerMixin { private final List mBeforeCheckedChangeListeners = new ArrayList<>(); From 69c7f5dac580361792bbb189178653b297d06c8a Mon Sep 17 00:00:00 2001 From: Adam Bookatz Date: Mon, 18 Nov 2024 15:53:41 -0800 Subject: [PATCH 07/11] Don't let profiles open the UserSettings overflow Really, profiles aren't expected to open Settings at all. But if they do somehow, the overflow shouldn't appear. Bug: 352542820 Flag: EXEMPT bugfix Test: Try to access the overflow of the work profile by launching its Settings in adb shell am start --user 10 'com.android.settings/.Settings\$UserSettingsActivity' Change-Id: I5e4c095cda3e19fa5c63c2c550a526f5da8ec5c1 --- src/com/android/settings/users/UserSettings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java index 0881b29a3b3..589f512ac23 100644 --- a/src/com/android/settings/users/UserSettings.java +++ b/src/com/android/settings/users/UserSettings.java @@ -465,7 +465,7 @@ public class UserSettings extends SettingsPreferenceFragment public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { int pos = 0; if (!isCurrentUserAdmin() && (canSwitchUserNow() || Flags.newMultiuserSettingsUx()) - && !isCurrentUserGuest()) { + && !isCurrentUserGuest() && !mUserManager.isProfile()) { String nickname = mUserManager.getUserName(); MenuItem removeThisUser = menu.add(0, MENU_REMOVE_USER, pos++, getResources().getString(R.string.user_remove_user_menu, nickname)); From 83708d4499e94dce7e70dccbe795b4f748eabea3 Mon Sep 17 00:00:00 2001 From: Jacky Wang Date: Tue, 19 Nov 2024 13:31:57 +0800 Subject: [PATCH 08/11] [Catalyst] Update PreferenceLifecycleContext.notifyPreferenceChange Change the signature from metadata to preference key, so that the API could be used to notify other preferences on the same screen. NO_IFTTT=Catalyst only Bug: 332201912 Flag: com.android.settings.flags.catalyst Test: atest Change-Id: Ibdf5f9004471d5491a0be1e84365cbe9f136c373 --- .../datausage/DataSaverMainSwitchPreference.kt | 8 ++++++-- .../settings/datausage/DataSaverScreen.kt | 2 +- .../display/AdaptiveSleepPreference.kt | 4 ++-- .../display/BrightnessLevelPreference.kt | 13 +++++++------ .../display/PeakRefreshRateSwitchPreference.kt | 18 +++++++++--------- .../display/darkmode/DarkModeScreen.kt | 4 ++-- .../batterysaver/BatterySaverPreference.kt | 4 ++-- 7 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/com/android/settings/datausage/DataSaverMainSwitchPreference.kt b/src/com/android/settings/datausage/DataSaverMainSwitchPreference.kt index f20e2642261..de128e6401c 100644 --- a/src/com/android/settings/datausage/DataSaverMainSwitchPreference.kt +++ b/src/com/android/settings/datausage/DataSaverMainSwitchPreference.kt @@ -32,7 +32,7 @@ class DataSaverMainSwitchPreference(context: Context) : private var dataSaverBackendListener: DataSaverBackend.Listener? = null override val key - get() = "use_data_saver" + get() = KEY override val title get() = R.string.data_saver_switch_title @@ -46,7 +46,7 @@ class DataSaverMainSwitchPreference(context: Context) : ReadWritePermit.ALLOW override fun onStart(context: PreferenceLifecycleContext) { - val listener = DataSaverBackend.Listener { context.notifyPreferenceChange(this) } + val listener = DataSaverBackend.Listener { context.notifyPreferenceChange(KEY) } dataSaverBackendListener = listener dataSaverBackend.addListener(listener) } @@ -71,4 +71,8 @@ class DataSaverMainSwitchPreference(context: Context) : dataSaverBackend.isDataSaverEnabled = value as Boolean } } + + companion object { + const val KEY = "use_data_saver" + } } diff --git a/src/com/android/settings/datausage/DataSaverScreen.kt b/src/com/android/settings/datausage/DataSaverScreen.kt index a0b579c3294..2e806432833 100644 --- a/src/com/android/settings/datausage/DataSaverScreen.kt +++ b/src/com/android/settings/datausage/DataSaverScreen.kt @@ -71,7 +71,7 @@ class DataSaverScreen : override fun hasCompleteHierarchy() = false override fun onStart(context: PreferenceLifecycleContext) { - val listener = DataSaverBackend.Listener { context.notifyPreferenceChange(this) } + val listener = DataSaverBackend.Listener { context.notifyPreferenceChange(KEY) } dataSaverBackendListener = listener dataSaverBackend = DataSaverBackend(context).apply { addListener(listener) } } diff --git a/src/com/android/settings/display/AdaptiveSleepPreference.kt b/src/com/android/settings/display/AdaptiveSleepPreference.kt index f31959b1182..71d7749eb9a 100644 --- a/src/com/android/settings/display/AdaptiveSleepPreference.kt +++ b/src/com/android/settings/display/AdaptiveSleepPreference.kt @@ -101,7 +101,7 @@ class AdaptiveSleepPreference : val receiver = object : BroadcastReceiver() { override fun onReceive(receiverContext: Context, intent: Intent) { - context.notifyPreferenceChange(this@AdaptiveSleepPreference) + context.notifyPreferenceChange(KEY) } } context.registerReceiver( @@ -111,7 +111,7 @@ class AdaptiveSleepPreference : broadcastReceiver = receiver val listener = OnSensorPrivacyChangedListener { _, _ -> - context.notifyPreferenceChange(this) + context.notifyPreferenceChange(KEY) } SensorPrivacyManager.getInstance(context).addSensorPrivacyListener(CAMERA, listener) sensorPrivacyChangedListener = listener diff --git a/src/com/android/settings/display/BrightnessLevelPreference.kt b/src/com/android/settings/display/BrightnessLevelPreference.kt index c053dfeeb71..5e2f6f218d0 100644 --- a/src/com/android/settings/display/BrightnessLevelPreference.kt +++ b/src/com/android/settings/display/BrightnessLevelPreference.kt @@ -58,7 +58,7 @@ class BrightnessLevelPreference : private var displayListener: DisplayListener? = null override val key: String - get() = "brightness" + get() = KEY override val title: Int get() = R.string.brightness @@ -85,10 +85,7 @@ class BrightnessLevelPreference : } override fun onStart(context: PreferenceLifecycleContext) { - val observer = - KeyedObserver { _, _ -> - context.notifyPreferenceChange(this@BrightnessLevelPreference) - } + val observer = KeyedObserver { _, _ -> context.notifyPreferenceChange(KEY) } brightnessObserver = observer SettingsSystemStore.get(context) .addObserver(System.SCREEN_AUTO_BRIGHTNESS_ADJ, observer, HandlerExecutor.main) @@ -100,7 +97,7 @@ class BrightnessLevelPreference : override fun onDisplayRemoved(displayId: Int) {} override fun onDisplayChanged(displayId: Int) { - context.notifyPreferenceChange(this@BrightnessLevelPreference) + context.notifyPreferenceChange(KEY) } } displayListener = listener @@ -162,5 +159,9 @@ class BrightnessLevelPreference : value < GAMMA_SPACE_MIN -> 0.0 else -> (value - GAMMA_SPACE_MIN) / (GAMMA_SPACE_MAX - GAMMA_SPACE_MIN) } + + companion object { + const val KEY = "brightness" + } } // LINT.ThenChange(BrightnessLevelPreferenceController.java) diff --git a/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt b/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt index 6615ee93f6f..6dc09f6e1c3 100644 --- a/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt +++ b/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt @@ -39,7 +39,7 @@ import kotlin.math.roundToInt // LINT.IfChange class PeakRefreshRateSwitchPreference : - SwitchPreference(PEAK_REFRESH_RATE, R.string.peak_refresh_rate_title), + SwitchPreference(KEY, R.string.peak_refresh_rate_title), PreferenceAvailabilityProvider, PreferenceSummaryProvider, PreferenceLifecycleProvider { @@ -69,7 +69,7 @@ class PeakRefreshRateSwitchPreference : // KEY_PEAK_REFRESH_RATE_DEFAULT value could be added, changed, removed or // unchanged. // Just force a UI update for any case. - context.notifyPreferenceChange(this) + context.notifyPreferenceChange(KEY) } propertiesChangedListener = listener @@ -97,14 +97,13 @@ class PeakRefreshRateSwitchPreference : override fun contains(key: String) = settingsStore.contains(key) override fun getDefaultValue(key: String, valueType: Class): T? { - if (key != PEAK_REFRESH_RATE) return super.getDefaultValue(key, valueType) + if (key != KEY) return super.getDefaultValue(key, valueType) return context.defaultPeakRefreshRate.refreshRateAsBoolean(context) as T } override fun getValue(key: String, valueType: Class): T? { - if (key != PEAK_REFRESH_RATE) return null - val refreshRate = - settingsStore.getFloat(PEAK_REFRESH_RATE) ?: context.defaultPeakRefreshRate + if (key != KEY) return null + val refreshRate = settingsStore.getFloat(KEY) ?: context.defaultPeakRefreshRate return refreshRate.refreshRateAsBoolean(context) as T } @@ -113,12 +112,12 @@ class PeakRefreshRateSwitchPreference : override fun setValue(key: String, valueType: Class, value: T?) = when { - key != PEAK_REFRESH_RATE -> {} - value == null -> settingsStore.setFloat(PEAK_REFRESH_RATE, null) + key != KEY -> {} + value == null -> settingsStore.setFloat(KEY, null) else -> { val peakRefreshRate = if (value as Boolean) context.refreshRateIfON() else DEFAULT_REFRESH_RATE - settingsStore.setFloat(PEAK_REFRESH_RATE, peakRefreshRate) + settingsStore.setFloat(KEY, peakRefreshRate) } } @@ -130,6 +129,7 @@ class PeakRefreshRateSwitchPreference : } companion object { + const val KEY = PEAK_REFRESH_RATE private const val INVALIDATE_REFRESH_RATE: Float = -1f private val Context.peakRefreshRate: Float diff --git a/src/com/android/settings/display/darkmode/DarkModeScreen.kt b/src/com/android/settings/display/darkmode/DarkModeScreen.kt index 1b951bedd03..91667053f69 100644 --- a/src/com/android/settings/display/darkmode/DarkModeScreen.kt +++ b/src/com/android/settings/display/darkmode/DarkModeScreen.kt @@ -112,7 +112,7 @@ class DarkModeScreen : val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(receiverContext: Context, intent: Intent) { - context.notifyPreferenceChange(this@DarkModeScreen) + context.notifyPreferenceChange(KEY) } } context.registerReceiver( @@ -121,7 +121,7 @@ class DarkModeScreen : ) val darkModeObserver = DarkModeObserver(context) - darkModeObserver.subscribe { context.notifyPreferenceChange(this@DarkModeScreen) } + darkModeObserver.subscribe { context.notifyPreferenceChange(KEY) } fragmentStates[context] = FragmentState(broadcastReceiver, darkModeObserver) } diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreference.kt b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreference.kt index 5d5ba3ccee9..da401c55704 100644 --- a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreference.kt +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreference.kt @@ -59,13 +59,13 @@ class BatterySaverPreference : object : BatterySaverListener { override fun onPowerSaveModeChanged() { handler.postDelayed( - { context.notifyPreferenceChange(this@BatterySaverPreference) }, + { context.notifyPreferenceChange(KEY) }, SWITCH_ANIMATION_DURATION, ) } override fun onBatteryChanged(pluggedIn: Boolean) = - context.notifyPreferenceChange(this@BatterySaverPreference) + context.notifyPreferenceChange(KEY) } ) setListening(true) From c9017355d9467d0c05e82d3292679e4728d593e0 Mon Sep 17 00:00:00 2001 From: tomhsu Date: Tue, 19 Nov 2024 03:30:03 +0000 Subject: [PATCH 09/11] [Satellite] Add category UI for Satellite functions Flag: com.android.settings.flags.satellite_oem_settings_ux_migration Fix: b/378408939 Test: atest pass Test: make pass Change-Id: Id0f2ba047f4ee71a9ae398aa828978097717ce1a --- res/values/strings.xml | 2 + res/xml/mobile_network_settings.xml | 23 ++-- .../telephony/MobileNetworkSettings.java | 6 + ...eSettingsPreferenceCategoryController.java | 70 ++++++++++ ...tingsPreferenceCategoryControllerTest.java | 127 ++++++++++++++++++ 5 files changed, 220 insertions(+), 8 deletions(-) create mode 100644 src/com/android/settings/network/telephony/SatelliteSettingsPreferenceCategoryController.java create mode 100644 tests/unit/src/com/android/settings/network/telephony/SatelliteSettingsPreferenceCategoryControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index b62df6081ee..3d359b0cefc 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12212,6 +12212,8 @@ Can’t turn on %1$s To turn on %1$s, first end the satellite connection + + Satellite connectivity diff --git a/res/xml/mobile_network_settings.xml b/res/xml/mobile_network_settings.xml index 29e7c253943..fb20dceacd3 100644 --- a/res/xml/mobile_network_settings.xml +++ b/res/xml/mobile_network_settings.xml @@ -204,6 +204,21 @@ settings:enableCopying="true" settings:controller="com.android.settings.network.telephony.MobileNetworkEidPreferenceController"/> + + + + + + - - Date: Tue, 19 Nov 2024 10:04:25 +0000 Subject: [PATCH 10/11] Ignore PrivateSpace tests that are failing in pre-submits. These are to be investigated later. Flag: TEST_ONLY Bug: 378392936 Test: atest PrivateSpaceMaintainerTest Change-Id: I32a92399a0c49cbbdcc4494b746b95b168e0b132 --- .../settings/privatespace/PrivateSpaceMaintainerTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java index 7095235b18f..bfe4be2d54c 100644 --- a/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java +++ b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java @@ -51,6 +51,7 @@ import com.android.settings.privatespace.PrivateSpaceMaintainer.ErrorDeletingPri import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -234,6 +235,7 @@ public class PrivateSpaceMaintainerTest { * Tests that {@link PrivateSpaceMaintainer#lockPrivateSpace()} when PS exists and is running * locks the private profile. */ + @Ignore("Flaky pre-submit b/378392936") @Test public void lockPrivateSpace_psExistsAndPrivateProfileRunning_locksCreatedPrivateSpace() { mSetFlagsRule.enableFlags( @@ -253,6 +255,7 @@ public class PrivateSpaceMaintainerTest { * Tests that {@link PrivateSpaceMaintainer#lockPrivateSpace()} when PS exist and private * profile not running returns false. */ + @Ignore("Flaky pre-submit b/378392936") @Test public void lockPrivateSpace_psExistsAndPrivateProfileNotRunning_returnsFalse() { mSetFlagsRule.enableFlags( From 46da53099c2137e9a0a402b0072c9c133f8bb5d6 Mon Sep 17 00:00:00 2001 From: Matthew DeVore Date: Wed, 16 Oct 2024 19:42:48 +0000 Subject: [PATCH 11/11] clampPosition function for dragging display blocks Uses the algorithm in the prototype and the design doc (go/extend-cd-settings). Bug: b/352650922 Test: atest TopologyClampTest.kt Flag: com.android.settings.flags.display_topology_pane_in_display_list Change-Id: I8d8d4427f5d5dc069b1529f8ca6ac2cee259ee8e --- .../connecteddevice/display/TopologyClamp.kt | 111 ++++++++++++++ .../display/TopologyClampTest.kt | 144 ++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 src/com/android/settings/connecteddevice/display/TopologyClamp.kt create mode 100644 tests/robotests/src/com/android/settings/connecteddevice/display/TopologyClampTest.kt diff --git a/src/com/android/settings/connecteddevice/display/TopologyClamp.kt b/src/com/android/settings/connecteddevice/display/TopologyClamp.kt new file mode 100644 index 00000000000..237e634bd12 --- /dev/null +++ b/src/com/android/settings/connecteddevice/display/TopologyClamp.kt @@ -0,0 +1,111 @@ +/* + * 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 android.graphics.RectF +import kotlin.math.hypot + +// Unfortunately, in the world of IEEE 32-bit floats, A + X - X is not always == A +// For example: A = 1075.4271f +// C = 1249.2203f +// For example: - A - 173.79326f = - C +// However: - C + A = - 173.79321f +// So we need to keep track of how the movingDisplay block is attaching to otherDisplays throughout +// the calculations below. We cannot use the rect.left with its width as a proxy for rect.right. We +// have to save the "inner" or attached side and use the width or height to calculate the "external" +// side. + +/** A potential X position for the display to clamp at. */ +private class XCoor( + val left : Float, val right : Float, + + /** + * If present, the position of the display being attached to. If absent, indicates the X + * position is derived from the exact drag position. + */ + val attaching : RectF?, +) + +/** A potential Y position for the display to clamp at. */ +private class YCoor( + val top : Float, val bottom : Float, + + /** + * If present, the position of the display being attached to. If absent, indicates the Y + * position is derived from the exact drag position. + */ + val attaching : RectF?, +) + +/** + * Finds the optimal clamp position assuming the user has dragged the block to `movingDisplay`. + * + * @param otherDisplays positions of the stationary displays (every one not being dragged) + * @param movingDisplay the position the user is current holding the block during a drag + * + * @return the clamp position as a RectF, whose dimensions will match that of `movingDisplay` + */ +fun clampPosition(otherDisplays : Iterable, movingDisplay : RectF) : RectF { + val xCoors = otherDisplays.flatMap { + listOf( + // Attaching to left edge of `it` + XCoor(it.left - movingDisplay.width(), it.left, it), + // Attaching to right edge of `it` + XCoor(it.right, it.right + movingDisplay.width(), it), + ) + }.plusElement(XCoor(movingDisplay.left, movingDisplay.right, null)) + + val yCoors = otherDisplays.flatMap { + listOf( + // Attaching to the top edge of `it` + YCoor(it.top - movingDisplay.height(), it.top, it), + // Attaching to the bottom edge of `it` + YCoor(it.bottom, it.bottom + movingDisplay.height(), it), + ) + }.plusElement(YCoor(movingDisplay.top, movingDisplay.bottom, null)) + + class Cand(val x : XCoor, val y : YCoor) + + val candidateGrid = xCoors.flatMap { x -> yCoors.map { y -> Cand(x, y) }} + val hasAttachInRange = candidateGrid.filter { + if (it.x.attaching != null) { + // Attaching to a vertical (left or right) edge. The y range of dragging and + // stationary blocks must overlap. + it.y.top <= it.x.attaching.bottom && it.y.bottom >= it.x.attaching.top + } else if (it.y.attaching != null) { + // Attaching to a horizontal (top or bottom) edge. The x range of dragging and + // stationary blocks must overlap. + it.x.left <= it.y.attaching.right && it.x.right >= it.y.attaching.left + } else { + // Not attaching to another display's edge at all, so not a valid clamp position. + false + } + } + // Clamp positions closest to the user's drag position are best. Sort by increasing distance + // from it, so the best will be first. + val prioritized = hasAttachInRange.sortedBy { + hypot(it.x.left - movingDisplay.left, it.y.top - movingDisplay.top) + } + val notIntersectingAny = prioritized.asSequence() + .map { RectF(it.x.left, it.y.top, it.x.right, it.y.bottom) } + .filter { p -> otherDisplays.all { !RectF.intersects(p, it) } } + + // Note we return a copy of `movingDisplay` if there is no valid clamp position, which will only + // happen if `otherDisplays` is empty or has no valid rectangles. It may not be wise to rely on + // this behavior. + return notIntersectingAny.firstOrNull() ?: RectF(movingDisplay) +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/display/TopologyClampTest.kt b/tests/robotests/src/com/android/settings/connecteddevice/display/TopologyClampTest.kt new file mode 100644 index 00000000000..5ab9babda10 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/display/TopologyClampTest.kt @@ -0,0 +1,144 @@ +/* + * 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 android.graphics.PointF +import android.graphics.RectF +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class TopologyClampTest { + @Test + fun clampToSides() { + val start = RectF(6f, 0f, 16f, 10f) + val clamp1 = clampPosition(listOf(RectF(0f, 0f, 10f, 10f)), start) + assertEquals(RectF(10f, 0f, 20f, 10f), clamp1) + + val clamp2 = clampPosition(listOf(RectF(18f, 0f, 28f, 10f)), start) + assertEquals(RectF(8f, 0f, 18f, 10f), clamp2) + } + + @Test + fun clampToTopOrBottom() { + val start = RectF(0f, 6f, 10f, 16f) + val clamp1 = clampPosition(listOf(RectF(0f, 0f, 10f, 10f)), start) + assertEquals(RectF(0f, 10f, 10f, 20f), clamp1) + + val clamp2 = clampPosition(listOf(RectF(0f, 18f, 10f, 28f)), start) + assertEquals(RectF(0f, 8f, 10f, 18f), clamp2) + } + + @Test + fun clampToCloserSide() { + // Shift one pixel right. + val start = RectF(9f, 8f, 19f, 18f) + val clamp1 = clampPosition(listOf(RectF(0f, 0f, 10f, 10f)), start) + assertEquals(RectF(10f, 8f, 20f, 18f), clamp1) + + // Shift two pixels down. + start.set(7f, 8f, 17f, 18f) + val clamp2 = clampPosition(listOf(RectF(0f, 0f, 10f, 10f)), start) + assertEquals(RectF(7f, 10f, 17f, 20f), clamp2) + + // Shift three pixels left. + start.set(-7f, -6f, 3f, 4f); + val s3 = clampPosition(listOf(RectF(0f, 0f, 10f, 10f)), start) + assertEquals(RectF(-10f, -6f, 0f, 4f), s3) + } + + @Test + fun clampToCloserDisplayInCorner() { + val start = RectF(9f, 6f, 19f, 16f) + val clamp1 = clampPosition(listOf(RectF(0f, 0f, 8f, 8f), RectF(8f, 0f, 16f, 4f)), start) + assertEquals(RectF(8f, 6f, 18f, 16f), clamp1) + + start.set(10f, 5f, 20f, 15f) + val clamp2 = clampPosition(listOf(RectF(0f, 0f, 8f, 8f), RectF(8f, 0f, 16f, 4f)), start) + assertEquals(RectF(10f, 4f, 20f, 14f), clamp2) + } + + @Test + fun clampToSecondDisplayToAvoidOverlap() { + val start = RectF(8f, 3f, 18f, 13f) + val clamp = clampPosition(listOf(RectF(0f, 0f, 8f, 8f), RectF(8f, 0f, 16f, 4f)), start) + assertEquals(RectF(8f, 4f, 18f, 14f), clamp) + } + + @Test + fun clampToInnerCorner() { + val start = RectF(4f, 4f, 14f, 14f) + val clamp = clampPosition(listOf(RectF(5f, 0f, 10f, 5f), RectF(0f, 5f, 5f, 10f)), start) + assertEquals(RectF(5f, 5f, 15f, 15f), clamp) + } + + @Test + fun mustBeAdjacent() { + val start = RectF(9f, 10f, 14f, 15f) + + // Have candidate X, Y pair that is not adjacent to any display. + val clamp = clampPosition(listOf(RectF(5f, 0f, 10f, 5f), RectF(0f, 5f, 5f, 10f)), start) + assertEquals(RectF(5f, 10f, 10f, 15f), clamp) + } + + @Test + fun mustNotIntersect() { + // 1 and 2 are attached with 1/3 of their respective sides. Attempt to drag the other + // display to 1's lower-right corner. It should be forced to the right side of 2. + //111 + //111 + //111 + // 222 + // 222 + // 222 + + val start = RectF(30f, 30f, 60f, 60f) + val clamp = clampPosition(listOf(RectF(0f, 0f, 30f, 30f), RectF(20f, 30f, 50f, 60f)), start) + assertEquals(RectF(50f, 30f, 80f, 60f), clamp) + } + + @Test + fun attachingToTwoRectsAtOnce() { + // 2 is being dragged and starts out overlapping 0 and 1, then it is + // clamped to the right side of 0 and the bottom of 1 at the same time. + // + //00 + //002 + // 2 + // 11 + // 11 + + val clamp = clampPosition( + listOf(RectF(0f, 0f, 20f, 20f), RectF(10f, 30f, 30f, 50f)), + RectF(10f, 11f, 20f, 31f)) + + assertEquals(RectF(20f, 10f, 30f, 30f), clamp) + } + + @Test + fun attachingToTwoRectsAtOnceAxisSwapped() { + // Same as previous but with x and y swapped. + + val clamp = clampPosition( + listOf(RectF(0f, 0f, 20f, 20f), RectF(30f, 10f, 50f, 30f)), + RectF(11f, 10f, 31f, 20f)) + + assertEquals(RectF(10f, 20f, 30f, 30f), clamp) + } +}