Adds ChooserPinMigration core startable

This will perform a one-time migration of legacy Chooser pin preferences
to the new Chooser and then delete the old data. This change is guarded
by the CHOOSER_MIGRATION_ENABLED flag.

Test: atest ChooserPinMigrationTest
BUG: 223249318
Change-Id: Ifae9ac988a291f35c1dfaabda9d7694cb9d65542
This commit is contained in:
Govinda Wasserman
2023-03-17 14:12:20 -04:00
parent 5c84e498a9
commit 03cecc1be6
5 changed files with 415 additions and 0 deletions

View File

@@ -344,6 +344,15 @@
<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.UNREGISTER_SLICE_RECEIVER" />
<protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" />

View File

@@ -0,0 +1,140 @@
/*
* 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")
}
}
}
}

View File

@@ -17,6 +17,7 @@
package com.android.systemui.dagger
import com.android.keyguard.KeyguardBiometricLockoutLogger
import com.android.systemui.ChooserPinMigration
import com.android.systemui.ChooserSelector
import com.android.systemui.CoreStartable
import com.android.systemui.LatencyTester
@@ -76,6 +77,13 @@ abstract class SystemUICoreStartableModule {
@ClassKey(AuthController::class)
abstract fun bindAuthController(service: AuthController): CoreStartable
/** Inject into ChooserPinMigration. */
@Binds
@IntoMap
@ClassKey(ChooserPinMigration::class)
@PerUser
abstract fun bindChooserPinMigration(sysui: ChooserPinMigration): CoreStartable
/** Inject into ChooserCoreStartable. */
@Binds
@IntoMap

View File

@@ -613,6 +613,9 @@ object Flags {
val 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
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
// TODO(b/267162944): Tracking bug

View File

@@ -0,0 +1,255 @@
/*
* 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()
}
}