diff options
13 files changed, 262 insertions, 178 deletions
diff --git a/java/src/com/android/intentresolver/ChooserHelper.kt b/java/src/com/android/intentresolver/ChooserHelper.kt index 6317ee1d..312911a6 100644 --- a/java/src/com/android/intentresolver/ChooserHelper.kt +++ b/java/src/com/android/intentresolver/ChooserHelper.kt @@ -18,6 +18,7 @@ package com.android.intentresolver import android.app.Activity import android.os.UserHandle +import android.provider.Settings import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.viewModels @@ -30,6 +31,7 @@ import com.android.intentresolver.annotation.JavaInterop import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ActivityResultRepository import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository import com.android.intentresolver.data.model.ChooserRequest +import com.android.intentresolver.platform.GlobalSettings import com.android.intentresolver.ui.viewmodel.ChooserViewModel import com.android.intentresolver.validation.Invalid import com.android.intentresolver.validation.Valid @@ -84,6 +86,7 @@ constructor( hostActivity: Activity, private val activityResultRepo: ActivityResultRepository, private val pendingSelectionCallbackRepo: PendingSelectionCallbackRepository, + private val globalSettings: GlobalSettings, ) : DefaultLifecycleObserver { // This is guaranteed by Hilt, since only a ComponentActivity is injectable. private val activity: ComponentActivity = hostActivity as ComponentActivity @@ -124,6 +127,12 @@ constructor( return } + if (globalSettings.getBooleanOrNull(Settings.Global.SECURE_FRP_MODE) == true) { + Log.e(TAG, "Sharing disabled due to active FRP lock.") + activity.finish() + return + } + when (val request = viewModel.initialRequest) { is Valid -> initializeActivity(request) is Invalid -> reportErrorsAndFinish(request) diff --git a/java/src/com/android/intentresolver/SecureSettings.kt b/java/src/com/android/intentresolver/SecureSettings.kt deleted file mode 100644 index 1e938895..00000000 --- a/java/src/com/android/intentresolver/SecureSettings.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.intentresolver - -import android.content.ContentResolver -import android.provider.Settings - -/** A proxy class for secure settings, for easier testing. */ -open class SecureSettings { - open fun getString(resolver: ContentResolver, name: String): String? { - return Settings.Secure.getString(resolver, name) - } -} diff --git a/java/src/com/android/intentresolver/platform/NearbyShareModule.kt b/java/src/com/android/intentresolver/platform/NearbyShareModule.kt index 6cb30b41..1e4b5241 100644 --- a/java/src/com/android/intentresolver/platform/NearbyShareModule.kt +++ b/java/src/com/android/intentresolver/platform/NearbyShareModule.kt @@ -41,7 +41,7 @@ object NearbyShareModule { fun nearbyShareComponent(@ApplicationOwned resources: Resources, settings: SecureSettings) = Optional.ofNullable( ComponentName.unflattenFromString( - settings.getString(NEARBY_SHARING_COMPONENT)?.ifEmpty { null } + settings.getStringOrNull(NEARBY_SHARING_COMPONENT)?.ifEmpty { null } ?: resources.getString(R.string.config_defaultNearbySharingComponent), ) ) diff --git a/java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt b/java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt deleted file mode 100644 index 0c802c97..00000000 --- a/java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.intentresolver.platform - -import android.content.ContentResolver -import android.provider.Settings -import javax.inject.Inject - -/** - * Implements [SecureSettings] backed by Settings.Secure and a ContentResolver. - * - * These methods make Binder calls and may block, so use on the Main thread should be avoided. - */ -class PlatformSecureSettings @Inject constructor(private val resolver: ContentResolver) : - SecureSettings { - - override fun getString(name: String): String? { - return Settings.Secure.getString(resolver, name) - } - - override fun getInt(name: String): Int? { - return runCatching { Settings.Secure.getInt(resolver, name) }.getOrNull() - } - - override fun getLong(name: String): Long? { - return runCatching { Settings.Secure.getLong(resolver, name) }.getOrNull() - } - - override fun getFloat(name: String): Float? { - return runCatching { Settings.Secure.getFloat(resolver, name) }.getOrNull() - } -} diff --git a/java/src/com/android/intentresolver/platform/SettingsImpl.kt b/java/src/com/android/intentresolver/platform/SettingsImpl.kt new file mode 100644 index 00000000..c7ff3521 --- /dev/null +++ b/java/src/com/android/intentresolver/platform/SettingsImpl.kt @@ -0,0 +1,59 @@ +/* + * 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.intentresolver.platform + +import android.content.ContentResolver +import android.provider.Settings +import javax.inject.Inject + +object SettingsImpl { + /** An implementation of GlobalSettings which forwards to [Settings.Global] */ + class Global @Inject constructor(private val contentResolver: ContentResolver) : + GlobalSettings { + override fun getStringOrNull(name: String): String? { + return Settings.Global.getString(contentResolver, name) + } + + override fun putString(name: String, value: String): Boolean { + return Settings.Global.putString(contentResolver, name, value) + } + } + + /** An implementation of SecureSettings which forwards to [Settings.Secure] */ + class Secure @Inject constructor(private val contentResolver: ContentResolver) : + SecureSettings { + override fun getStringOrNull(name: String): String? { + return Settings.Secure.getString(contentResolver, name) + } + + override fun putString(name: String, value: String): Boolean { + return Settings.Secure.putString(contentResolver, name, value) + } + } + + /** An implementation of SystemSettings which forwards to [Settings.System] */ + class System @Inject constructor(private val contentResolver: ContentResolver) : + SystemSettings { + override fun getStringOrNull(name: String): String? { + return Settings.System.getString(contentResolver, name) + } + + override fun putString(name: String, value: String): Boolean { + return Settings.System.putString(contentResolver, name, value) + } + } +} diff --git a/java/src/com/android/intentresolver/platform/SecureSettingsModule.kt b/java/src/com/android/intentresolver/platform/SettingsModule.kt index fa3ee4fe..3d5c50da 100644 --- a/java/src/com/android/intentresolver/platform/SecureSettingsModule.kt +++ b/java/src/com/android/intentresolver/platform/SettingsModule.kt @@ -24,7 +24,10 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) -interface SecureSettingsModule { +interface SettingsModule { + @Binds @Reusable fun globalSettings(settings: SettingsImpl.Global): GlobalSettings - @Binds @Reusable fun secureSettings(settings: PlatformSecureSettings): SecureSettings + @Binds @Reusable fun secureSettings(settings: SettingsImpl.Secure): SecureSettings + + @Binds @Reusable fun systemSettings(settings: SettingsImpl.System): SystemSettings } diff --git a/java/src/com/android/intentresolver/platform/SettingsProxy.kt b/java/src/com/android/intentresolver/platform/SettingsProxy.kt new file mode 100644 index 00000000..d97a0414 --- /dev/null +++ b/java/src/com/android/intentresolver/platform/SettingsProxy.kt @@ -0,0 +1,92 @@ +/* + * 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.intentresolver.platform + +/** A proxy to Settings.Global */ +interface GlobalSettings : SettingsProxy + +/** A proxy to Settings.Secure */ +interface SecureSettings : SettingsProxy + +/** A proxy to Settings.System */ +interface SystemSettings : SettingsProxy + +/** A generic Settings proxy interface */ +sealed interface SettingsProxy { + + /** Returns the String value set for the given settings key, or null if no value exists. */ + fun getStringOrNull(name: String): String? + + /** + * Writes a new string value for the given settings key. + * + * @return true if the value did not previously exist or was modified + */ + fun putString(name: String, value: String): Boolean + + /** + * Returns the Int value for the given settings key or null if no value exists or it cannot be + * interpreted as an Int. + */ + fun getIntOrNull(name: String): Int? = getStringOrNull(name)?.toIntOrNull() + + /** + * Writes a new int value for the given settings key. + * + * @return true if the value did not previously exist or was modified + */ + fun putInt(name: String, value: Int): Boolean = putString(name, value.toString()) + + /** + * Returns the Boolean value for the given settings key or null if no value exists or it cannot + * be interpreted as a Boolean. + */ + fun getBooleanOrNull(name: String): Boolean? = getIntOrNull(name)?.let { it != 0 } + + /** + * Writes a new Boolean value for the given settings key. + * + * @return true if the value did not previously exist or was modified + */ + fun putBoolean(name: String, value: Boolean): Boolean = putInt(name, if (value) 1 else 0) + + /** + * Returns the Long value for the given settings key or null if no value exists or it cannot be + * interpreted as a Long. + */ + fun getLongOrNull(name: String): Long? = getStringOrNull(name)?.toLongOrNull() + + /** + * Writes a new Long value for the given settings key. + * + * @return true if the value did not previously exist or was modified + */ + fun putLong(name: String, value: Long): Boolean = putString(name, value.toString()) + + /** + * Returns the Float value for the given settings key or null if no value exists or it cannot be + * interpreted as a Float. + */ + fun getFloatOrNull(name: String): Float? = getStringOrNull(name)?.toFloatOrNull() + + /** + * Writes a new float value for the given settings key. + * + * @return true if the value did not previously exist or was modified + */ + fun putFloat(name: String, value: Float): Boolean = putString(name, value.toString()) +} diff --git a/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java b/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java index 66f7650d..24b7fb12 100644 --- a/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java +++ b/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java @@ -91,6 +91,7 @@ import android.os.UserHandle; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.DeviceConfig; +import android.provider.Settings; import android.service.chooser.ChooserAction; import android.service.chooser.ChooserTarget; import android.text.Spannable; @@ -130,6 +131,7 @@ import com.android.intentresolver.logging.EventLog; import com.android.intentresolver.logging.FakeEventLog; import com.android.intentresolver.platform.AppPredictionAvailable; import com.android.intentresolver.platform.AppPredictionModule; +import com.android.intentresolver.platform.GlobalSettings; import com.android.intentresolver.platform.ImageEditor; import com.android.intentresolver.platform.ImageEditorModule; import com.android.intentresolver.shared.model.User; @@ -233,6 +235,9 @@ public class ChooserActivityTest { @ApplicationContext Context mContext; + @Inject + GlobalSettings mGlobalSettings; + /** An arbitrary pre-installed activity that handles this type of intent. */ @BindValue @ImageEditor @@ -2754,6 +2759,16 @@ public class ChooserActivityTest { assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE)); } + @Test + public void chooserDisabledWhileDeviceFrpLocked() { + mGlobalSettings.putBoolean(Settings.Global.SECURE_FRP_MODE, true); + Intent viewIntent = createSendTextIntent(); + ChooserWrapperActivity activity = mActivityRule.launchActivity( + Intent.createChooser(viewIntent, "chooser test")); + waitForIdle(); + assertTrue(activity.isFinishing()); + } + private Intent createChooserIntent(Intent intent, Intent[] initialIntents) { Intent chooserIntent = new Intent(); chooserIntent.setAction(Intent.ACTION_CHOOSER); diff --git a/java/src/com/android/intentresolver/platform/SecureSettings.kt b/tests/activity/src/com/android/intentresolver/platform/FakeSettingsModule.kt index 8a1dc531..9295f054 100644 --- a/java/src/com/android/intentresolver/platform/SecureSettings.kt +++ b/tests/activity/src/com/android/intentresolver/platform/FakeSettingsModule.kt @@ -16,26 +16,18 @@ package com.android.intentresolver.platform -import android.provider.Settings.SettingNotFoundException +import dagger.Module +import dagger.Provides +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import javax.inject.Singleton -/** - * A component which provides access to values from [android.provider.Settings.Secure]. - * - * All methods return nullable types instead of throwing [SettingNotFoundException] which yields - * cleaner, more idiomatic Kotlin code: - * - * // apply a default: val foo = settings.getInt(FOO) ?: DEFAULT_FOO - * - * // assert if missing: val required = settings.getInt(REQUIRED_VALUE) ?: error("required value - * missing") - */ -interface SecureSettings { - - fun getString(name: String): String? - - fun getInt(name: String): Int? +@Module +@TestInstallIn(components = [SingletonComponent::class], replaces = [SettingsModule::class]) +object FakeSettingsModule { + @Provides @Singleton fun secureSettings(): SecureSettings = FakeSettings() - fun getLong(name: String): Long? + @Provides @Singleton fun systemSettings(): SystemSettings = FakeSettings() - fun getFloat(name: String): Float? + @Provides @Singleton fun globalSettings(): GlobalSettings = FakeSettings() } diff --git a/tests/shared/src/com/android/intentresolver/platform/FakeSecureSettings.kt b/tests/shared/src/com/android/intentresolver/platform/FakeSecureSettings.kt deleted file mode 100644 index 862be76f..00000000 --- a/tests/shared/src/com/android/intentresolver/platform/FakeSecureSettings.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.intentresolver.platform - -/** - * Creates a SecureSettings instance with predefined values: - * - * val settings = fakeSecureSettings { - * putString("stringValue", "example") - * putInt("intValue", 42) - * } - */ -fun fakeSecureSettings(block: FakeSecureSettings.Builder.() -> Unit): SecureSettings { - return FakeSecureSettings.Builder().apply(block).build() -} - -/** An in memory implementation of [SecureSettings]. */ -class FakeSecureSettings private constructor(private val map: Map<String, String>) : - SecureSettings { - - override fun getString(name: String): String? = map[name] - override fun getInt(name: String): Int? = getString(name)?.toIntOrNull() - override fun getLong(name: String): Long? = getString(name)?.toLongOrNull() - override fun getFloat(name: String): Float? = getString(name)?.toFloatOrNull() - - class Builder { - private val map = mutableMapOf<String, String>() - - fun putString(name: String, value: String) { - map[name] = value - } - fun putInt(name: String, value: Int) { - map[name] = value.toString() - } - fun putLong(name: String, value: Long) { - map[name] = value.toString() - } - fun putFloat(name: String, value: Float) { - map[name] = value.toString() - } - - fun build(): SecureSettings { - return FakeSecureSettings(map.toMap()) - } - } -} diff --git a/tests/shared/src/com/android/intentresolver/platform/FakeSettings.kt b/tests/shared/src/com/android/intentresolver/platform/FakeSettings.kt new file mode 100644 index 00000000..55cd7127 --- /dev/null +++ b/tests/shared/src/com/android/intentresolver/platform/FakeSettings.kt @@ -0,0 +1,43 @@ +/* + * 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.intentresolver.platform + +/** + * Creates a Settings instance with predefined values: + * + * val settings: SecureSettings = fakeSettings { + * putString("stringValue", "example") + * putInt("intValue", 42) + * } + */ +inline fun <reified T : SettingsProxy> fakeSettings(block: SettingsProxy.() -> Unit): T { + return FakeSettings(mutableMapOf()).apply(block) as T +} + +/** A memory-only implementation of [SettingsProxy]. */ +class FakeSettings( + private val map: MutableMap<String, String>, +) : GlobalSettings, SecureSettings, SystemSettings { + constructor() : this(mutableMapOf()) + + override fun getStringOrNull(name: String): String? = map[name] + + override fun putString(name: String, value: String): Boolean { + map[name] = value + return true + } +} diff --git a/tests/unit/src/com/android/intentresolver/platform/FakeSecureSettingsTest.kt b/tests/unit/src/com/android/intentresolver/platform/FakeSettingsTest.kt index fd74b50a..82daca55 100644 --- a/tests/unit/src/com/android/intentresolver/platform/FakeSecureSettingsTest.kt +++ b/tests/unit/src/com/android/intentresolver/platform/FakeSettingsTest.kt @@ -17,48 +17,52 @@ package com.android.intentresolver.platform import com.google.common.truth.Truth.assertThat +import org.junit.Test -class FakeSecureSettingsTest { +class FakeSettingsTest { - private val secureSettings = fakeSecureSettings { + private val settings: FakeSettings = fakeSettings { putInt(intKey, intVal) putString(stringKey, stringVal) putFloat(floatKey, floatVal) putLong(longKey, longVal) } + @Test fun testExpectedValues_returned() { - assertThat(secureSettings.getInt(intKey)).isEqualTo(intVal) - assertThat(secureSettings.getString(stringKey)).isEqualTo(stringVal) - assertThat(secureSettings.getFloat(floatKey)).isEqualTo(floatVal) - assertThat(secureSettings.getLong(longKey)).isEqualTo(longVal) + assertThat(settings.getIntOrNull(intKey)).isEqualTo(intVal) + assertThat(settings.getStringOrNull(stringKey)).isEqualTo(stringVal) + assertThat(settings.getFloatOrNull(floatKey)).isEqualTo(floatVal) + assertThat(settings.getLongOrNull(longKey)).isEqualTo(longVal) } + @Test fun testUndefinedValues_returnNull() { - assertThat(secureSettings.getInt("unknown")).isNull() - assertThat(secureSettings.getString("unknown")).isNull() - assertThat(secureSettings.getFloat("unknown")).isNull() - assertThat(secureSettings.getLong("unknown")).isNull() + assertThat(settings.getIntOrNull("unknown")).isNull() + assertThat(settings.getStringOrNull("unknown")).isNull() + assertThat(settings.getFloatOrNull("unknown")).isNull() + assertThat(settings.getLongOrNull("unknown")).isNull() } /** * FakeSecureSettings models the real secure settings by storing values in String form. The * value is returned if/when it can be parsed from the string value, otherwise null. */ + @Test fun testMismatchedTypes() { - assertThat(secureSettings.getString(intKey)).isEqualTo(intVal.toString()) - assertThat(secureSettings.getString(floatKey)).isEqualTo(floatVal.toString()) - assertThat(secureSettings.getString(longKey)).isEqualTo(longVal.toString()) + assertThat(settings.getStringOrNull(intKey)).isEqualTo(intVal.toString()) + assertThat(settings.getStringOrNull(floatKey)).isEqualTo(floatVal.toString()) + assertThat(settings.getStringOrNull(longKey)).isEqualTo(longVal.toString()) - assertThat(secureSettings.getInt(stringKey)).isNull() - assertThat(secureSettings.getLong(stringKey)).isNull() - assertThat(secureSettings.getFloat(stringKey)).isNull() + assertThat(settings.getIntOrNull(stringKey)).isNull() + assertThat(settings.getLongOrNull(stringKey)).isNull() + assertThat(settings.getFloatOrNull(stringKey)).isNull() - assertThat(secureSettings.getInt(longKey)).isNull() - assertThat(secureSettings.getFloat(longKey)).isNull() // TODO: verify Long.MAX > Float.MAX ? + assertThat(settings.getIntOrNull(longKey)).isNull() + assertThat(settings.getFloatOrNull(longKey)).isWithin(0.00001f).of(Long.MAX_VALUE.toFloat()) - assertThat(secureSettings.getLong(floatKey)).isNull() // TODO: or is Float.MAX > Long.MAX? - assertThat(secureSettings.getInt(floatKey)).isNull() + assertThat(settings.getLongOrNull(floatKey)).isNull() + assertThat(settings.getIntOrNull(floatKey)).isNull() } companion object Data { diff --git a/tests/unit/src/com/android/intentresolver/platform/NearbyShareModuleTest.kt b/tests/unit/src/com/android/intentresolver/platform/NearbyShareModuleTest.kt index 71ef2919..6e5c97c2 100644 --- a/tests/unit/src/com/android/intentresolver/platform/NearbyShareModuleTest.kt +++ b/tests/unit/src/com/android/intentresolver/platform/NearbyShareModuleTest.kt @@ -49,7 +49,7 @@ class NearbyShareModuleTest { @Test fun valueIsAbsent_whenUnset() { - val secureSettings = fakeSecureSettings {} + val secureSettings: SecureSettings = fakeSettings {} val resources = context.fakeResources { addOverride(R.string.config_defaultNearbySharingComponent, "") } @@ -59,7 +59,7 @@ class NearbyShareModuleTest { @Test fun defaultValue_readFromResources() { - val secureSettings = fakeSecureSettings {} + val secureSettings: SecureSettings = fakeSettings {} val resources = context.fakeResources { addOverride( @@ -76,7 +76,7 @@ class NearbyShareModuleTest { @Test fun secureSettings_overridesDefault() { - val secureSettings = fakeSecureSettings { + val secureSettings: SecureSettings = fakeSettings { putString(Settings.Secure.NEARBY_SHARING_COMPONENT, "com.example/.BComponent") } val resources = |