diff --git a/Android.bp b/Android.bp index a16078551cf..907ff12bc86 100644 --- a/Android.bp +++ b/Android.bp @@ -132,6 +132,7 @@ android_library { ], flags_packages: [ "aconfig_settings_flags", + "aconfig_settingslib_flags", "android.app.flags-aconfig", "android.provider.flags-aconfig", "android.security.flags-aconfig", diff --git a/AndroidManifest.xml b/AndroidManifest.xml index db2e0d49697..2295ee3dd1a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -5412,6 +5412,17 @@ + + + + + + + diff --git a/src/com/android/settings/service/PreferenceService.kt b/src/com/android/settings/service/PreferenceService.kt new file mode 100644 index 00000000000..3a677629afd --- /dev/null +++ b/src/com/android/settings/service/PreferenceService.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.settings.service + +import android.os.Binder +import android.os.OutcomeReceiver +import android.os.Process +import android.service.settings.preferences.GetValueRequest +import android.service.settings.preferences.GetValueResult +import android.service.settings.preferences.MetadataRequest +import android.service.settings.preferences.MetadataResult +import android.service.settings.preferences.SetValueRequest +import android.service.settings.preferences.SetValueResult +import android.service.settings.preferences.SettingsPreferenceService +import com.android.settingslib.graph.PreferenceGetterApiHandler +import com.android.settingslib.graph.PreferenceSetterApiHandler +import com.android.settingslib.ipc.ApiPermissionChecker +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import java.lang.Exception + +class PreferenceService : SettingsPreferenceService() { + + private val scope = CoroutineScope(Job() + Dispatchers.Main) + + private val getApiHandler = PreferenceGetterApiHandler(1, ApiPermissionChecker.alwaysAllow()) + private val setApiHandler = PreferenceSetterApiHandler(2, ApiPermissionChecker.alwaysAllow()) + + override fun onGetAllPreferenceMetadata( + request: MetadataRequest, + callback: OutcomeReceiver + ) { + // TODO(379750656): Update graph API to be usable outside SettingsLib + callback.onError(UnsupportedOperationException("Not yet supported")) + } + + override fun onGetPreferenceValue( + request: GetValueRequest, + callback: OutcomeReceiver + ) { + scope.launch(Dispatchers.IO) { + val apiRequest = transformFrameworkGetValueRequest(request) + val response = getApiHandler.invoke(application, Process.myUid(), + Binder.getCallingPid(), apiRequest) + val result = transformCatalystGetValueResponse( + this@PreferenceService, + request, + response + ) + if (result == null) { + callback.onError(IllegalStateException("No response")) + } else { + callback.onResult(result) + } + } + } + + override fun onSetPreferenceValue( + request: SetValueRequest, + callback: OutcomeReceiver + ) { + scope.launch(Dispatchers.IO) { + val apiRequest = transformFrameworkSetValueRequest(request) + if (apiRequest == null) { + callback.onResult( + SetValueResult.Builder(SetValueResult.RESULT_INVALID_REQUEST).build() + ) + } else { + val response = setApiHandler.invoke(application, Process.myUid(), + Binder.getCallingPid(), apiRequest) + + callback.onResult(transformCatalystSetValueResponse(response)) + } + } + } +} diff --git a/src/com/android/settings/service/PreferenceServiceRequestTransformer.kt b/src/com/android/settings/service/PreferenceServiceRequestTransformer.kt new file mode 100644 index 00000000000..7a4c7fc9dff --- /dev/null +++ b/src/com/android/settings/service/PreferenceServiceRequestTransformer.kt @@ -0,0 +1,156 @@ +/* + * 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.settings.service + +import android.content.Context +import android.service.settings.preferences.GetValueRequest +import android.service.settings.preferences.GetValueResult +import android.service.settings.preferences.SetValueRequest +import android.service.settings.preferences.SetValueResult +import android.service.settings.preferences.SettingsPreferenceMetadata +import android.service.settings.preferences.SettingsPreferenceValue +import com.android.settingslib.graph.PreferenceCoordinate +import com.android.settingslib.graph.PreferenceGetterErrorCode +import com.android.settingslib.graph.PreferenceGetterFlags +import com.android.settingslib.graph.PreferenceGetterRequest +import com.android.settingslib.graph.PreferenceGetterResponse +import com.android.settingslib.graph.PreferenceSetterRequest +import com.android.settingslib.graph.PreferenceSetterResult +import com.android.settingslib.graph.preferenceValueProto +import com.android.settingslib.graph.proto.PreferenceProto +import com.android.settingslib.graph.proto.PreferenceValueProto +import com.android.settingslib.graph.getText +import com.android.settingslib.graph.toIntent +import com.android.settingslib.metadata.SensitivityLevel + +/** Translate Framework GET VALUE request to Catalyst GET VALUE request */ +fun transformFrameworkGetValueRequest( + request: GetValueRequest, + flags: Int = PreferenceGetterFlags.ALL +): PreferenceGetterRequest { + val coord = PreferenceCoordinate(request.screenKey, request.preferenceKey) + return PreferenceGetterRequest( + arrayOf(coord), + flags + ) +} + +/** Translate Catalyst GET VALUE result to Framework GET VALUE result */ +fun transformCatalystGetValueResponse( + context: Context, + request: GetValueRequest, + response: PreferenceGetterResponse +): GetValueResult? { + val coord = PreferenceCoordinate(request.screenKey, request.preferenceKey) + val errorResponse = response.errors[coord] + val valueResponse = response.preferences[coord] + when { + errorResponse != null -> { + val errorCode = when (errorResponse) { + PreferenceGetterErrorCode.NOT_FOUND -> GetValueResult.RESULT_UNSUPPORTED + PreferenceGetterErrorCode.DISALLOW -> GetValueResult.RESULT_DISALLOW + else -> GetValueResult.RESULT_INTERNAL_ERROR + } + return GetValueResult.Builder(errorCode).build() + } + valueResponse != null -> { + val resultBuilder = GetValueResult.Builder(GetValueResult.RESULT_OK) + resultBuilder.setMetadata(valueResponse.toMetadata(context, coord.screenKey)) + val prefValue = valueResponse.value + when (prefValue.valueCase.number) { + PreferenceValueProto.BOOLEAN_VALUE_FIELD_NUMBER -> { + resultBuilder.setValue( + SettingsPreferenceValue.Builder( + SettingsPreferenceValue.TYPE_BOOLEAN + ).setBooleanValue(prefValue.booleanValue) + .build() + ) + return resultBuilder.build() + } + PreferenceValueProto.INT_VALUE_FIELD_NUMBER -> { + resultBuilder.setValue( + SettingsPreferenceValue.Builder( + SettingsPreferenceValue.TYPE_INT + ).setIntValue(prefValue.intValue) + .build() + ) + return resultBuilder.build() + } + } + return GetValueResult.Builder( + GetValueResult.RESULT_UNSUPPORTED + ).build() + } + else -> return null + } +} + +/** Translate Framework SET VALUE request to Catalyst SET VALUE request */ +fun transformFrameworkSetValueRequest(request: SetValueRequest): PreferenceSetterRequest? { + val valueProto = when (request.preferenceValue.type) { + SettingsPreferenceValue.TYPE_BOOLEAN -> preferenceValueProto { + booleanValue = request.preferenceValue.booleanValue + } + SettingsPreferenceValue.TYPE_INT -> preferenceValueProto { + intValue = request.preferenceValue.intValue + } + else -> null + } + return valueProto?.let { + PreferenceSetterRequest(request.screenKey, request.preferenceKey, it) + } +} + +/** Translate Catalyst SET VALUE result to Framework SET VALUE result */ +fun transformCatalystSetValueResponse(@PreferenceSetterResult response: Int): SetValueResult { + val resultCode = when (response) { + PreferenceSetterResult.OK -> SetValueResult.RESULT_OK + PreferenceSetterResult.UNAVAILABLE -> SetValueResult.RESULT_UNAVAILABLE + PreferenceSetterResult.DISABLED -> SetValueResult.RESULT_DISABLED + PreferenceSetterResult.UNSUPPORTED -> SetValueResult.RESULT_UNSUPPORTED + PreferenceSetterResult.DISALLOW -> SetValueResult.RESULT_DISALLOW + PreferenceSetterResult.REQUIRE_APP_PERMISSION -> + SetValueResult.RESULT_REQUIRE_APP_PERMISSION + PreferenceSetterResult.REQUIRE_USER_AGREEMENT -> SetValueResult.RESULT_REQUIRE_USER_CONSENT + PreferenceSetterResult.RESTRICTED -> SetValueResult.RESULT_RESTRICTED + PreferenceSetterResult.INVALID_REQUEST -> SetValueResult.RESULT_INVALID_REQUEST + else -> SetValueResult.RESULT_INTERNAL_ERROR + } + return SetValueResult.Builder(resultCode).build() +} + +private fun PreferenceProto.toMetadata( + context: Context, + screenKey: String +): SettingsPreferenceMetadata { + val sensitivity = when (sensitivityLevel) { + SensitivityLevel.NO_SENSITIVITY -> SettingsPreferenceMetadata.NO_SENSITIVITY + SensitivityLevel.LOW_SENSITIVITY -> SettingsPreferenceMetadata.EXPECT_POST_CONFIRMATION + SensitivityLevel.MEDIUM_SENSITIVITY -> SettingsPreferenceMetadata.EXPECT_PRE_CONFIRMATION + else -> SettingsPreferenceMetadata.NO_DIRECT_ACCESS + } + return SettingsPreferenceMetadata.Builder(screenKey, key) + .setTitle(title.getText(context)) + .setSummary(summary.getText(context)) + .setEnabled(enabled) + .setAvailable(available) + .setRestricted(restricted) + .setWritable(persistent) + .setLaunchIntent(launchIntent.toIntent()) + .setWriteSensitivity(sensitivity) + .build() +} diff --git a/tests/robotests/src/com/android/settings/service/PreferenceServiceRequestTransformerTest.kt b/tests/robotests/src/com/android/settings/service/PreferenceServiceRequestTransformerTest.kt new file mode 100644 index 00000000000..f064b221282 --- /dev/null +++ b/tests/robotests/src/com/android/settings/service/PreferenceServiceRequestTransformerTest.kt @@ -0,0 +1,235 @@ +/* + * 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.settings.service + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.service.settings.preferences.GetValueRequest +import android.service.settings.preferences.GetValueResult +import android.service.settings.preferences.SetValueRequest +import android.service.settings.preferences.SetValueResult +import android.service.settings.preferences.SettingsPreferenceMetadata +import android.service.settings.preferences.SettingsPreferenceValue +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.homepage.SettingsHomepageActivity +import com.android.settingslib.flags.Flags.FLAG_SETTINGS_CATALYST +import com.android.settingslib.graph.PreferenceCoordinate +import com.android.settingslib.graph.PreferenceGetterErrorCode +import com.android.settingslib.graph.PreferenceGetterFlags +import com.android.settingslib.graph.PreferenceGetterResponse +import com.android.settingslib.graph.PreferenceSetterResult +import com.android.settingslib.graph.proto.PreferenceProto +import com.android.settingslib.graph.proto.PreferenceValueProto +import com.android.settingslib.graph.proto.TextProto +import com.android.settingslib.graph.toProto +import com.android.settingslib.metadata.SensitivityLevel +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@RequiresFlagsEnabled(FLAG_SETTINGS_CATALYST) +class PreferenceServiceRequestTransformerTest { + + @get:Rule + val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @Test + fun transformFrameworkGetValueRequest_returnsValidCatalystRequest() { + val fRequest = GetValueRequest.Builder("screen", "pref").build() + val cRequest = transformFrameworkGetValueRequest(fRequest) + with(cRequest) { + assertThat(preferences).hasLength(1) + assertThat(preferences.first().screenKey).isEqualTo(fRequest.screenKey) + assertThat(preferences.first().key).isEqualTo(fRequest.preferenceKey) + assertThat(flags).isEqualTo(PreferenceGetterFlags.ALL) + } + } + + @Test + fun transformCatalystGetValueResponse_success_returnsValidFrameworkResponse() { + val context: Context = ApplicationProvider.getApplicationContext() + val fRequest = GetValueRequest.Builder("screen", "key").build() + val cResult = PreferenceGetterResponse( + emptyMap(), + mapOf( + PreferenceCoordinate(fRequest.screenKey, fRequest.preferenceKey) to + PreferenceProto.newBuilder() + .setKey("key") + .setTitle(TextProto.newBuilder().setString("title")) + .setSummary(TextProto.newBuilder().setString("summary")) + .setEnabled(true) + .setAvailable(true) + .setRestricted(true) + .setPersistent(true) + .setSensitivityLevel(SensitivityLevel.LOW_SENSITIVITY) + .setLaunchIntent( + Intent(context, SettingsHomepageActivity::class.java).toProto() + ) + .setValue(PreferenceValueProto.newBuilder().setBooleanValue(true)) + .build() + ) + ) + val fResult = transformCatalystGetValueResponse(context, fRequest, cResult) + assertThat(fResult!!.resultCode).isEqualTo(GetValueResult.RESULT_OK) + with(fResult.metadata!!) { + assertThat(title).isEqualTo("title") + assertThat(summary).isEqualTo("summary") + assertThat(isEnabled).isTrue() + assertThat(isAvailable).isTrue() + assertThat(isRestricted).isTrue() + assertThat(isWritable).isTrue() + assertThat(writeSensitivity) + .isEqualTo(SettingsPreferenceMetadata.EXPECT_POST_CONFIRMATION) + assertThat(launchIntent).isNotNull() + assertThat(launchIntent!!.component!!.className) + .isEqualTo(SettingsHomepageActivity::class.java.name) + } + with(fResult.value!!) { + assertThat(type).isEqualTo(SettingsPreferenceValue.TYPE_BOOLEAN) + assertThat(booleanValue).isTrue() + } + } + + @Test + fun transformCatalystGetValueResponse_failure_returnsValidFrameworkResponse() { + val context: Context = ApplicationProvider.getApplicationContext() + val fRequest = GetValueRequest.Builder("screen", "key").build() + val cResult = PreferenceGetterResponse( + mapOf( + PreferenceCoordinate(fRequest.screenKey, fRequest.preferenceKey) to + PreferenceGetterErrorCode.NOT_FOUND + ), + emptyMap() + ) + val fResult = transformCatalystGetValueResponse(context, fRequest, cResult) + with(fResult!!) { + assertThat(resultCode).isEqualTo(GetValueResult.RESULT_UNSUPPORTED) + assertThat(metadata).isNull() + assertThat(value).isNull() + } + } + + @Test + fun transformCatalystGetValueResponse_invalidResponse_returnsNull() { + val context: Context = ApplicationProvider.getApplicationContext() + val fRequest = GetValueRequest.Builder("screen", "key").build() + val cResult = PreferenceGetterResponse(emptyMap(), emptyMap()) + val fResult = transformCatalystGetValueResponse(context, fRequest, cResult) + assertThat(fResult).isNull() + } + + @Test + fun transformFrameworkSetValueRequest_typeBoolean_returnsValidCatalystRequest() { + val fRequest = SetValueRequest.Builder( + "screen", + "pref", + SettingsPreferenceValue.Builder(SettingsPreferenceValue.TYPE_BOOLEAN) + .setBooleanValue(true) + .build() + ).build() + val cRequest = transformFrameworkSetValueRequest(fRequest) + with(cRequest!!) { + assertThat(screenKey).isEqualTo(fRequest.screenKey) + assertThat(key).isEqualTo(fRequest.preferenceKey) + assertThat(value.hasBooleanValue()).isTrue() + assertThat(value.booleanValue).isTrue() + } + } + + @Test + fun transformFrameworkSetValueRequest_typeInt_returnsValidCatalystRequest() { + val fRequest = SetValueRequest.Builder( + "screen", + "pref", + SettingsPreferenceValue.Builder(SettingsPreferenceValue.TYPE_INT) + .setIntValue(5) + .build() + ).build() + val cRequest = transformFrameworkSetValueRequest(fRequest) + with(cRequest!!) { + assertThat(screenKey).isEqualTo(fRequest.screenKey) + assertThat(key).isEqualTo(fRequest.preferenceKey) + assertThat(value.hasIntValue()).isTrue() + assertThat(value.intValue).isEqualTo(5) + } + } + + @Test + fun transformFrameworkSetValueRequest_typeString_returnsNull() { + val fRequest = SetValueRequest.Builder( + "screen", + "pref", + SettingsPreferenceValue.Builder(SettingsPreferenceValue.TYPE_STRING) + .setStringValue("value") + .build() + ).build() + val cRequest = transformFrameworkSetValueRequest(fRequest) + assertThat(cRequest).isNull() + } + + @Test + fun transformCatalystSetValueResponse_returnsValidFrameworkResponse() { + assertThat( + transformCatalystSetValueResponse(PreferenceSetterResult.OK).resultCode + ).isEqualTo(SetValueResult.RESULT_OK) + + assertThat( + transformCatalystSetValueResponse(PreferenceSetterResult.UNAVAILABLE).resultCode + ).isEqualTo(SetValueResult.RESULT_UNAVAILABLE) + + assertThat( + transformCatalystSetValueResponse(PreferenceSetterResult.DISABLED).resultCode + ).isEqualTo(SetValueResult.RESULT_DISABLED) + + assertThat( + transformCatalystSetValueResponse(PreferenceSetterResult.UNSUPPORTED).resultCode + ).isEqualTo(SetValueResult.RESULT_UNSUPPORTED) + + assertThat( + transformCatalystSetValueResponse(PreferenceSetterResult.DISALLOW).resultCode + ).isEqualTo(SetValueResult.RESULT_DISALLOW) + + assertThat( + transformCatalystSetValueResponse(PreferenceSetterResult.REQUIRE_APP_PERMISSION) + .resultCode + ).isEqualTo(SetValueResult.RESULT_REQUIRE_APP_PERMISSION) + + assertThat( + transformCatalystSetValueResponse(PreferenceSetterResult.REQUIRE_USER_AGREEMENT) + .resultCode + ).isEqualTo(SetValueResult.RESULT_REQUIRE_USER_CONSENT) + + assertThat( + transformCatalystSetValueResponse(PreferenceSetterResult.RESTRICTED).resultCode + ).isEqualTo(SetValueResult.RESULT_RESTRICTED) + + assertThat( + transformCatalystSetValueResponse(PreferenceSetterResult.INVALID_REQUEST).resultCode + ).isEqualTo(SetValueResult.RESULT_INVALID_REQUEST) + + assertThat( + transformCatalystSetValueResponse(PreferenceSetterResult.INTERNAL_ERROR).resultCode + ).isEqualTo(SetValueResult.RESULT_INTERNAL_ERROR) + } +} \ No newline at end of file