Loading packages/SystemUI/AndroidManifest.xml +9 −0 Original line number Original line Diff line number Diff line Loading @@ -347,6 +347,15 @@ <uses-permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT" /> <uses-permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT" /> <!-- Intent Chooser --> <permission android:name="android.permission.ADD_CHOOSER_PINS" android:protectionLevel="signature" /> <uses-permission android:name="android.permission.ADD_CHOOSER_PINS" /> <permission android:name="android.permission.RECEIVE_CHOOSER_PIN_MIGRATION" android:protectionLevel="signature" /> <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" /> <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" /> Loading packages/SystemUI/src/com/android/systemui/ChooserPinMigration.kt 0 → 100644 +140 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2023 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.systemui import android.content.ComponentName import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.Intent import android.content.SharedPreferences import android.os.Bundle import android.os.Environment import android.os.storage.StorageManager import android.util.Log import androidx.core.util.Supplier import com.android.internal.R import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import java.io.File import javax.inject.Inject /** * Performs a migration of pinned targets to the unbundled chooser if legacy data exists. * * Sends an explicit broadcast with the contents of the legacy pin preferences. The broadcast is * protected by the RECEIVE_CHOOSER_PIN_MIGRATION permission. This class requires the * ADD_CHOOSER_PINS permission in order to be able to send this broadcast. */ class ChooserPinMigration @Inject constructor( private val context: Context, private val featureFlags: FeatureFlags, private val broadcastSender: BroadcastSender, legacyPinPrefsFileSupplier: LegacyPinPrefsFileSupplier, ) : CoreStartable { private val legacyPinPrefsFile = legacyPinPrefsFileSupplier.get() private val chooserComponent = ComponentName.unflattenFromString( context.resources.getString(R.string.config_chooserActivity) ) override fun start() { if (migrationIsRequired()) { doMigration() } } private fun migrationIsRequired(): Boolean { return featureFlags.isEnabled(Flags.CHOOSER_MIGRATION_ENABLED) && legacyPinPrefsFile.exists() && chooserComponent?.packageName != null } private fun doMigration() { Log.i(TAG, "Beginning migration") val legacyPinPrefs = context.getSharedPreferences(legacyPinPrefsFile, MODE_PRIVATE) if (legacyPinPrefs.all.isEmpty()) { Log.i(TAG, "No data to migrate, deleting legacy file") } else { sendSharedPreferences(legacyPinPrefs) Log.i(TAG, "Legacy data sent, deleting legacy preferences") val legacyPinPrefsEditor = legacyPinPrefs.edit() legacyPinPrefsEditor.clear() if (!legacyPinPrefsEditor.commit()) { Log.e(TAG, "Failed to delete legacy preferences") return } } if (!legacyPinPrefsFile.delete()) { Log.e(TAG, "Legacy preferences deleted, but failed to delete legacy preferences file") return } Log.i(TAG, "Legacy preference deletion complete") } private fun sendSharedPreferences(sharedPreferences: SharedPreferences) { val bundle = Bundle() sharedPreferences.all.entries.forEach { (key, value) -> when (value) { is Boolean -> bundle.putBoolean(key, value) else -> Log.e(TAG, "Unsupported preference type for $key: ${value?.javaClass}") } } sendBundle(bundle) } private fun sendBundle(bundle: Bundle) { val intent = Intent().apply { `package` = chooserComponent?.packageName!! action = BROADCAST_ACTION putExtras(bundle) } broadcastSender.sendBroadcast(intent, BROADCAST_PERMISSION) } companion object { private const val TAG = "PinnedShareTargetMigration" private const val BROADCAST_ACTION = "android.intent.action.CHOOSER_PIN_MIGRATION" private const val BROADCAST_PERMISSION = "android.permission.RECEIVE_CHOOSER_PIN_MIGRATION" class LegacyPinPrefsFileSupplier @Inject constructor(private val context: Context) : Supplier<File> { override fun get(): File { val packageDirectory = Environment.getDataUserCePackageDirectory( StorageManager.UUID_PRIVATE_INTERNAL, context.userId, context.packageName, ) val sharedPrefsDirectory = File(packageDirectory, "shared_prefs") return File(sharedPrefsDirectory, "chooser_pin_settings.xml") } } } } packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +8 −0 Original line number Original line Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.dagger package com.android.systemui.dagger import com.android.keyguard.KeyguardBiometricLockoutLogger import com.android.keyguard.KeyguardBiometricLockoutLogger import com.android.systemui.ChooserPinMigration import com.android.systemui.ChooserSelector import com.android.systemui.ChooserSelector import com.android.systemui.CoreStartable import com.android.systemui.CoreStartable import com.android.systemui.LatencyTester import com.android.systemui.LatencyTester Loading Loading @@ -75,6 +76,13 @@ abstract class SystemUICoreStartableModule { @ClassKey(AuthController::class) @ClassKey(AuthController::class) abstract fun bindAuthController(service: AuthController): CoreStartable abstract fun bindAuthController(service: AuthController): CoreStartable /** Inject into ChooserPinMigration. */ @Binds @IntoMap @ClassKey(ChooserPinMigration::class) @PerUser abstract fun bindChooserPinMigration(sysui: ChooserPinMigration): CoreStartable /** Inject into ChooserCoreStartable. */ /** Inject into ChooserCoreStartable. */ @Binds @Binds @IntoMap @IntoMap Loading packages/SystemUI/src/com/android/systemui/flags/Flags.kt +3 −0 Original line number Original line Diff line number Diff line Loading @@ -610,6 +610,9 @@ object Flags { val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW = val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW = releasedFlag(1504, "sharesheet_scrollable_image_preview") releasedFlag(1504, "sharesheet_scrollable_image_preview") // TODO(b/274137694) Tracking Bug val CHOOSER_MIGRATION_ENABLED = unreleasedFlag(1505, "chooser_migration_enabled") // 1700 - clipboard // 1700 - clipboard @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior") @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior") // TODO(b/267162944): Tracking bug // TODO(b/267162944): Tracking bug Loading packages/SystemUI/tests/src/com/android/systemui/ChooserPinMigrationTest.kt 0 → 100644 +255 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2023 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.systemui import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.res.Resources import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.io.File import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest class ChooserPinMigrationTest : SysuiTestCase() { private val fakeFeatureFlags = FakeFeatureFlags() private val fakePreferences = mutableMapOf( "TestPinnedPackage/TestPinnedClass" to true, "TestUnpinnedPackage/TestUnpinnedClass" to false, ) private val intent = kotlinArgumentCaptor<Intent>() private val permission = kotlinArgumentCaptor<String>() private lateinit var chooserPinMigration: ChooserPinMigration @Mock private lateinit var mockContext: Context @Mock private lateinit var mockResources: Resources @Mock private lateinit var mockLegacyPinPrefsFileSupplier: ChooserPinMigration.Companion.LegacyPinPrefsFileSupplier @Mock private lateinit var mockFile: File @Mock private lateinit var mockSharedPreferences: SharedPreferences @Mock private lateinit var mockSharedPreferencesEditor: SharedPreferences.Editor @Mock private lateinit var mockBroadcastSender: BroadcastSender @Before fun setup() { MockitoAnnotations.initMocks(this) whenever(mockContext.resources).thenReturn(mockResources) whenever(mockContext.getSharedPreferences(any<File>(), anyInt())) .thenReturn(mockSharedPreferences) whenever(mockResources.getString(anyInt())).thenReturn("TestPackage/TestClass") whenever(mockSharedPreferences.all).thenReturn(fakePreferences) whenever(mockSharedPreferences.edit()).thenReturn(mockSharedPreferencesEditor) whenever(mockSharedPreferencesEditor.commit()).thenReturn(true) whenever(mockLegacyPinPrefsFileSupplier.get()).thenReturn(mockFile) whenever(mockFile.exists()).thenReturn(true) whenever(mockFile.delete()).thenReturn(true) fakeFeatureFlags.set(Flags.CHOOSER_MIGRATION_ENABLED, true) } @Test fun start_performsMigration() { // Arrange chooserPinMigration = ChooserPinMigration( mockContext, fakeFeatureFlags, mockBroadcastSender, mockLegacyPinPrefsFileSupplier, ) // Act chooserPinMigration.start() // Assert verify(mockBroadcastSender).sendBroadcast(intent.capture(), permission.capture()) assertThat(intent.value.action).isEqualTo("android.intent.action.CHOOSER_PIN_MIGRATION") assertThat(intent.value.`package`).isEqualTo("TestPackage") assertThat(intent.value.extras?.keySet()).hasSize(2) assertThat(intent.value.hasExtra("TestPinnedPackage/TestPinnedClass")).isTrue() assertThat(intent.value.getBooleanExtra("TestPinnedPackage/TestPinnedClass", false)) .isTrue() assertThat(intent.value.hasExtra("TestUnpinnedPackage/TestUnpinnedClass")).isTrue() assertThat(intent.value.getBooleanExtra("TestUnpinnedPackage/TestUnpinnedClass", true)) .isFalse() assertThat(permission.value).isEqualTo("android.permission.RECEIVE_CHOOSER_PIN_MIGRATION") // Assert verify(mockSharedPreferencesEditor).clear() verify(mockSharedPreferencesEditor).commit() // Assert verify(mockFile).delete() } @Test fun start_doesNotDeleteLegacyPreferencesFile_whenClearingItFails() { // Arrange whenever(mockSharedPreferencesEditor.commit()).thenReturn(false) chooserPinMigration = ChooserPinMigration( mockContext, fakeFeatureFlags, mockBroadcastSender, mockLegacyPinPrefsFileSupplier, ) // Act chooserPinMigration.start() // Assert verify(mockBroadcastSender).sendBroadcast(intent.capture(), permission.capture()) assertThat(intent.value.action).isEqualTo("android.intent.action.CHOOSER_PIN_MIGRATION") assertThat(intent.value.`package`).isEqualTo("TestPackage") assertThat(intent.value.extras?.keySet()).hasSize(2) assertThat(intent.value.hasExtra("TestPinnedPackage/TestPinnedClass")).isTrue() assertThat(intent.value.getBooleanExtra("TestPinnedPackage/TestPinnedClass", false)) .isTrue() assertThat(intent.value.hasExtra("TestUnpinnedPackage/TestUnpinnedClass")).isTrue() assertThat(intent.value.getBooleanExtra("TestUnpinnedPackage/TestUnpinnedClass", true)) .isFalse() assertThat(permission.value).isEqualTo("android.permission.RECEIVE_CHOOSER_PIN_MIGRATION") // Assert verify(mockSharedPreferencesEditor).clear() verify(mockSharedPreferencesEditor).commit() // Assert verify(mockFile, never()).delete() } @Test fun start_OnlyDeletesLegacyPreferencesFile_whenEmpty() { // Arrange whenever(mockSharedPreferences.all).thenReturn(emptyMap()) chooserPinMigration = ChooserPinMigration( mockContext, fakeFeatureFlags, mockBroadcastSender, mockLegacyPinPrefsFileSupplier, ) // Act chooserPinMigration.start() // Assert verifyZeroInteractions(mockBroadcastSender) // Assert verifyZeroInteractions(mockSharedPreferencesEditor) // Assert verify(mockFile).delete() } @Test fun start_DoesNotDoMigration_whenFlagIsDisabled() { // Arrange fakeFeatureFlags.set(Flags.CHOOSER_MIGRATION_ENABLED, false) chooserPinMigration = ChooserPinMigration( mockContext, fakeFeatureFlags, mockBroadcastSender, mockLegacyPinPrefsFileSupplier, ) // Act chooserPinMigration.start() // Assert verifyZeroInteractions(mockBroadcastSender) // Assert verifyZeroInteractions(mockSharedPreferencesEditor) // Assert verify(mockFile, never()).delete() } @Test fun start_DoesNotDoMigration_whenLegacyPreferenceFileNotPresent() { // Arrange whenever(mockFile.exists()).thenReturn(false) chooserPinMigration = ChooserPinMigration( mockContext, fakeFeatureFlags, mockBroadcastSender, mockLegacyPinPrefsFileSupplier, ) // Act chooserPinMigration.start() // Assert verifyZeroInteractions(mockBroadcastSender) // Assert verifyZeroInteractions(mockSharedPreferencesEditor) // Assert verify(mockFile, never()).delete() } @Test fun start_DoesNotDoMigration_whenConfiguredChooserComponentIsInvalid() { // Arrange whenever(mockResources.getString(anyInt())).thenReturn("InvalidComponent") chooserPinMigration = ChooserPinMigration( mockContext, fakeFeatureFlags, mockBroadcastSender, mockLegacyPinPrefsFileSupplier, ) // Act chooserPinMigration.start() // Assert verifyZeroInteractions(mockBroadcastSender) // Assert verifyZeroInteractions(mockSharedPreferencesEditor) // Assert verify(mockFile, never()).delete() } } Loading
packages/SystemUI/AndroidManifest.xml +9 −0 Original line number Original line Diff line number Diff line Loading @@ -347,6 +347,15 @@ <uses-permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT" /> <uses-permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT" /> <!-- Intent Chooser --> <permission android:name="android.permission.ADD_CHOOSER_PINS" android:protectionLevel="signature" /> <uses-permission android:name="android.permission.ADD_CHOOSER_PINS" /> <permission android:name="android.permission.RECEIVE_CHOOSER_PIN_MIGRATION" android:protectionLevel="signature" /> <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" /> <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" /> Loading
packages/SystemUI/src/com/android/systemui/ChooserPinMigration.kt 0 → 100644 +140 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2023 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.systemui import android.content.ComponentName import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.Intent import android.content.SharedPreferences import android.os.Bundle import android.os.Environment import android.os.storage.StorageManager import android.util.Log import androidx.core.util.Supplier import com.android.internal.R import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import java.io.File import javax.inject.Inject /** * Performs a migration of pinned targets to the unbundled chooser if legacy data exists. * * Sends an explicit broadcast with the contents of the legacy pin preferences. The broadcast is * protected by the RECEIVE_CHOOSER_PIN_MIGRATION permission. This class requires the * ADD_CHOOSER_PINS permission in order to be able to send this broadcast. */ class ChooserPinMigration @Inject constructor( private val context: Context, private val featureFlags: FeatureFlags, private val broadcastSender: BroadcastSender, legacyPinPrefsFileSupplier: LegacyPinPrefsFileSupplier, ) : CoreStartable { private val legacyPinPrefsFile = legacyPinPrefsFileSupplier.get() private val chooserComponent = ComponentName.unflattenFromString( context.resources.getString(R.string.config_chooserActivity) ) override fun start() { if (migrationIsRequired()) { doMigration() } } private fun migrationIsRequired(): Boolean { return featureFlags.isEnabled(Flags.CHOOSER_MIGRATION_ENABLED) && legacyPinPrefsFile.exists() && chooserComponent?.packageName != null } private fun doMigration() { Log.i(TAG, "Beginning migration") val legacyPinPrefs = context.getSharedPreferences(legacyPinPrefsFile, MODE_PRIVATE) if (legacyPinPrefs.all.isEmpty()) { Log.i(TAG, "No data to migrate, deleting legacy file") } else { sendSharedPreferences(legacyPinPrefs) Log.i(TAG, "Legacy data sent, deleting legacy preferences") val legacyPinPrefsEditor = legacyPinPrefs.edit() legacyPinPrefsEditor.clear() if (!legacyPinPrefsEditor.commit()) { Log.e(TAG, "Failed to delete legacy preferences") return } } if (!legacyPinPrefsFile.delete()) { Log.e(TAG, "Legacy preferences deleted, but failed to delete legacy preferences file") return } Log.i(TAG, "Legacy preference deletion complete") } private fun sendSharedPreferences(sharedPreferences: SharedPreferences) { val bundle = Bundle() sharedPreferences.all.entries.forEach { (key, value) -> when (value) { is Boolean -> bundle.putBoolean(key, value) else -> Log.e(TAG, "Unsupported preference type for $key: ${value?.javaClass}") } } sendBundle(bundle) } private fun sendBundle(bundle: Bundle) { val intent = Intent().apply { `package` = chooserComponent?.packageName!! action = BROADCAST_ACTION putExtras(bundle) } broadcastSender.sendBroadcast(intent, BROADCAST_PERMISSION) } companion object { private const val TAG = "PinnedShareTargetMigration" private const val BROADCAST_ACTION = "android.intent.action.CHOOSER_PIN_MIGRATION" private const val BROADCAST_PERMISSION = "android.permission.RECEIVE_CHOOSER_PIN_MIGRATION" class LegacyPinPrefsFileSupplier @Inject constructor(private val context: Context) : Supplier<File> { override fun get(): File { val packageDirectory = Environment.getDataUserCePackageDirectory( StorageManager.UUID_PRIVATE_INTERNAL, context.userId, context.packageName, ) val sharedPrefsDirectory = File(packageDirectory, "shared_prefs") return File(sharedPrefsDirectory, "chooser_pin_settings.xml") } } } }
packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +8 −0 Original line number Original line Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.dagger package com.android.systemui.dagger import com.android.keyguard.KeyguardBiometricLockoutLogger import com.android.keyguard.KeyguardBiometricLockoutLogger import com.android.systemui.ChooserPinMigration import com.android.systemui.ChooserSelector import com.android.systemui.ChooserSelector import com.android.systemui.CoreStartable import com.android.systemui.CoreStartable import com.android.systemui.LatencyTester import com.android.systemui.LatencyTester Loading Loading @@ -75,6 +76,13 @@ abstract class SystemUICoreStartableModule { @ClassKey(AuthController::class) @ClassKey(AuthController::class) abstract fun bindAuthController(service: AuthController): CoreStartable abstract fun bindAuthController(service: AuthController): CoreStartable /** Inject into ChooserPinMigration. */ @Binds @IntoMap @ClassKey(ChooserPinMigration::class) @PerUser abstract fun bindChooserPinMigration(sysui: ChooserPinMigration): CoreStartable /** Inject into ChooserCoreStartable. */ /** Inject into ChooserCoreStartable. */ @Binds @Binds @IntoMap @IntoMap Loading
packages/SystemUI/src/com/android/systemui/flags/Flags.kt +3 −0 Original line number Original line Diff line number Diff line Loading @@ -610,6 +610,9 @@ object Flags { val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW = val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW = releasedFlag(1504, "sharesheet_scrollable_image_preview") releasedFlag(1504, "sharesheet_scrollable_image_preview") // TODO(b/274137694) Tracking Bug val CHOOSER_MIGRATION_ENABLED = unreleasedFlag(1505, "chooser_migration_enabled") // 1700 - clipboard // 1700 - clipboard @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior") @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior") // TODO(b/267162944): Tracking bug // TODO(b/267162944): Tracking bug Loading
packages/SystemUI/tests/src/com/android/systemui/ChooserPinMigrationTest.kt 0 → 100644 +255 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2023 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.systemui import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.res.Resources import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.io.File import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest class ChooserPinMigrationTest : SysuiTestCase() { private val fakeFeatureFlags = FakeFeatureFlags() private val fakePreferences = mutableMapOf( "TestPinnedPackage/TestPinnedClass" to true, "TestUnpinnedPackage/TestUnpinnedClass" to false, ) private val intent = kotlinArgumentCaptor<Intent>() private val permission = kotlinArgumentCaptor<String>() private lateinit var chooserPinMigration: ChooserPinMigration @Mock private lateinit var mockContext: Context @Mock private lateinit var mockResources: Resources @Mock private lateinit var mockLegacyPinPrefsFileSupplier: ChooserPinMigration.Companion.LegacyPinPrefsFileSupplier @Mock private lateinit var mockFile: File @Mock private lateinit var mockSharedPreferences: SharedPreferences @Mock private lateinit var mockSharedPreferencesEditor: SharedPreferences.Editor @Mock private lateinit var mockBroadcastSender: BroadcastSender @Before fun setup() { MockitoAnnotations.initMocks(this) whenever(mockContext.resources).thenReturn(mockResources) whenever(mockContext.getSharedPreferences(any<File>(), anyInt())) .thenReturn(mockSharedPreferences) whenever(mockResources.getString(anyInt())).thenReturn("TestPackage/TestClass") whenever(mockSharedPreferences.all).thenReturn(fakePreferences) whenever(mockSharedPreferences.edit()).thenReturn(mockSharedPreferencesEditor) whenever(mockSharedPreferencesEditor.commit()).thenReturn(true) whenever(mockLegacyPinPrefsFileSupplier.get()).thenReturn(mockFile) whenever(mockFile.exists()).thenReturn(true) whenever(mockFile.delete()).thenReturn(true) fakeFeatureFlags.set(Flags.CHOOSER_MIGRATION_ENABLED, true) } @Test fun start_performsMigration() { // Arrange chooserPinMigration = ChooserPinMigration( mockContext, fakeFeatureFlags, mockBroadcastSender, mockLegacyPinPrefsFileSupplier, ) // Act chooserPinMigration.start() // Assert verify(mockBroadcastSender).sendBroadcast(intent.capture(), permission.capture()) assertThat(intent.value.action).isEqualTo("android.intent.action.CHOOSER_PIN_MIGRATION") assertThat(intent.value.`package`).isEqualTo("TestPackage") assertThat(intent.value.extras?.keySet()).hasSize(2) assertThat(intent.value.hasExtra("TestPinnedPackage/TestPinnedClass")).isTrue() assertThat(intent.value.getBooleanExtra("TestPinnedPackage/TestPinnedClass", false)) .isTrue() assertThat(intent.value.hasExtra("TestUnpinnedPackage/TestUnpinnedClass")).isTrue() assertThat(intent.value.getBooleanExtra("TestUnpinnedPackage/TestUnpinnedClass", true)) .isFalse() assertThat(permission.value).isEqualTo("android.permission.RECEIVE_CHOOSER_PIN_MIGRATION") // Assert verify(mockSharedPreferencesEditor).clear() verify(mockSharedPreferencesEditor).commit() // Assert verify(mockFile).delete() } @Test fun start_doesNotDeleteLegacyPreferencesFile_whenClearingItFails() { // Arrange whenever(mockSharedPreferencesEditor.commit()).thenReturn(false) chooserPinMigration = ChooserPinMigration( mockContext, fakeFeatureFlags, mockBroadcastSender, mockLegacyPinPrefsFileSupplier, ) // Act chooserPinMigration.start() // Assert verify(mockBroadcastSender).sendBroadcast(intent.capture(), permission.capture()) assertThat(intent.value.action).isEqualTo("android.intent.action.CHOOSER_PIN_MIGRATION") assertThat(intent.value.`package`).isEqualTo("TestPackage") assertThat(intent.value.extras?.keySet()).hasSize(2) assertThat(intent.value.hasExtra("TestPinnedPackage/TestPinnedClass")).isTrue() assertThat(intent.value.getBooleanExtra("TestPinnedPackage/TestPinnedClass", false)) .isTrue() assertThat(intent.value.hasExtra("TestUnpinnedPackage/TestUnpinnedClass")).isTrue() assertThat(intent.value.getBooleanExtra("TestUnpinnedPackage/TestUnpinnedClass", true)) .isFalse() assertThat(permission.value).isEqualTo("android.permission.RECEIVE_CHOOSER_PIN_MIGRATION") // Assert verify(mockSharedPreferencesEditor).clear() verify(mockSharedPreferencesEditor).commit() // Assert verify(mockFile, never()).delete() } @Test fun start_OnlyDeletesLegacyPreferencesFile_whenEmpty() { // Arrange whenever(mockSharedPreferences.all).thenReturn(emptyMap()) chooserPinMigration = ChooserPinMigration( mockContext, fakeFeatureFlags, mockBroadcastSender, mockLegacyPinPrefsFileSupplier, ) // Act chooserPinMigration.start() // Assert verifyZeroInteractions(mockBroadcastSender) // Assert verifyZeroInteractions(mockSharedPreferencesEditor) // Assert verify(mockFile).delete() } @Test fun start_DoesNotDoMigration_whenFlagIsDisabled() { // Arrange fakeFeatureFlags.set(Flags.CHOOSER_MIGRATION_ENABLED, false) chooserPinMigration = ChooserPinMigration( mockContext, fakeFeatureFlags, mockBroadcastSender, mockLegacyPinPrefsFileSupplier, ) // Act chooserPinMigration.start() // Assert verifyZeroInteractions(mockBroadcastSender) // Assert verifyZeroInteractions(mockSharedPreferencesEditor) // Assert verify(mockFile, never()).delete() } @Test fun start_DoesNotDoMigration_whenLegacyPreferenceFileNotPresent() { // Arrange whenever(mockFile.exists()).thenReturn(false) chooserPinMigration = ChooserPinMigration( mockContext, fakeFeatureFlags, mockBroadcastSender, mockLegacyPinPrefsFileSupplier, ) // Act chooserPinMigration.start() // Assert verifyZeroInteractions(mockBroadcastSender) // Assert verifyZeroInteractions(mockSharedPreferencesEditor) // Assert verify(mockFile, never()).delete() } @Test fun start_DoesNotDoMigration_whenConfiguredChooserComponentIsInvalid() { // Arrange whenever(mockResources.getString(anyInt())).thenReturn("InvalidComponent") chooserPinMigration = ChooserPinMigration( mockContext, fakeFeatureFlags, mockBroadcastSender, mockLegacyPinPrefsFileSupplier, ) // Act chooserPinMigration.start() // Assert verifyZeroInteractions(mockBroadcastSender) // Assert verifyZeroInteractions(mockSharedPreferencesEditor) // Assert verify(mockFile, never()).delete() } }