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

Commit a0ea150e authored by Automerger Merge Worker's avatar Automerger Merge Worker Committed by Android (Google) Code Review
Browse files

Merge "Merge "Adds ChooserPinMigration core startable" into udc-dev am: 7e0a03f0 am: 202df719"

parents 1c31d6a0 8f6c141b
Loading
Loading
Loading
Loading
+9 −0
Original line number Original line Diff line number Diff line
@@ -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" />
+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
@@ -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
+3 −0
Original line number Original line Diff line number Diff line
@@ -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
+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()
    }
}