Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 03cecc1b authored by Govinda Wasserman's avatar Govinda Wasserman
Browse files

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
parent 5c84e498
Loading
Loading
Loading
Loading
+9 −0
Original line number Original line Diff line number Diff line
@@ -344,6 +344,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" />
+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")
            }
        }
    }
}
+8 −0
Original line number Original line Diff line number Diff line
@@ -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
@@ -76,6 +77,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
+3 −0
Original line number Original line Diff line number Diff line
@@ -613,6 +613,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
+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()
    }
}