From 3362e5d14ade139482ab41034c0c04c43d7f37e6 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 4 Jun 2024 19:41:55 +0800 Subject: [PATCH] Refactor PrintSettings (1/n) Add PrintRepository for display Print services. Bug: 320076351 Flag: com.android.settings.flags.refactor_print_settings Test: manual Test: unit test Change-Id: I1ea52508d504161675eeffeb3ec077caa641cb2c --- ...tings_experience_flag_declarations.aconfig | 10 ++ .../android/settings/print/PrintRepository.kt | 76 ++++++++++++ .../settings/print/PrintSettingsFragment.java | 12 ++ .../print/PrintSettingsPageProvider.kt | 102 ++++++++++++++++ .../settings/spa/SettingsSpaEnvironment.kt | 2 + .../settings/print/PrintRepositoryTest.kt | 110 ++++++++++++++++++ .../print/PrintSettingsPageProviderTest.kt | 104 +++++++++++++++++ 7 files changed, 416 insertions(+) create mode 100644 src/com/android/settings/print/PrintRepository.kt create mode 100644 src/com/android/settings/print/PrintSettingsPageProvider.kt create mode 100644 tests/spa_unit/src/com/android/settings/print/PrintRepositoryTest.kt create mode 100644 tests/spa_unit/src/com/android/settings/print/PrintSettingsPageProviderTest.kt diff --git a/aconfig/settings_experience_flag_declarations.aconfig b/aconfig/settings_experience_flag_declarations.aconfig index 9fe3f327383..d5caccfbb5c 100644 --- a/aconfig/settings_experience_flag_declarations.aconfig +++ b/aconfig/settings_experience_flag_declarations.aconfig @@ -7,3 +7,13 @@ flag { description: "Change to the new APN page." bug: "298906796" } + +flag { + name: "refactor_print_settings" + namespace: "settings_experience" + description: "Refactor the PrintSettings page." + bug: "320076351" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/src/com/android/settings/print/PrintRepository.kt b/src/com/android/settings/print/PrintRepository.kt new file mode 100644 index 00000000000..8a9182a952d --- /dev/null +++ b/src/com/android/settings/print/PrintRepository.kt @@ -0,0 +1,76 @@ +/* + * 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.print + +import android.content.Context +import android.graphics.drawable.Drawable +import android.print.PrintManager +import android.printservice.PrintServiceInfo +import com.android.settings.R +import com.android.settingslib.spa.framework.util.mapItem +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +class PrintRepository(private val context: Context) { + + private val printManager = context.getSystemService(PrintManager::class.java)!! + private val packageManager = context.packageManager + + data class PrintServiceDisplayInfo( + val title: String, + val isEnabled: Boolean, + val summary: String, + val icon: Drawable, + val componentName: String, + ) + + fun printServiceDisplayInfosFlow(): Flow> = + printServicesFlow() + .mapItem { printService -> printService.toPrintServiceDisplayInfo() } + .conflate() + .flowOn(Dispatchers.Default) + + private fun PrintServiceInfo.toPrintServiceDisplayInfo() = PrintServiceDisplayInfo( + title = resolveInfo.loadLabel(packageManager).toString(), + isEnabled = isEnabled, + summary = context.getString( + if (isEnabled) R.string.print_feature_state_on else R.string.print_feature_state_off + ), + icon = resolveInfo.loadIcon(packageManager), + componentName = componentName.flattenToString(), + ) + + private fun printServicesFlow(): Flow> = + printManager.printServicesChangeFlow() + .map { printManager.getPrintServices(PrintManager.ALL_SERVICES) } + .conflate() + .flowOn(Dispatchers.Default) + + private companion object { + fun PrintManager.printServicesChangeFlow(): Flow = callbackFlow { + val listener = PrintManager.PrintServicesChangeListener { trySend(Unit) } + addPrintServicesChangeListener(listener, null) + trySend(Unit) + awaitClose { removePrintServicesChangeListener(listener) } + }.conflate().flowOn(Dispatchers.Default) + } +} diff --git a/src/com/android/settings/print/PrintSettingsFragment.java b/src/com/android/settings/print/PrintSettingsFragment.java index cd80998170c..ee94683b012 100644 --- a/src/com/android/settings/print/PrintSettingsFragment.java +++ b/src/com/android/settings/print/PrintSettingsFragment.java @@ -46,6 +46,7 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.loader.app.LoaderManager.LoaderCallbacks; import androidx.loader.content.AsyncTaskLoader; @@ -54,7 +55,9 @@ import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import com.android.settings.R; +import com.android.settings.flags.Flags; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.spa.SpaActivity; import com.android.settingslib.search.Indexable; import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.widget.AppPreference; @@ -101,6 +104,15 @@ public class PrintSettingsFragment extends ProfileSettingsPreferenceFragment super(UserManager.DISALLOW_PRINTING); } + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (Flags.refactorPrintSettings()) { + SpaActivity.startSpaActivity(context, PrintSettingsPageProvider.INSTANCE.getName()); + finish(); + } + } + @Override protected String getLogTag() { return TAG; diff --git a/src/com/android/settings/print/PrintSettingsPageProvider.kt b/src/com/android/settings/print/PrintSettingsPageProvider.kt new file mode 100644 index 00000000000..aac0a5d0cf4 --- /dev/null +++ b/src/com/android/settings/print/PrintSettingsPageProvider.kt @@ -0,0 +1,102 @@ +/* + * 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.print + +import android.app.settings.SettingsEnums +import android.os.Bundle +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.core.os.bundleOf +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.settings.R +import com.android.settings.core.SubSettingLauncher +import com.android.settings.print.PrintRepository.PrintServiceDisplayInfo +import com.android.settings.print.PrintSettingsFragment.EXTRA_CHECKED +import com.android.settings.print.PrintSettingsFragment.EXTRA_SERVICE_COMPONENT_NAME +import com.android.settings.print.PrintSettingsFragment.EXTRA_TITLE +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.rememberDrawablePainter +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category +import com.android.settingslib.spaprivileged.template.common.UserProfilePager + +object PrintSettingsPageProvider : SettingsPageProvider { + override val name = "PrintSettings" + + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(title = stringResource(R.string.print_settings)) { + val context = LocalContext.current + val printRepository = remember(context) { PrintRepository(context) } + UserProfilePager { + PrintServices(printRepository) + } + } + } + + @Composable + private fun PrintServices(printRepository: PrintRepository) { + val printServiceDisplayInfos by remember { + printRepository.printServiceDisplayInfosFlow() + }.collectAsStateWithLifecycle(initialValue = emptyList()) + Category(title = stringResource(R.string.print_settings_title)) { + for (printServiceDisplayInfo in printServiceDisplayInfos) { + PrintService(printServiceDisplayInfo) + } + } + } + + @VisibleForTesting + @Composable + fun PrintService(displayInfo: PrintServiceDisplayInfo) { + val context = LocalContext.current + Preference(model = object : PreferenceModel { + override val title = displayInfo.title + override val summary = { displayInfo.summary } + override val icon: @Composable () -> Unit = { + Image( + painter = rememberDrawablePainter(displayInfo.icon), + contentDescription = null, + modifier = Modifier.size(SettingsDimension.appIconItemSize), + ) + } + override val onClick = { + SubSettingLauncher(context).apply { + setDestination(PrintServiceSettingsFragment::class.qualifiedName) + setArguments( + bundleOf( + EXTRA_CHECKED to displayInfo.isEnabled, + EXTRA_TITLE to displayInfo.title, + EXTRA_SERVICE_COMPONENT_NAME to displayInfo.componentName + ) + ) + setSourceMetricsCategory(SettingsEnums.PRINT_SETTINGS) + }.launch() + } + }) + } +} diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt index 5dbc8dc7099..45d238a8771 100644 --- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt +++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt @@ -19,6 +19,7 @@ package com.android.settings.spa import android.content.Context import android.util.FeatureFlagUtils import com.android.settings.network.apn.ApnEditPageProvider +import com.android.settings.print.PrintSettingsPageProvider import com.android.settings.spa.about.AboutPhonePageProvider import com.android.settings.spa.app.AllAppListPageProvider import com.android.settings.spa.app.AppsMainPageProvider @@ -120,6 +121,7 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) { BatteryOptimizationModeAppListPageProvider, NetworkCellularGroupProvider(), WifiPrivacyPageProvider, + PrintSettingsPageProvider, ) override val logger = if (FeatureFlagUtils.isEnabled( diff --git a/tests/spa_unit/src/com/android/settings/print/PrintRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/print/PrintRepositoryTest.kt new file mode 100644 index 00000000000..79d86da1a3a --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/print/PrintRepositoryTest.kt @@ -0,0 +1,110 @@ +/* + * 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.print + +import android.content.Context +import android.content.pm.ResolveInfo +import android.content.pm.ServiceInfo +import android.graphics.drawable.Drawable +import android.print.PrintManager +import android.printservice.PrintServiceInfo +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy + +@RunWith(AndroidJUnit4::class) +class PrintRepositoryTest { + + private val printServiceInfo = PrintServiceInfo( + /* resolveInfo = */ ResolveInfo().apply { serviceInfo = MockServiceInfo }, + /* settingsActivityName = */ "", + /* addPrintersActivityName = */ "", + /* advancedPrintOptionsActivityName = */ "", + ) + + private val mockPrintManager = mock { + on { getPrintServices(PrintManager.ALL_SERVICES) } doReturn listOf(printServiceInfo) + } + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + on { getSystemService(PrintManager::class.java) } doReturn mockPrintManager + } + + private val repository = PrintRepository(context) + + @Test + fun printServiceDisplayInfosFlow_title() = runBlocking { + val displayInfo = repository.printServiceDisplayInfosFlow().firstWithTimeoutOrNull()!! + .single() + + assertThat(displayInfo.title).isEqualTo(LABEL) + } + + @Test + fun printServiceDisplayInfosFlow_isEnabled() = runBlocking { + printServiceInfo.setIsEnabled(true) + + val displayInfo = repository.printServiceDisplayInfosFlow().firstWithTimeoutOrNull()!! + .single() + + assertThat(displayInfo.isEnabled).isTrue() + assertThat(displayInfo.summary) + .isEqualTo(context.getString(R.string.print_feature_state_on)) + } + + @Test + fun printServiceDisplayInfosFlow_notEnabled() = runBlocking { + printServiceInfo.setIsEnabled(false) + + val displayInfo = repository.printServiceDisplayInfosFlow().firstWithTimeoutOrNull()!! + .single() + + assertThat(displayInfo.isEnabled).isFalse() + assertThat(displayInfo.summary) + .isEqualTo(context.getString(R.string.print_feature_state_off)) + } + + @Test + fun printServiceDisplayInfosFlow_componentName() = runBlocking { + val displayInfo = repository.printServiceDisplayInfosFlow().firstWithTimeoutOrNull()!! + .single() + + assertThat(displayInfo.componentName).isEqualTo("$PACKAGE_NAME/$SERVICE_NAME") + } + + private companion object { + const val PACKAGE_NAME = "package.name" + const val SERVICE_NAME = "ServiceName" + const val LABEL = "Label" + val MockServiceInfo = mock { + on { loadLabel(any()) } doReturn LABEL + on { loadIcon(any()) } doReturn mock() + }.apply { + packageName = PACKAGE_NAME + name = SERVICE_NAME + } + } +} diff --git a/tests/spa_unit/src/com/android/settings/print/PrintSettingsPageProviderTest.kt b/tests/spa_unit/src/com/android/settings/print/PrintSettingsPageProviderTest.kt new file mode 100644 index 00000000000..746816b52c8 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/print/PrintSettingsPageProviderTest.kt @@ -0,0 +1,104 @@ +/* + * 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.print + +import android.content.Context +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settings.SettingsActivity +import com.android.settings.print.PrintRepository.PrintServiceDisplayInfo +import com.android.settings.print.PrintSettingsFragment.EXTRA_CHECKED +import com.android.settings.print.PrintSettingsFragment.EXTRA_SERVICE_COMPONENT_NAME +import com.android.settings.print.PrintSettingsFragment.EXTRA_TITLE +import com.android.settings.print.PrintSettingsPageProvider.PrintService +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class PrintSettingsPageProviderTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + doNothing().whenever(mock).startActivity(any()) + } + + private val displayInfo = PrintServiceDisplayInfo( + title = TITLE, + isEnabled = true, + summary = SUMMARY, + icon = context.getDrawable(R.drawable.ic_settings_print)!!, + componentName = "ComponentName", + ) + + @Test + fun printService_titleDisplayed() { + composeTestRule.setContent { + PrintService(displayInfo) + } + + composeTestRule.onNodeWithText(TITLE).isDisplayed() + } + + @Test + fun printService_summaryDisplayed() { + composeTestRule.setContent { + PrintService(displayInfo) + } + + composeTestRule.onNodeWithText(SUMMARY).isDisplayed() + } + + @Test + fun printService_onClick() { + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + PrintService(displayInfo) + } + } + + composeTestRule.onNodeWithText(TITLE).performClick() + + verify(context).startActivity(argThat { + val fragment = getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT) + val arguments = getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS)!! + fragment == PrintServiceSettingsFragment::class.qualifiedName && + arguments.getBoolean(EXTRA_CHECKED) == displayInfo.isEnabled && + arguments.getString(EXTRA_TITLE) == displayInfo.title && + arguments.getString(EXTRA_SERVICE_COMPONENT_NAME) == displayInfo.componentName + }) + } + + private companion object { + const val TITLE = "Title" + const val SUMMARY = "Summary" + } +}