From 9e88efecdfcd80c3684d66c3083424a2d153e6ae Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Mon, 17 Jun 2024 17:56:45 +0800 Subject: [PATCH 1/7] Refactor PrintSettings (2/n) The "Add service" button. Bug: 320076351 Flag: com.android.settings.flags.refactor_print_settings Test: manual Test: unit test Change-Id: I624293edcbfe9ef8388d48759611aeac522352a4 --- .../print/PrintSettingsPageProvider.kt | 133 ++++++++++++++---- .../print/PrintSettingsPageProviderTest.kt | 76 ++++++---- 2 files changed, 149 insertions(+), 60 deletions(-) diff --git a/src/com/android/settings/print/PrintSettingsPageProvider.kt b/src/com/android/settings/print/PrintSettingsPageProvider.kt index aac0a5d0cf4..f28f0bcd6f4 100644 --- a/src/com/android/settings/print/PrintSettingsPageProvider.kt +++ b/src/com/android/settings/print/PrintSettingsPageProvider.kt @@ -17,16 +17,32 @@ package com.android.settings.print import android.app.settings.SettingsEnums +import android.content.Context +import android.content.Intent +import android.net.Uri import android.os.Bundle +import android.provider.Settings import androidx.annotation.VisibleForTesting import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Print +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settings.R @@ -36,13 +52,18 @@ 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.rememberContext import com.android.settingslib.spa.framework.compose.rememberDrawablePainter import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.SettingsOpacity 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.spa.widget.ui.SettingsIcon +import com.android.settingslib.spaprivileged.settingsprovider.settingsSecureStringFlow import com.android.settingslib.spaprivileged.template.common.UserProfilePager +import kotlinx.coroutines.flow.Flow object PrintSettingsPageProvider : SettingsPageProvider { override val name = "PrintSettings" @@ -52,51 +73,101 @@ object PrintSettingsPageProvider : SettingsPageProvider { RegularScaffold(title = stringResource(R.string.print_settings)) { val context = LocalContext.current val printRepository = remember(context) { PrintRepository(context) } - UserProfilePager { - PrintServices(printRepository) - } + 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) + val printServiceDisplayInfos by + remember { printRepository.printServiceDisplayInfosFlow() } + .collectAsStateWithLifecycle(initialValue = emptyList()) + if (printServiceDisplayInfos.isEmpty()) { + NoServicesInstalled() + } else { + Category(title = stringResource(R.string.print_settings_title)) { + for (printServiceDisplayInfo in printServiceDisplayInfos) { + PrintService(printServiceDisplayInfo) + } } } + AddPrintService() + } + + @Composable + private fun NoServicesInstalled() { + Column( + modifier = Modifier.fillMaxSize().padding(SettingsDimension.itemPaddingAround), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Outlined.Print, + contentDescription = null, + modifier = + Modifier.size(110.dp) + .padding(SettingsDimension.itemPaddingAround) + .alpha(SettingsOpacity.SurfaceTone), + ) + Text( + text = stringResource(R.string.print_no_services_installed), + style = MaterialTheme.typography.titleLarge, + ) + } } @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 - ) + Preference( + 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), ) - setSourceMetricsCategory(SettingsEnums.PRINT_SETTINGS) - }.launch() + } + override val onClick = { launchPrintServiceSettings(context, displayInfo) } } - }) + ) + } + + private fun launchPrintServiceSettings(context: Context, displayInfo: PrintServiceDisplayInfo) { + 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() + } + + @Composable + fun AddPrintService( + searchUriFlow: Flow = rememberContext { context -> + context.settingsSecureStringFlow(Settings.Secure.PRINT_SERVICE_SEARCH_URI) + }, + ) { + val context = LocalContext.current + val searchUri by searchUriFlow.collectAsStateWithLifecycle("") + if (searchUri.isEmpty()) return + Preference( + object : PreferenceModel { + override val title = stringResource(R.string.print_menu_item_add_service) + override val icon = @Composable { SettingsIcon(imageVector = Icons.Outlined.Add) } + override val onClick = { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(searchUri))) + } + } + ) } } diff --git a/tests/spa_unit/src/com/android/settings/print/PrintSettingsPageProviderTest.kt b/tests/spa_unit/src/com/android/settings/print/PrintSettingsPageProviderTest.kt index 746816b52c8..25714063d15 100644 --- a/tests/spa_unit/src/com/android/settings/print/PrintSettingsPageProviderTest.kt +++ b/tests/spa_unit/src/com/android/settings/print/PrintSettingsPageProviderTest.kt @@ -17,6 +17,7 @@ package com.android.settings.print import android.content.Context +import android.net.Uri import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.isDisplayed @@ -31,7 +32,9 @@ 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.AddPrintService import com.android.settings.print.PrintSettingsPageProvider.PrintService +import kotlinx.coroutines.flow.flowOf import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -44,35 +47,32 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class PrintSettingsPageProviderTest { - @get:Rule - val composeTestRule = createComposeRule() + @get:Rule val composeTestRule = createComposeRule() - private val context: Context = spy(ApplicationProvider.getApplicationContext()) { - doNothing().whenever(mock).startActivity(any()) - } + 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", - ) + 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.setContent { PrintService(displayInfo) } composeTestRule.onNodeWithText(TITLE).isDisplayed() } @Test fun printService_summaryDisplayed() { - composeTestRule.setContent { - PrintService(displayInfo) - } + composeTestRule.setContent { PrintService(displayInfo) } composeTestRule.onNodeWithText(SUMMARY).isDisplayed() } @@ -80,25 +80,43 @@ class PrintSettingsPageProviderTest { @Test fun printService_onClick() { composeTestRule.setContent { - CompositionLocalProvider(LocalContext provides context) { - PrintService(displayInfo) - } + 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 - }) + 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 + } + ) + } + + @Test + fun addPrintService_onClick() { + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + AddPrintService(flowOf(SEARCH_URI)) + } + } + + composeTestRule + .onNodeWithText(context.getString(R.string.print_menu_item_add_service)) + .performClick() + + verify(context).startActivity(argThat { data == Uri.parse(SEARCH_URI) }) } private companion object { const val TITLE = "Title" const val SUMMARY = "Summary" + const val SEARCH_URI = "search.uri" } } From 5097783e6e42752663600903d302244e3303610f Mon Sep 17 00:00:00 2001 From: Kai Zhou Date: Tue, 7 May 2024 08:00:41 +0000 Subject: [PATCH 2/7] Retrieve WifiDppQrGeneratorFragment at runtime. * Add getWifiDppQrCodeGeneratorFragment() in WifiFeatureProvider * WifiDppConfiguratorActivity calls from the FeatureFactory to get WifiDppQrCodeGeneratorFragment instance * Use a flag to guard this behaviour * Updated a few private variables of WifiDppQrCodeGeneratorFragment to protected so I can access them in derived classes. Bug: b/338891273 Settings Intake: b/329012096 Test: manual test, override the flag locally, and check I can switch to overriden implementation. Flag: com.android.settings.flags.enable_wifi_sharing_runtime_fragment Change-Id: I715bf89bd9be221f06914e87629c76bf7a2f743f --- aconfig/settings_wifi_flag_declarations.aconfig | 12 ++++++++++++ .../wifi/dpp/WifiDppConfiguratorActivity.java | 9 ++++++++- .../wifi/dpp/WifiDppQrCodeGeneratorFragment.java | 4 ++-- .../settings/wifi/factory/WifiFeatureProvider.java | 10 ++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 aconfig/settings_wifi_flag_declarations.aconfig diff --git a/aconfig/settings_wifi_flag_declarations.aconfig b/aconfig/settings_wifi_flag_declarations.aconfig new file mode 100644 index 00000000000..cb8007f2e93 --- /dev/null +++ b/aconfig/settings_wifi_flag_declarations.aconfig @@ -0,0 +1,12 @@ +package: "com.android.settings.flags" +container: "system_ext" + +# NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. + +flag { + name: "enable_wifi_sharing_runtime_fragment" + namespace: "prism_qr" + description: "Use WifiFeatureProvider to get the instance of WifiDppQrCodeGeneratorFragment." + bug: "329012096" +} + diff --git a/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java b/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java index a7527d7332f..c7ad9ca0d8d 100644 --- a/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java +++ b/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java @@ -35,6 +35,8 @@ import androidx.annotation.VisibleForTesting; import androidx.fragment.app.FragmentTransaction; import com.android.settings.R; +import com.android.settings.flags.Flags; +import com.android.settings.overlay.FeatureFactory; import java.util.List; @@ -236,7 +238,12 @@ public class WifiDppConfiguratorActivity extends WifiDppBaseActivity implements WifiDppUtils.TAG_FRAGMENT_QR_CODE_GENERATOR); if (fragment == null) { - fragment = new WifiDppQrCodeGeneratorFragment(); + if (Flags.enableWifiSharingRuntimeFragment()) { + fragment = FeatureFactory.getFeatureFactory().getWifiFeatureProvider() + .getWifiDppQrCodeGeneratorFragment(); + } else { + fragment = new WifiDppQrCodeGeneratorFragment(); + } } else { if (fragment.isVisible()) { return; diff --git a/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java b/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java index 3d437e22e7d..1213b0dc2e3 100644 --- a/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java +++ b/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java @@ -56,7 +56,7 @@ public class WifiDppQrCodeGeneratorFragment extends WifiDppQrCodeBaseFragment { private static final String TAG = "WifiDppQrCodeGeneratorFragment"; private ImageView mQrCodeView; - private String mQrCode; + protected String mQrCode; private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label"; private static final String CHIP_ICON_METADATA_KEY = "android.service.chooser.chip_icon"; @@ -258,7 +258,7 @@ public class WifiDppQrCodeGeneratorFragment extends WifiDppQrCodeBaseFragment { return button; } - private void setQrCode() { + protected void setQrCode() { try { final int qrcodeSize = getContext().getResources().getDimensionPixelSize( R.dimen.qrcode_size); diff --git a/src/com/android/settings/wifi/factory/WifiFeatureProvider.java b/src/com/android/settings/wifi/factory/WifiFeatureProvider.java index 5ab899afdc5..e5bf81a3c35 100644 --- a/src/com/android/settings/wifi/factory/WifiFeatureProvider.java +++ b/src/com/android/settings/wifi/factory/WifiFeatureProvider.java @@ -27,6 +27,7 @@ import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelStoreOwner; import com.android.settings.wifi.details.WifiNetworkDetailsViewModel; +import com.android.settings.wifi.dpp.WifiDppQrCodeGeneratorFragment; import com.android.settings.wifi.repository.SharedConnectivityRepository; import com.android.settings.wifi.repository.WifiHotspotRepository; import com.android.settings.wifi.tether.WifiHotspotSecurityViewModel; @@ -146,6 +147,15 @@ public class WifiFeatureProvider { return viewModel; } + /** + * Gets an instance of WifiDppQrCodeGeneratorFragment + */ + public WifiDppQrCodeGeneratorFragment getWifiDppQrCodeGeneratorFragment() { + WifiDppQrCodeGeneratorFragment fragment = new WifiDppQrCodeGeneratorFragment(); + verboseLog(TAG, "getWifiDppQrCodeGeneratorFragment():" + fragment); + return fragment; + } + /** * Send a {@link Log#VERBOSE} log message. * From d226a97617f535777dc6d3ffa84fa18902d3f7c9 Mon Sep 17 00:00:00 2001 From: YK Hung Date: Tue, 18 Jun 2024 16:18:40 +0000 Subject: [PATCH 3/7] Dump the BatteryReattribute from the database into bugreport (2/5) Example: BatteryReattributeEntity{ Jun 18, 2024, 16:22:51 Jun 18, 2024, 16:23:01 reattribute_data { key: 1001 value: 0.1 } reattribute_data { key: 1002 value: 0.99 } timestamp_end: 1718727781368 timestamp_start: 1718727771368 } BatteryReattributeEntity{ Jun 18, 2024, 16:22:31 Jun 18, 2024, 16:22:51 reattribute_data { key: 1003 value: 1.0 } timestamp_end: 1718727771368 timestamp_start: 1718727751368 } Bug: 346706894 Test: atest SettingsRoboTests:com.android.settings.fuelgauge.batteryusage.bugreport Flag: EXEMPT bug fix Change-Id: Ie2544b9f43e399d87eb29354b401834372c3a9f7 --- .../bugreport/BugReportContentProvider.java | 1 + .../batteryusage/bugreport/LogUtils.java | 22 ++++ .../db/BatteryReattributeEntity.java | 1 + .../batteryusage/bugreport/LogUtilsTest.java | 102 ++++++++++++++++++ 4 files changed, 126 insertions(+) create mode 100644 tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/LogUtilsTest.java diff --git a/src/com/android/settings/fuelgauge/batteryusage/bugreport/BugReportContentProvider.java b/src/com/android/settings/fuelgauge/batteryusage/bugreport/BugReportContentProvider.java index 7613d9ab097..baae10959c3 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/bugreport/BugReportContentProvider.java +++ b/src/com/android/settings/fuelgauge/batteryusage/bugreport/BugReportContentProvider.java @@ -60,6 +60,7 @@ public final class BugReportContentProvider extends ContentProvider { LogUtils.dumpBatteryUsageSlotDatabaseHist(context, writer); LogUtils.dumpBatteryEventDatabaseHist(context, writer); LogUtils.dumpBatteryStateDatabaseHist(context, writer); + LogUtils.dumpBatteryReattributeDatabaseHist(context, writer); } @Override diff --git a/src/com/android/settings/fuelgauge/batteryusage/bugreport/LogUtils.java b/src/com/android/settings/fuelgauge/batteryusage/bugreport/LogUtils.java index b2300308fd4..d2f3adcd9d7 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/bugreport/LogUtils.java +++ b/src/com/android/settings/fuelgauge/batteryusage/bugreport/LogUtils.java @@ -19,6 +19,8 @@ package com.android.settings.fuelgauge.batteryusage.bugreport; import android.content.Context; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.batteryusage.AppOptModeSharedPreferencesUtils; import com.android.settings.fuelgauge.batteryusage.AppOptimizationModeEvent; @@ -29,6 +31,8 @@ import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventDao; import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity; import com.android.settings.fuelgauge.batteryusage.db.BatteryEventDao; import com.android.settings.fuelgauge.batteryusage.db.BatteryEventEntity; +import com.android.settings.fuelgauge.batteryusage.db.BatteryReattributeDao; +import com.android.settings.fuelgauge.batteryusage.db.BatteryReattributeEntity; import com.android.settings.fuelgauge.batteryusage.db.BatteryState; import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao; import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase; @@ -127,6 +131,24 @@ public final class LogUtils { dumpListItems(writer, entities, entity -> entity); } + static void dumpBatteryReattributeDatabaseHist(Context context, PrintWriter writer) { + dumpBatteryReattributeDatabaseHist( + BatteryStateDatabase.getInstance(context).batteryReattributeDao(), + writer); + } + + @VisibleForTesting + static void dumpBatteryReattributeDatabaseHist( + BatteryReattributeDao batteryReattributeDao, PrintWriter writer) { + writer.println("\n\tBatteryReattribute DatabaseHistory:"); + final List entities = + batteryReattributeDao.getAllAfter( + Clock.systemUTC().millis() - DUMP_TIME_OFFSET.toMillis()); + if (entities != null && !entities.isEmpty()) { + dumpListItems(writer, entities, entity -> entity); + } + } + private static void dumpListItems( PrintWriter writer, List itemList, Function itemConverter) { final AtomicInteger counter = new AtomicInteger(0); diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryReattributeEntity.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryReattributeEntity.java index aa7e50ea9be..d518230b28c 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryReattributeEntity.java +++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryReattributeEntity.java @@ -62,6 +62,7 @@ public class BatteryReattributeEntity { .append("\nBatteryReattributeEntity{") .append("\n\t" + utcToLocalTimeForLogging(timestampStart)) .append("\n\t" + utcToLocalTimeForLogging(timestampEnd)) + .append("\n\t" + ConvertUtils.decodeBatteryReattribute(reattributeData)) .append("\n}"); return builder.toString(); } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/LogUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/LogUtilsTest.java new file mode 100644 index 00000000000..bf2b191a65d --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/LogUtilsTest.java @@ -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.fuelgauge.batteryusage.bugreport; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.fuelgauge.batteryusage.BatteryReattribute; +import com.android.settings.fuelgauge.batteryusage.db.BatteryReattributeDao; +import com.android.settings.fuelgauge.batteryusage.db.BatteryReattributeEntity; +import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase; +import com.android.settings.testutils.BatteryTestUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.robolectric.RobolectricTestRunner; + +import java.io.PrintWriter; +import java.io.StringWriter; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@RunWith(RobolectricTestRunner.class) +public final class LogUtilsTest { + + private StringWriter mTestStringWriter; + private PrintWriter mTestPrintWriter; + private Context mContext; + private BatteryStateDatabase mDatabase; + private BatteryReattributeDao mBatteryReattributeDao; + + @Before + public void setUp() { + mContext = ApplicationProvider.getApplicationContext(); + mTestStringWriter = new StringWriter(); + mTestPrintWriter = new PrintWriter(mTestStringWriter); + mDatabase = BatteryTestUtils.setUpBatteryStateDatabase(mContext); + mBatteryReattributeDao = mDatabase.batteryReattributeDao(); + } + + @After + public void cleanUp() { + mBatteryReattributeDao.clearAll(); + } + + @Test + public void dumpBatteryReattributeDatabaseHist_noData_printExpectedResult() { + LogUtils.dumpBatteryReattributeDatabaseHist(mBatteryReattributeDao, mTestPrintWriter); + + assertThat(mTestStringWriter.toString()) + .contains("BatteryReattribute DatabaseHistory:"); + } + + @Test + public void dumpBatteryReattributeDatabaseHist_printExpectedResult() { + final long currentTimeMillis = System.currentTimeMillis(); + // Insert the first testing data. + final BatteryReattribute batteryReattribute1 = + BatteryReattribute.newBuilder() + .setTimestampStart(currentTimeMillis - 20000) + .setTimestampEnd(currentTimeMillis - 10000) + .putReattributeData(1001, 0.1f) + .putReattributeData(1002, 0.99f) + .build(); + mBatteryReattributeDao.insert(new BatteryReattributeEntity(batteryReattribute1)); + // Insert the second testing data. + final BatteryReattribute batteryReattribute2 = + BatteryReattribute.newBuilder() + .setTimestampStart(currentTimeMillis - 40000) + .setTimestampEnd(currentTimeMillis - 20000) + .putReattributeData(1003, 1f) + .build(); + mBatteryReattributeDao.insert(new BatteryReattributeEntity(batteryReattribute2)); + + LogUtils.dumpBatteryReattributeDatabaseHist(mBatteryReattributeDao, mTestPrintWriter); + + final String result = mTestStringWriter.toString(); + assertThat(result).contains("BatteryReattribute DatabaseHistory:"); + assertThat(result).contains(batteryReattribute1.toString()); + assertThat(result).contains(batteryReattribute2.toString()); + } +} From 71331802ef4078762cf83ce3408e9541d9cd826c Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 18 Jun 2024 18:16:25 +0800 Subject: [PATCH 4/7] Refactor ApnEditCarrierEnabled Also fix tests. Bug: 342374681 Flag: EXEMPT refactor Test: manual - on Apn Edit Test: unit test Change-Id: I6db7f27a26341084940be4f8039f5097faa07ec8 --- .../network/apn/ApnEditCarrierEnabled.kt | 40 +++++++ .../network/apn/ApnEditPageProvider.kt | 14 +-- .../settings/network/apn/ApnRepository.kt | 2 +- .../android/settings/network/apn/ApnStatus.kt | 9 +- .../network/apn/ApnEditCarrierEnabledTest.kt | 109 ++++++++++++++++++ .../network/apn/ApnEditPageProviderTest.kt | 42 ------- 6 files changed, 153 insertions(+), 63 deletions(-) create mode 100644 src/com/android/settings/network/apn/ApnEditCarrierEnabled.kt create mode 100644 tests/spa_unit/src/com/android/settings/network/apn/ApnEditCarrierEnabledTest.kt diff --git a/src/com/android/settings/network/apn/ApnEditCarrierEnabled.kt b/src/com/android/settings/network/apn/ApnEditCarrierEnabled.kt new file mode 100644 index 00000000000..bd58da873ab --- /dev/null +++ b/src/com/android/settings/network/apn/ApnEditCarrierEnabled.kt @@ -0,0 +1,40 @@ +/* + * 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.network.apn + +import android.provider.Telephony +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.booleanResource +import androidx.compose.ui.res.stringResource +import com.android.settings.R +import com.android.settingslib.spa.widget.preference.SwitchPreference +import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel + +@Composable +fun ApnEditCarrierEnabled(apnData: ApnData, onCarrierEnabledChanged: (Boolean) -> Unit) { + SwitchPreference( + object : SwitchPreferenceModel { + override val title = stringResource(R.string.carrier_enabled) + val allowEdit = booleanResource(R.bool.config_allow_edit_carrier_enabled) + override val changeable = { + allowEdit && apnData.isFieldEnabled(Telephony.Carriers.CARRIER_ENABLED) + } + override val checked = { apnData.carrierEnabled } + override val onCheckedChange = onCarrierEnabledChanged + } + ) +} diff --git a/src/com/android/settings/network/apn/ApnEditPageProvider.kt b/src/com/android/settings/network/apn/ApnEditPageProvider.kt index 099e2faa357..544208247fa 100644 --- a/src/com/android/settings/network/apn/ApnEditPageProvider.kt +++ b/src/com/android/settings/network/apn/ApnEditPageProvider.kt @@ -235,19 +235,7 @@ fun ApnPage(apnDataInit: ApnData, apnDataCur: MutableState, uriInit: Ur enabled = apnData.isFieldEnabled(Telephony.Carriers.ROAMING_PROTOCOL), ) { apnData = apnData.copy(apnRoaming = it) } ApnNetworkTypeCheckBox(apnData) { apnData = apnData.copy(networkType = it) } - SwitchPreference( - object : SwitchPreferenceModel { - override val title = stringResource(R.string.carrier_enabled) - override val changeable = { - apnData.apnEnableEnabled && - apnData.isFieldEnabled(Telephony.Carriers.CARRIER_ENABLED) - } - override val checked = { apnData.apnEnable } - override val onCheckedChange = { newChecked: Boolean -> - apnData = apnData.copy(apnEnable = newChecked) - } - } - ) + ApnEditCarrierEnabled(apnData) { apnData = apnData.copy(carrierEnabled = it) } } } } \ No newline at end of file diff --git a/src/com/android/settings/network/apn/ApnRepository.kt b/src/com/android/settings/network/apn/ApnRepository.kt index 2d41976b239..843371501b7 100644 --- a/src/com/android/settings/network/apn/ApnRepository.kt +++ b/src/com/android/settings/network/apn/ApnRepository.kt @@ -90,7 +90,7 @@ fun getApnDataFromUri(uri: Uri, context: Context): ApnData { apnRoaming = context.convertProtocol2Options( cursor.getString(Telephony.Carriers.ROAMING_PROTOCOL) ), - apnEnable = cursor.getInt(Telephony.Carriers.CARRIER_ENABLED) == 1, + carrierEnabled = cursor.getInt(Telephony.Carriers.CARRIER_ENABLED) == 1, networkType = cursor.getLong(Telephony.Carriers.NETWORK_TYPE_BITMASK), edited = cursor.getInt(Telephony.Carriers.EDITED_STATUS), userEditable = cursor.getInt(Telephony.Carriers.USER_EDITABLE), diff --git a/src/com/android/settings/network/apn/ApnStatus.kt b/src/com/android/settings/network/apn/ApnStatus.kt index dc50452a995..6492d39417d 100644 --- a/src/com/android/settings/network/apn/ApnStatus.kt +++ b/src/com/android/settings/network/apn/ApnStatus.kt @@ -44,11 +44,10 @@ data class ApnData( val apnType: String = "", val apnProtocol: Int = -1, val apnRoaming: Int = -1, - val apnEnable: Boolean = true, + val carrierEnabled: Boolean = true, val networkType: Long = 0, val edited: Int = Telephony.Carriers.USER_EDITED, val userEditable: Int = 1, - val apnEnableEnabled: Boolean = true, val newApn: Boolean = false, val subId: Int = -1, val validEnabled: Boolean = false, @@ -72,7 +71,7 @@ data class ApnData( Telephony.Carriers.NETWORK_TYPE_BITMASK to networkType, // Copy network type into lingering network type. Telephony.Carriers.LINGERING_NETWORK_TYPE_BITMASK to networkType, - Telephony.Carriers.CARRIER_ENABLED to apnEnable, + Telephony.Carriers.CARRIER_ENABLED to carrierEnabled, Telephony.Carriers.EDITED_STATUS to Telephony.Carriers.USER_EDITED, ) @@ -134,10 +133,6 @@ fun getApnDataInit(arguments: Bundle, context: Context, uriInit: Uri, subId: Int ) } - apnDataInit = apnDataInit.copy( - apnEnableEnabled = - context.resources.getBoolean(R.bool.config_allow_edit_carrier_enabled) - ) // TODO: mIsCarrierIdApn return disableInit(apnDataInit) } diff --git a/tests/spa_unit/src/com/android/settings/network/apn/ApnEditCarrierEnabledTest.kt b/tests/spa_unit/src/com/android/settings/network/apn/ApnEditCarrierEnabledTest.kt new file mode 100644 index 00000000000..bd97482ddfa --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/apn/ApnEditCarrierEnabledTest.kt @@ -0,0 +1,109 @@ +/* + * 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.network.apn + +import android.content.Context +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.assertIsOff +import androidx.compose.ui.test.assertIsOn +import androidx.compose.ui.test.junit4.ComposeTestRule +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 org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class ApnEditCarrierEnabledTest { + + @get:Rule val composeTestRule = createComposeRule() + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) {} + + private val resources = spy(context.resources) {} + + @Before + fun setUp() { + context.stub { on { resources } doReturn resources } + } + + @Test + fun carrierEnabled_displayed() { + composeTestRule.setContent { ApnEditCarrierEnabled(ApnData()) {} } + + composeTestRule.onCarrierEnabled().assertIsDisplayed() + } + + @Test + fun carrierEnabled_isChecked() { + val apnData = ApnData(carrierEnabled = true) + + composeTestRule.setContent { ApnEditCarrierEnabled(apnData) {} } + + composeTestRule.onCarrierEnabled().assertIsOn() + } + + @Test + fun carrierEnabled_allowEdit_checkChanged() { + resources.stub { on { getBoolean(R.bool.config_allow_edit_carrier_enabled) } doReturn true } + var apnData by mutableStateOf(ApnData(carrierEnabled = true)) + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + ApnEditCarrierEnabled(apnData) { apnData = apnData.copy(carrierEnabled = it) } + } + } + + composeTestRule.onCarrierEnabled().performClick() + + composeTestRule.onCarrierEnabled().assertIsEnabled().assertIsOff() + } + + @Test + fun carrierEnabled_notAllowEdit_checkNotChanged() { + resources.stub { + on { getBoolean(R.bool.config_allow_edit_carrier_enabled) } doReturn false + } + var apnData by mutableStateOf(ApnData(carrierEnabled = true)) + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + ApnEditCarrierEnabled(apnData) { apnData = apnData.copy(carrierEnabled = it) } + } + } + + composeTestRule.onCarrierEnabled().performClick() + + composeTestRule.onCarrierEnabled().assertIsNotEnabled().assertIsOn() + } + + private fun ComposeTestRule.onCarrierEnabled() = + onNodeWithText(context.getString(R.string.carrier_enabled)) +} diff --git a/tests/spa_unit/src/com/android/settings/network/apn/ApnEditPageProviderTest.kt b/tests/spa_unit/src/com/android/settings/network/apn/ApnEditPageProviderTest.kt index 3621948c9fb..d310604ccf9 100644 --- a/tests/spa_unit/src/com/android/settings/network/apn/ApnEditPageProviderTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/apn/ApnEditPageProviderTest.kt @@ -21,24 +21,17 @@ import android.net.Uri import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsOff -import androidx.compose.ui.test.assertIsOn import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.isFocused import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onChild import androidx.compose.ui.test.onChildAt -import androidx.compose.ui.test.onLast import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToNode import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.R import com.google.common.truth.Truth -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -55,7 +48,6 @@ class ApnEditPageProviderTest { private val port = "port" private val apnType = context.resources.getString(R.string.apn_type) private val apnRoaming = "IPv4" - private val apnEnable = context.resources.getString(R.string.carrier_enabled) private val apnProtocolOptions = context.resources.getStringArray(R.array.apn_protocol_entries).toList() private val passwordTitle = context.resources.getString(R.string.apn_password) @@ -65,7 +57,6 @@ class ApnEditPageProviderTest { port = port, apnType = apnType, apnRoaming = apnProtocolOptions.indexOf(apnRoaming), - apnEnable = true ) private val apnData = mutableStateOf( apnInit @@ -133,39 +124,6 @@ class ApnEditPageProviderTest { composeTestRule.onNodeWithText(apnRoaming, true).assertIsDisplayed() } - @Ignore("b/342374681") - @Test - fun carrier_enabled_displayed() { - composeTestRule.setContent { - ApnPage(apnInit, remember { apnData }, uri) - } - composeTestRule.onRoot().onChild().onChildAt(0) - .performScrollToNode(hasText(apnEnable, true)) - composeTestRule.onNodeWithText(apnEnable, true).assertIsDisplayed() - } - - @Test - fun carrier_enabled_isChecked() { - composeTestRule.setContent { - ApnPage(apnInit, remember { apnData }, uri) - } - composeTestRule.onRoot().onChild().onChildAt(0) - .performScrollToNode(hasText(apnEnable, true)) - composeTestRule.onNodeWithText(apnEnable, true).assertIsOn() - } - - @Ignore("b/342374681") - @Test - fun carrier_enabled_checkChanged() { - composeTestRule.setContent { - ApnPage(apnInit, remember { apnData }, uri) - } - composeTestRule.onRoot().onChild().onChildAt(0) - .performScrollToNode(hasText(apnEnable, true)) - composeTestRule.onNodeWithText(apnEnable, true).performClick() - composeTestRule.onNodeWithText(apnEnable, true).assertIsOff() - } - @Test fun password_displayed() { composeTestRule.setContent { From 89df32b48c3ec1a4cb715070aabc628e6da66fff Mon Sep 17 00:00:00 2001 From: chelseahao Date: Tue, 18 Jun 2024 19:36:20 +0800 Subject: [PATCH 5/7] [Audiosharing] Add log for debugging. Test: atest -c com.android.settings.connecteddevice.audiosharing.audiostreams Bug: 347605485 Flag: com.android.settingslib.flags.enable_le_audio_qr_code_private_broadcast_sharing Change-Id: I319747652837c08ffcbbd5980609a0f8f35e3c24 --- .../audiostreams/AudioStreamMediaService.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java index 275b811d633..4a2912676d1 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java @@ -74,6 +74,10 @@ public class AudioStreamMediaService extends Service { public void onSourceLost(int broadcastId) { super.onSourceLost(broadcastId); if (broadcastId == mBroadcastId) { + Log.d(TAG, "onSourceLost() : stopSelf"); + if (mNotificationManager != null) { + mNotificationManager.cancel(NOTIFICATION_ID); + } stopSelf(); } } @@ -85,6 +89,10 @@ public class AudioStreamMediaService extends Service { && mAudioStreamsHelper.getAllConnectedSources().stream() .map(BluetoothLeBroadcastReceiveState::getBroadcastId) .noneMatch(id -> id == mBroadcastId)) { + Log.d(TAG, "onSourceRemoved() : stopSelf"); + if (mNotificationManager != null) { + mNotificationManager.cancel(NOTIFICATION_ID); + } stopSelf(); } } @@ -95,6 +103,10 @@ public class AudioStreamMediaService extends Service { @Override public void onBluetoothStateChanged(int bluetoothState) { if (BluetoothAdapter.STATE_OFF == bluetoothState) { + Log.d(TAG, "onBluetoothStateChanged() : stopSelf"); + if (mNotificationManager != null) { + mNotificationManager.cancel(NOTIFICATION_ID); + } stopSelf(); } } @@ -119,6 +131,10 @@ public class AudioStreamMediaService extends Service { }); } if (mDevices == null || mDevices.isEmpty()) { + Log.d(TAG, "onProfileConnectionStateChanged() : stopSelf"); + if (mNotificationManager != null) { + mNotificationManager.cancel(NOTIFICATION_ID); + } stopSelf(); } } @@ -245,21 +261,27 @@ public class AudioStreamMediaService extends Service { @Override public void onDestroy() { + Log.d(TAG, "onDestroy()"); super.onDestroy(); if (!AudioSharingUtils.isFeatureEnabled()) { + Log.d(TAG, "onDestroy() : skip due to feature not enabled"); return; } if (mLocalBtManager != null) { + Log.d(TAG, "onDestroy() : unregister mBluetoothCallback"); mLocalBtManager.getEventManager().unregisterCallback(mBluetoothCallback); } if (mLeBroadcastAssistant != null) { + Log.d(TAG, "onDestroy() : unregister mBroadcastAssistantCallback"); mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); } if (mVolumeControl != null) { + Log.d(TAG, "onDestroy() : unregister mVolumeControlCallback"); mVolumeControl.unregisterCallback(mVolumeControlCallback); } if (mLocalSession != null) { + Log.d(TAG, "onDestroy() : release mLocalSession"); mLocalSession.release(); mLocalSession = null; } @@ -272,6 +294,9 @@ public class AudioStreamMediaService extends Service { mBroadcastId = intent != null ? intent.getIntExtra(BROADCAST_ID, -1) : -1; if (mBroadcastId == -1) { Log.w(TAG, "Invalid broadcast ID. Service will not start."); + if (mNotificationManager != null) { + mNotificationManager.cancel(NOTIFICATION_ID); + } stopSelf(); return START_NOT_STICKY; } @@ -281,6 +306,9 @@ public class AudioStreamMediaService extends Service { } if (mDevices == null || mDevices.isEmpty()) { Log.w(TAG, "No device. Service will not start."); + if (mNotificationManager != null) { + mNotificationManager.cancel(NOTIFICATION_ID); + } stopSelf(); return START_NOT_STICKY; } From 0bf4899f3ef3f9fc61cae8fb20851e4be5d477df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Mon, 17 Jun 2024 16:17:15 +0200 Subject: [PATCH 6/7] Support adding an automatic schedule to previously-manual system-owned modes Bug: 342156843 Bug: 326442408 Flag: android.app.modes_ui Test: atest com.android.settings.notification.modes Change-Id: Ib7d15b8fc949fa73015537f03a882ef7eb6b4bcd --- res/layout/zen_mode_type_item.xml | 62 ++++++++ res/values/strings.xml | 13 ++ .../settings/notification/modes/ZenMode.java | 56 +++++++ .../notification/modes/ZenModeFragment.java | 2 +- .../modes/ZenModeScheduleChooserDialog.java | 149 ++++++++++++++++++ ...enModeSetCalendarPreferenceController.java | 10 +- ...enModeSetSchedulePreferenceController.java | 13 +- ...odeSetTriggerLinkPreferenceController.java | 77 +++++---- .../notification/modes/ZenModesBackend.java | 17 +- .../modes/ZenSubSettingLauncher.java | 2 +- ...deSetCalendarPreferenceControllerTest.java | 6 +- ...deSetSchedulePreferenceControllerTest.java | 7 +- ...etTriggerLinkPreferenceControllerTest.java | 104 +++++++++--- 13 files changed, 430 insertions(+), 88 deletions(-) create mode 100644 res/layout/zen_mode_type_item.xml create mode 100644 src/com/android/settings/notification/modes/ZenModeScheduleChooserDialog.java diff --git a/res/layout/zen_mode_type_item.xml b/res/layout/zen_mode_type_item.xml new file mode 100644 index 00000000000..841ca0066f8 --- /dev/null +++ b/res/layout/zen_mode_type_item.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index f92fd2aca84..386ece3caf4 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7927,6 +7927,19 @@ Only get notified by important people and apps + + Select activation type + + + Time + + Ex. \"9:30 – 5:00 PM\" + + + Calendar + + Ex. \"Personal calendar\" + Limit interruptions diff --git a/src/com/android/settings/notification/modes/ZenMode.java b/src/com/android/settings/notification/modes/ZenMode.java index cbe915b5bab..1040d1e0021 100644 --- a/src/com/android/settings/notification/modes/ZenMode.java +++ b/src/com/android/settings/notification/modes/ZenMode.java @@ -18,6 +18,12 @@ package com.android.settings.notification.modes; import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent; +import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleTime; +import static android.service.notification.ZenModeConfig.tryParseEventConditionId; +import static android.service.notification.ZenModeConfig.tryParseScheduleConditionId; + +import static com.google.common.base.Preconditions.checkState; import static java.util.Objects.requireNonNull; @@ -26,7 +32,10 @@ import android.app.AutomaticZenRule; import android.app.NotificationManager; import android.content.Context; import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.service.notification.SystemZenRules; import android.service.notification.ZenDeviceEffects; +import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; import android.util.Log; @@ -204,6 +213,44 @@ class ZenMode { : new ZenDeviceEffects.Builder().build(); } + public void setCustomModeConditionId(Context context, Uri conditionId) { + checkState(SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName()), + "Trying to change condition of non-system-owned rule %s (to %s)", + mRule, conditionId); + + Uri oldCondition = mRule.getConditionId(); + mRule.setConditionId(conditionId); + + ZenModeConfig.ScheduleInfo scheduleInfo = tryParseScheduleConditionId(conditionId); + if (scheduleInfo != null) { + mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_TIME); + mRule.setOwner(ZenModeConfig.getScheduleConditionProvider()); + mRule.setTriggerDescription( + getTriggerDescriptionForScheduleTime(context, scheduleInfo)); + return; + } + + ZenModeConfig.EventInfo eventInfo = tryParseEventConditionId(conditionId); + if (eventInfo != null) { + mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR); + mRule.setOwner(ZenModeConfig.getEventConditionProvider()); + mRule.setTriggerDescription(getTriggerDescriptionForScheduleEvent(context, eventInfo)); + return; + } + + if (ZenModeConfig.isValidCustomManualConditionId(conditionId)) { + mRule.setType(AutomaticZenRule.TYPE_OTHER); + mRule.setOwner(ZenModeConfig.getCustomManualConditionProvider()); + mRule.setTriggerDescription(""); + return; + } + + Log.wtf(TAG, String.format( + "Changed condition of rule %s (%s -> %s) but cannot recognize which kind of " + + "condition it was!", + mRule, oldCondition, conditionId)); + } + public boolean canEditName() { return !isManualDnd(); } @@ -224,6 +271,15 @@ class ZenMode { return mIsActive; } + public boolean isSystemOwned() { + return SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName()); + } + + @AutomaticZenRule.Type + public int getType() { + return mRule.getType(); + } + @Override public boolean equals(@Nullable Object obj) { return obj instanceof ZenMode other diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index 6bda5e13c97..ee497ae74df 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -52,7 +52,7 @@ public class ZenModeFragment extends ZenModeFragmentBase { prefControllers.add(new ZenModeDisplayLinkPreferenceController( context, "mode_display_settings", mBackend, mHelperBackend)); prefControllers.add(new ZenModeSetTriggerLinkPreferenceController(context, - "zen_automatic_trigger_category", mBackend)); + "zen_automatic_trigger_category", this, mBackend)); return prefControllers; } diff --git a/src/com/android/settings/notification/modes/ZenModeScheduleChooserDialog.java b/src/com/android/settings/notification/modes/ZenModeScheduleChooserDialog.java new file mode 100644 index 00000000000..14264b7a844 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeScheduleChooserDialog.java @@ -0,0 +1,149 @@ +/* + * 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.notification.modes; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import android.app.Dialog; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.service.notification.ZenModeConfig; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.dashboard.DashboardFragment; + +import com.google.common.collect.ImmutableList; + +public class ZenModeScheduleChooserDialog extends InstrumentedDialogFragment { + + private static final String TAG = "ZenModeScheduleChooserDialog"; + + static final int OPTION_TIME = 0; + static final int OPTION_CALENDAR = 1; + + private record ScheduleOption(@StringRes int nameResId, @StringRes int exampleResId, + @DrawableRes int iconResId) {} + + private static final ImmutableList SCHEDULE_OPTIONS = ImmutableList.of( + new ScheduleOption(R.string.zen_mode_select_schedule_time, + R.string.zen_mode_select_schedule_time_example, + com.android.internal.R.drawable.ic_zen_mode_type_schedule_time), + new ScheduleOption(R.string.zen_mode_select_schedule_calendar, + R.string.zen_mode_select_schedule_calendar_example, + com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar)); + + private OnScheduleOptionListener mOptionListener; + + interface OnScheduleOptionListener { + void onScheduleSelected(Uri conditionId); + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - Update metrics category + return 0; + } + + static void show(DashboardFragment parent, OnScheduleOptionListener optionListener) { + ZenModeScheduleChooserDialog dialog = new ZenModeScheduleChooserDialog(); + dialog.mOptionListener = optionListener; + dialog.setTargetFragment(parent, 0); + dialog.show(parent.getParentFragmentManager(), TAG); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + checkState(getContext() != null); + return new AlertDialog.Builder(getContext()) + .setTitle(R.string.zen_mode_choose_rule_type) + .setAdapter(new OptionsAdapter(getContext()), + (dialog, which) -> onScheduleTypeSelected(which)) + .setNegativeButton(R.string.cancel, null) + .create(); + } + + private static class OptionsAdapter extends ArrayAdapter { + + private final LayoutInflater mInflater; + + OptionsAdapter(@NonNull Context context) { + super(context, R.layout.zen_mode_type_item, SCHEDULE_OPTIONS); + mInflater = LayoutInflater.from(context); + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + if (convertView == null) { + convertView = mInflater.inflate(R.layout.zen_mode_type_item, parent, false); + } + // No need for holder pattern since we have only 2 items. + ImageView imageView = checkNotNull(convertView.findViewById(R.id.icon)); + TextView title = checkNotNull(convertView.findViewById(R.id.title)); + TextView subtitle = checkNotNull(convertView.findViewById(R.id.subtitle)); + + ScheduleOption option = checkNotNull(getItem(position)); + imageView.setImageResource(option.iconResId()); + title.setText(option.nameResId()); + subtitle.setText(option.exampleResId()); + + return convertView; + } + } + + private void onScheduleTypeSelected(int whichOption) { + Uri conditionId = switch (whichOption) { + case OPTION_TIME -> getDefaultScheduleTimeCondition(); + case OPTION_CALENDAR -> getDefaultScheduleCalendarCondition(); + default -> ZenModeConfig.toCustomManualConditionId(); + }; + + mOptionListener.onScheduleSelected(conditionId); + } + + private static Uri getDefaultScheduleTimeCondition() { + ZenModeConfig.ScheduleInfo schedule = new ZenModeConfig.ScheduleInfo(); + schedule.days = ZenModeConfig.ALL_DAYS; + schedule.startHour = 9; + schedule.startMinute = 30; + schedule.endHour = 17; + return ZenModeConfig.toScheduleConditionId(schedule); + } + + private static Uri getDefaultScheduleCalendarCondition() { + ZenModeConfig.EventInfo eventInfo = new ZenModeConfig.EventInfo(); + eventInfo.calendarId = null; // All calendars of the current user. + eventInfo.reply = ZenModeConfig.EventInfo.REPLY_ANY_EXCEPT_NO; + return ZenModeConfig.toEventConditionId(eventInfo); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceController.java index 28413091a37..e87907647db 100644 --- a/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceController.java @@ -16,14 +16,12 @@ package com.android.settings.notification.modes; -import android.app.Flags; import android.content.Context; import android.content.pm.PackageManager; import android.database.Cursor; import android.os.UserHandle; import android.os.UserManager; import android.provider.CalendarContract; -import android.service.notification.SystemZenRules; import android.service.notification.ZenModeConfig; import androidx.annotation.NonNull; @@ -42,7 +40,7 @@ import java.util.List; import java.util.Objects; import java.util.function.Function; -public class ZenModeSetCalendarPreferenceController extends AbstractZenModePreferenceController { +class ZenModeSetCalendarPreferenceController extends AbstractZenModePreferenceController { @VisibleForTesting protected static final String KEY_CALENDAR = "calendar"; @VisibleForTesting @@ -122,11 +120,7 @@ public class ZenModeSetCalendarPreferenceController extends AbstractZenModePrefe @VisibleForTesting protected Function updateEventMode(ZenModeConfig.EventInfo event) { return (zenMode) -> { - zenMode.getRule().setConditionId(ZenModeConfig.toEventConditionId(event)); - if (Flags.modesApi() && Flags.modesUi()) { - zenMode.getRule().setTriggerDescription( - SystemZenRules.getTriggerDescriptionForScheduleEvent(mContext, event)); - } + zenMode.setCustomModeConditionId(mContext, ZenModeConfig.toEventConditionId(event)); return zenMode; }; } diff --git a/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java index a6008ccd768..3432ed5154f 100644 --- a/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java @@ -16,9 +16,7 @@ package com.android.settings.notification.modes; -import android.app.Flags; import android.content.Context; -import android.service.notification.SystemZenRules; import android.service.notification.ZenModeConfig; import android.text.format.DateFormat; import android.util.ArraySet; @@ -116,16 +114,13 @@ class ZenModeSetSchedulePreferenceController extends AbstractZenModePreferenceCo @VisibleForTesting protected Function updateScheduleMode(ZenModeConfig.ScheduleInfo schedule) { return (zenMode) -> { - zenMode.getRule().setConditionId(ZenModeConfig.toScheduleConditionId(schedule)); - if (Flags.modesApi() && Flags.modesUi()) { - zenMode.getRule().setTriggerDescription( - SystemZenRules.getTriggerDescriptionForScheduleTime(mContext, schedule)); - } + zenMode.setCustomModeConditionId(mContext, + ZenModeConfig.toScheduleConditionId(schedule)); return zenMode; }; } - private ZenModeTimePickerFragment.TimeSetter mStartSetter = (hour, minute) -> { + private final ZenModeTimePickerFragment.TimeSetter mStartSetter = (hour, minute) -> { if (!isValidTime(hour, minute)) { return; } @@ -137,7 +132,7 @@ class ZenModeSetSchedulePreferenceController extends AbstractZenModePreferenceCo saveMode(updateScheduleMode(mSchedule)); }; - private ZenModeTimePickerFragment.TimeSetter mEndSetter = (hour, minute) -> { + private final ZenModeTimePickerFragment.TimeSetter mEndSetter = (hour, minute) -> { if (!isValidTime(hour, minute)) { return; } diff --git a/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java index 14d5d59a19d..fd27958db95 100644 --- a/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java @@ -13,15 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.android.settings.notification.modes; import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR; import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; -import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; - import android.content.Context; -import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -29,7 +27,7 @@ import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import com.android.settings.R; -import com.android.settings.core.SubSettingLauncher; +import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.PrimarySwitchPreference; /** @@ -39,9 +37,13 @@ class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenc @VisibleForTesting protected static final String AUTOMATIC_TRIGGER_PREF_KEY = "zen_automatic_trigger_settings"; + private final DashboardFragment mFragment; + ZenModeSetTriggerLinkPreferenceController(Context context, String key, + DashboardFragment fragment, ZenModesBackend backend) { super(context, key, backend); + mFragment = fragment; } @Override @@ -54,46 +56,52 @@ class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenc // This controller is expected to govern a preference category so that it controls the // availability of the entire preference category if the mode doesn't have a way to // automatically trigger (such as manual DND). - Preference switchPref = ((PreferenceCategory) preference).findPreference( + PrimarySwitchPreference switchPref = ((PreferenceCategory) preference).findPreference( AUTOMATIC_TRIGGER_PREF_KEY); if (switchPref == null) { return; } - ((PrimarySwitchPreference) switchPref).setChecked(zenMode.getRule().isEnabled()); + switchPref.setChecked(zenMode.getRule().isEnabled()); switchPref.setOnPreferenceChangeListener(mSwitchChangeListener); - Bundle bundle = new Bundle(); - bundle.putString(MODE_ID, zenMode.getId()); + switchPref.setSummary(zenMode.getRule().getTriggerDescription()); + switchPref.setIcon(null); + switchPref.setOnPreferenceClickListener(null); + switchPref.setIntent(null); - // TODO: b/341961712 - direct preference to app-owned intent if available - switch (zenMode.getRule().getType()) { - case TYPE_SCHEDULE_TIME: - switchPref.setTitle(R.string.zen_mode_set_schedule_link); - switchPref.setSummary(zenMode.getRule().getTriggerDescription()); - switchPref.setIntent(new SubSettingLauncher(mContext) - .setDestination(ZenModeSetScheduleFragment.class.getName()) - // TODO: b/332937635 - set correct metrics category - .setSourceMetricsCategory(0) - .setArguments(bundle) - .toIntent()); - break; - case TYPE_SCHEDULE_CALENDAR: - switchPref.setTitle(R.string.zen_mode_set_calendar_link); - switchPref.setSummary(zenMode.getRule().getTriggerDescription()); - switchPref.setIntent(new SubSettingLauncher(mContext) - .setDestination(ZenModeSetCalendarFragment.class.getName()) - // TODO: b/332937635 - set correct metrics category - .setSourceMetricsCategory(0) - .setArguments(bundle) - .toIntent()); - break; - default: - // TODO: b/342156843 - change this to allow adding a trigger condition for system - // rules that don't yet have a type selected - switchPref.setTitle("not implemented"); + if (zenMode.isSystemOwned() && zenMode.getType() == TYPE_SCHEDULE_TIME) { + switchPref.setTitle(R.string.zen_mode_set_schedule_link); + // TODO: b/332937635 - set correct metrics category + switchPref.setIntent(ZenSubSettingLauncher.forModeFragment(mContext, + ZenModeSetScheduleFragment.class, zenMode.getId(), 0).toIntent()); + } else if (zenMode.isSystemOwned() && zenMode.getType() == TYPE_SCHEDULE_CALENDAR) { + switchPref.setTitle(R.string.zen_mode_set_calendar_link); + switchPref.setIcon(null); + // TODO: b/332937635 - set correct metrics category + switchPref.setIntent(ZenSubSettingLauncher.forModeFragment(mContext, + ZenModeSetCalendarFragment.class, zenMode.getId(), 0).toIntent()); + } else if (zenMode.isSystemOwned()) { + switchPref.setTitle(R.string.zen_mode_select_schedule); + switchPref.setIcon(R.drawable.ic_add_24dp); + switchPref.setSummary(""); + // TODO: b/342156843 - Hide the switch (needs support in SettingsLib). + switchPref.setOnPreferenceClickListener(clickedPreference -> { + ZenModeScheduleChooserDialog.show(mFragment, mOnScheduleOptionListener); + return true; + }); + } else { + // TODO: b/341961712 - direct preference to app-owned intent if available + switchPref.setTitle("not implemented"); } } + @VisibleForTesting + final ZenModeScheduleChooserDialog.OnScheduleOptionListener mOnScheduleOptionListener = + conditionId -> saveMode(mode -> { + mode.setCustomModeConditionId(mContext, conditionId); + return mode; + }); + @VisibleForTesting protected Preference.OnPreferenceChangeListener mSwitchChangeListener = (p, newValue) -> { final boolean newEnabled = (Boolean) newValue; @@ -103,5 +111,6 @@ class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenc } return zenMode; }); + // TODO: b/342156843 - Do we want to jump to the corresponding schedule editing screen? }; } diff --git a/src/com/android/settings/notification/modes/ZenModesBackend.java b/src/com/android/settings/notification/modes/ZenModesBackend.java index b58e3107934..4f86778cf63 100644 --- a/src/com/android/settings/notification/modes/ZenModesBackend.java +++ b/src/com/android/settings/notification/modes/ZenModesBackend.java @@ -24,7 +24,6 @@ import android.content.Context; import android.net.Uri; import android.provider.Settings; import android.service.notification.Condition; -import android.service.notification.SystemZenRules; import android.service.notification.ZenModeConfig; import android.util.Log; @@ -194,19 +193,11 @@ class ZenModesBackend { */ @Nullable ZenMode addCustomMode(String name) { - ZenModeConfig.ScheduleInfo schedule = new ZenModeConfig.ScheduleInfo(); - schedule.days = ZenModeConfig.ALL_DAYS; - schedule.startHour = 22; - schedule.endHour = 7; - - // TODO: b/326442408 - Create as "manual" (i.e. no trigger) instead of schedule-time. AutomaticZenRule rule = new AutomaticZenRule.Builder(name, - ZenModeConfig.toScheduleConditionId(schedule)) - .setPackage(ZenModeConfig.getScheduleConditionProvider().getPackageName()) - .setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR) - .setOwner(ZenModeConfig.getScheduleConditionProvider()) - .setTriggerDescription(SystemZenRules.getTriggerDescriptionForScheduleTime( - mContext, schedule)) + ZenModeConfig.toCustomManualConditionId()) + .setPackage(ZenModeConfig.getCustomManualConditionProvider().getPackageName()) + .setType(AutomaticZenRule.TYPE_OTHER) + .setOwner(ZenModeConfig.getCustomManualConditionProvider()) .setManualInvocationAllowed(true) .build(); diff --git a/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java b/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java index 11f3492f36d..aa66e6c5186 100644 --- a/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java +++ b/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java @@ -29,7 +29,7 @@ class ZenSubSettingLauncher { SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION); } - private static SubSettingLauncher forModeFragment(Context context, + static SubSettingLauncher forModeFragment(Context context, Class fragmentClass, String modeId, int sourceMetricsCategory) { Bundle bundle = new Bundle(); diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceControllerTest.java index 6b24fa21832..0ede058aed7 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceControllerTest.java @@ -34,6 +34,7 @@ import android.content.Context; import android.net.Uri; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.SystemZenRules; import android.service.notification.ZenModeConfig; import androidx.preference.DropDownPreference; @@ -85,7 +86,9 @@ public class ZenModeSetCalendarPreferenceControllerTest { @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) public void updateEventMode_updatesConditionAndTriggerDescription() { ZenMode mode = new ZenMode("id", - new AutomaticZenRule.Builder("name", Uri.parse("condition")).build(), + new AutomaticZenRule.Builder("name", Uri.parse("condition")) + .setPackage(SystemZenRules.PACKAGE_ANDROID) + .build(), true); // is active // Explicitly update preference controller with mode info first, which will also call @@ -99,6 +102,7 @@ public class ZenModeSetCalendarPreferenceControllerTest { // apply event mode updater to existing mode ZenMode out = mPrefController.updateEventMode(eventInfo).apply(mode); + assertThat(out.getRule().getOwner()).isEqualTo(ZenModeConfig.getEventConditionProvider()); assertThat(out.getRule().getConditionId()).isEqualTo( ZenModeConfig.toEventConditionId(eventInfo)); assertThat(out.getRule().getTriggerDescription()).isEqualTo("My events"); diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java index 7cf327c983e..5f492b971a8 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java @@ -29,6 +29,7 @@ import android.content.Context; import android.net.Uri; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.SystemZenRules; import android.service.notification.ZenModeConfig; import android.view.ViewGroup; import android.widget.ToggleButton; @@ -81,7 +82,9 @@ public class ZenModeSetSchedulePreferenceControllerTest { @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) public void updateScheduleRule_updatesConditionAndTriggerDescription() { ZenMode mode = new ZenMode("id", - new AutomaticZenRule.Builder("name", Uri.parse("condition")).build(), + new AutomaticZenRule.Builder("name", Uri.parse("condition")) + .setPackage(SystemZenRules.PACKAGE_ANDROID) + .build(), true); // is active ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo(); @@ -93,6 +96,8 @@ public class ZenModeSetSchedulePreferenceControllerTest { assertThat(out.getRule().getConditionId()) .isEqualTo(ZenModeConfig.toScheduleConditionId(scheduleInfo)); assertThat(out.getRule().getTriggerDescription()).isNotEmpty(); + assertThat(out.getRule().getOwner()).isEqualTo( + ZenModeConfig.getScheduleConditionProvider()); } @Test diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java index 91de4ea8348..ff4d4a3c94c 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java @@ -16,6 +16,7 @@ package com.android.settings.notification.modes; +import static android.app.AutomaticZenRule.TYPE_OTHER; import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR; import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; @@ -31,10 +32,10 @@ import static org.mockito.Mockito.when; import android.app.AutomaticZenRule; import android.app.Flags; import android.content.Context; -import android.content.Intent; import android.net.Uri; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.SystemZenRules; import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; @@ -43,6 +44,7 @@ import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; import com.android.settings.SettingsActivity; +import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.PrimarySwitchPreference; import org.junit.Before; @@ -57,6 +59,7 @@ import org.robolectric.RobolectricTestRunner; import java.util.Calendar; @RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) public class ZenModeSetTriggerLinkPreferenceControllerTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); @@ -65,10 +68,13 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest { private ZenModesBackend mBackend; private Context mContext; + private PrimarySwitchPreference mPreference; + @Mock private PreferenceCategory mPrefCategory; @Mock - private PrimarySwitchPreference mPreference; + private DashboardFragment mFragment; + private ZenModeSetTriggerLinkPreferenceController mPrefController; @Before @@ -77,12 +83,12 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest { mContext = ApplicationProvider.getApplicationContext(); mPrefController = new ZenModeSetTriggerLinkPreferenceController(mContext, - "zen_automatic_trigger_category", mBackend); + "zen_automatic_trigger_category", mFragment, mBackend); + mPreference = new PrimarySwitchPreference(mContext); when(mPrefCategory.findPreference(AUTOMATIC_TRIGGER_PREF_KEY)).thenReturn(mPreference); } @Test - @EnableFlags(Flags.FLAG_MODES_UI) public void testIsAvailable() { // should not be available for manual DND ZenMode manualMode = ZenMode.manualDndMode(new AutomaticZenRule.Builder("Do Not Disturb", @@ -117,12 +123,12 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest { // Update preference controller with a zen mode that is not enabled mPrefController.updateZenMode(mPrefCategory, zenMode); - verify(mPreference).setChecked(false); + assertThat(mPreference.getCheckedState()).isFalse(); // Now with the rule enabled zenMode.getRule().setEnabled(true); mPrefController.updateZenMode(mPrefCategory, zenMode); - verify(mPreference).setChecked(true); + assertThat(mPreference.getCheckedState()).isTrue(); } @Test @@ -154,21 +160,24 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest { eventInfo.calName = "My events"; ZenMode mode = new ZenMode("id", new AutomaticZenRule.Builder("name", ZenModeConfig.toEventConditionId(eventInfo)) + .setPackage(SystemZenRules.PACKAGE_ANDROID) .setType(TYPE_SCHEDULE_CALENDAR) .setTriggerDescription("My events") .build(), true); // is active mPrefController.updateZenMode(mPrefCategory, mode); - verify(mPreference).setTitle(R.string.zen_mode_set_calendar_link); - verify(mPreference).setSummary(mode.getRule().getTriggerDescription()); + assertThat(mPreference.getTitle()).isNotNull(); + assertThat(mPreference.getTitle().toString()).isEqualTo( + mContext.getString(R.string.zen_mode_set_calendar_link)); + assertThat(mPreference.getSummary()).isNotNull(); + assertThat(mPreference.getSummary().toString()).isEqualTo( + mode.getRule().getTriggerDescription()); + assertThat(mPreference.getIcon()).isNull(); - ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); - verify(mPreference).setIntent(captor.capture()); // Destination as written into the intent by SubSettingLauncher - assertThat( - captor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)).isEqualTo( - ZenModeSetCalendarFragment.class.getName()); + assertThat(mPreference.getIntent().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(ZenModeSetCalendarFragment.class.getName()); } @Test @@ -179,20 +188,75 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest { scheduleInfo.endHour = 15; ZenMode mode = new ZenMode("id", new AutomaticZenRule.Builder("name", ZenModeConfig.toScheduleConditionId(scheduleInfo)) + .setPackage(SystemZenRules.PACKAGE_ANDROID) .setType(TYPE_SCHEDULE_TIME) .setTriggerDescription("some schedule") .build(), true); // is active mPrefController.updateZenMode(mPrefCategory, mode); - verify(mPreference).setTitle(R.string.zen_mode_set_schedule_link); - verify(mPreference).setSummary(mode.getRule().getTriggerDescription()); + assertThat(mPreference.getTitle()).isNotNull(); + assertThat(mPreference.getTitle().toString()).isEqualTo( + mContext.getString(R.string.zen_mode_set_schedule_link)); + assertThat(mPreference.getSummary()).isNotNull(); + assertThat(mPreference.getSummary().toString()).isEqualTo( + mode.getRule().getTriggerDescription()); + assertThat(mPreference.getIcon()).isNull(); - ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); - verify(mPreference).setIntent(captor.capture()); // Destination as written into the intent by SubSettingLauncher - assertThat( - captor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)).isEqualTo( - ZenModeSetScheduleFragment.class.getName()); + assertThat(mPreference.getIntent().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(ZenModeSetScheduleFragment.class.getName()); + } + + @Test + public void testRuleLink_manual() { + ZenMode mode = new ZenMode("id", new AutomaticZenRule.Builder("name", + ZenModeConfig.toCustomManualConditionId()) + .setPackage(SystemZenRules.PACKAGE_ANDROID) + .setType(TYPE_OTHER) + .setTriggerDescription("Will not be shown") + .build(), + true); // is active + mPrefController.updateZenMode(mPrefCategory, mode); + + assertThat(mPreference.getTitle()).isNotNull(); + assertThat(mPreference.getTitle().toString()).isEqualTo( + mContext.getString(R.string.zen_mode_select_schedule)); + assertThat(mPreference.getIcon()).isNotNull(); + assertThat(mPreference.getSummary()).isNotNull(); + assertThat(mPreference.getSummary().toString()).isEqualTo(""); + + // Set up a click listener to open the dialog. + assertThat(mPreference.getOnPreferenceClickListener()).isNotNull(); + } + + @Test + public void onScheduleChosen_updatesMode() { + ZenMode originalMode = new ZenMode("id", + new AutomaticZenRule.Builder("name", ZenModeConfig.toCustomManualConditionId()) + .setPackage(SystemZenRules.PACKAGE_ANDROID) + .setType(TYPE_OTHER) + .setTriggerDescription("") + .build(), + false); + mPrefController.updateZenMode(mPrefCategory, originalMode); + + ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo(); + scheduleInfo.days = new int[] { Calendar.MONDAY }; + scheduleInfo.startHour = 12; + scheduleInfo.endHour = 15; + Uri scheduleUri = ZenModeConfig.toScheduleConditionId(scheduleInfo); + + mPrefController.mOnScheduleOptionListener.onScheduleSelected(scheduleUri); + + // verify the backend got asked to update the mode to be schedule-based. + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + ZenMode updatedMode = captor.getValue(); + assertThat(updatedMode.getType()).isEqualTo(TYPE_SCHEDULE_TIME); + assertThat(updatedMode.getRule().getConditionId()).isEqualTo(scheduleUri); + assertThat(updatedMode.getRule().getTriggerDescription()).isNotEmpty(); + assertThat(updatedMode.getRule().getOwner()).isEqualTo( + ZenModeConfig.getScheduleConditionProvider()); } } From 5ac9d9c8fa0005f787b75052010dcc9935efcb4f Mon Sep 17 00:00:00 2001 From: Rambo Wang Date: Wed, 19 Jun 2024 02:09:41 +0000 Subject: [PATCH 7/7] Fix Settings restart during Reset mobile nework settings flow This CL avoids restarting Settings in the reset mobile flow when phone process is restarted, by switching the usage of the stable content provider connection to the unstable client. The CL also arranges restarting phone process as the last reset operation in the flow (later than RILD reset) to avoid any reset operation get impacted by phone process restarting. Since the permission to protect the TelephonyContentProvider has been renamed, the CL also renames the requsted permision. Bug: 347047105 Test: atest ResetNetworkOperationBuilderTest Test: Reset mobile network feature test Flag: EXEMPT resource update with minor refactoring Change-Id: I7bfa79bc9d7451a4a03269704b0009a3730e287f --- AndroidManifest.xml | 2 +- .../android/settings/ResetNetworkRequest.java | 6 +-- .../network/ResetNetworkOperationBuilder.java | 51 +++++++++++-------- .../ResetNetworkOperationBuilderTest.java | 35 ++++++------- 4 files changed, 50 insertions(+), 44 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 90b42f41cbe..ca963287c18 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -140,7 +140,7 @@ - + diff --git a/src/com/android/settings/ResetNetworkRequest.java b/src/com/android/settings/ResetNetworkRequest.java index 7632ea01d71..8df67e771b2 100644 --- a/src/com/android/settings/ResetNetworkRequest.java +++ b/src/com/android/settings/ResetNetworkRequest.java @@ -271,12 +271,12 @@ public class ResetNetworkRequest { builder.resetIms(mSubscriptionIdToResetIms); } // Reset phone process and RILD may impact above components, keep them at the end - if ((mResetOptions & RESET_PHONE_PROCESS) != 0) { - builder.restartPhoneProcess(); - } if ((mResetOptions & RESET_RILD) != 0) { builder.restartRild(); } + if ((mResetOptions & RESET_PHONE_PROCESS) != 0) { + builder.restartPhoneProcess(); + } return builder; } } diff --git a/src/com/android/settings/network/ResetNetworkOperationBuilder.java b/src/com/android/settings/network/ResetNetworkOperationBuilder.java index 6f36074d145..47c06d4480d 100644 --- a/src/com/android/settings/network/ResetNetworkOperationBuilder.java +++ b/src/com/android/settings/network/ResetNetworkOperationBuilder.java @@ -18,6 +18,7 @@ package com.android.settings.network; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; +import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.net.ConnectivityManager; @@ -28,11 +29,14 @@ import android.net.wifi.WifiManager; import android.net.wifi.p2p.WifiP2pManager; import android.os.Looper; import android.os.RecoverySystem; +import android.os.RemoteException; import android.os.SystemClock; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.util.Log; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; import com.android.settings.ResetNetworkRequest; @@ -257,15 +261,15 @@ public class ResetNetworkOperationBuilder { */ public ResetNetworkOperationBuilder restartPhoneProcess() { Runnable runnable = () -> { - try { - mContext.getContentResolver().call( - getResetTelephonyContentProviderAuthority(), - METHOD_RESTART_PHONE_PROCESS, - /* arg= */ null, - /* extras= */ null); - Log.i(TAG, "Phone process was restarted."); - } catch (IllegalArgumentException iae) { - Log.w(TAG, "Fail to restart phone process: " + iae); + // Unstable content provider can avoid us getting killed together with phone process + try (ContentProviderClient client = getUnstableTelephonyContentProviderClient()) { + if (client != null) { + client.call(METHOD_RESTART_PHONE_PROCESS, /* arg= */ null, /* extra= */ null); + Log.i(TAG, "Phone process was restarted."); + } + } catch (RemoteException re) { + // It's normal to throw RE since phone process immediately dies + Log.i(TAG, "Phone process has been restarted: " + re); } }; mResetSequence.add(runnable); @@ -279,15 +283,13 @@ public class ResetNetworkOperationBuilder { */ public ResetNetworkOperationBuilder restartRild() { Runnable runnable = () -> { - try { - mContext.getContentResolver().call( - getResetTelephonyContentProviderAuthority(), - METHOD_RESTART_RILD, - /* arg= */ null, - /* extras= */ null); - Log.i(TAG, "RILD was restarted."); - } catch (IllegalArgumentException iae) { - Log.w(TAG, "Fail to restart RILD: " + iae); + try (ContentProviderClient client = getUnstableTelephonyContentProviderClient()) { + if (client != null) { + client.call(METHOD_RESTART_RILD, /* arg= */ null, /* extra= */ null); + Log.i(TAG, "RILD was restarted."); + } + } catch (RemoteException re) { + Log.w(TAG, "Fail to restart RILD: " + re); } }; mResetSequence.add(runnable); @@ -322,9 +324,18 @@ public class ResetNetworkOperationBuilder { * @return the authority of the telephony content provider that support methods * resetPhoneProcess and resetRild. */ - @VisibleForTesting - String getResetTelephonyContentProviderAuthority() { + private String getResetTelephonyContentProviderAuthority() { return mContext.getResources().getString( R.string.reset_telephony_stack_content_provider_authority); } + + /** + * @return the unstable content provider to avoid us getting killed with phone process + */ + @Nullable + @VisibleForTesting + public ContentProviderClient getUnstableTelephonyContentProviderClient() { + return mContext.getContentResolver().acquireUnstableContentProviderClient( + getResetTelephonyContentProviderAuthority()); + } } diff --git a/tests/unit/src/com/android/settings/network/ResetNetworkOperationBuilderTest.java b/tests/unit/src/com/android/settings/network/ResetNetworkOperationBuilderTest.java index 5f544064924..7f1c475dbfd 100644 --- a/tests/unit/src/com/android/settings/network/ResetNetworkOperationBuilderTest.java +++ b/tests/unit/src/com/android/settings/network/ResetNetworkOperationBuilderTest.java @@ -16,20 +16,16 @@ package com.android.settings.network; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import android.content.ContentProvider; -import android.content.ContentResolver; +import android.content.ContentProviderClient; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkPolicyManager; @@ -67,7 +63,7 @@ public class ResetNetworkOperationBuilderTest { @Mock private NetworkPolicyManager mNetworkPolicyManager; @Mock - private ContentProvider mContentProvider;; + private ContentProviderClient mContentProviderClient; private Context mContext; @@ -77,9 +73,8 @@ public class ResetNetworkOperationBuilderTest { public void setUp() { MockitoAnnotations.initMocks(this); mContext = spy(ApplicationProvider.getApplicationContext()); - doReturn(ContentResolver.wrap(mContentProvider)).when(mContext).getContentResolver(); - mBuilder = spy(new ResetNetworkOperationBuilder(mContext)); + doReturn(mContentProviderClient).when(mBuilder).getUnstableTelephonyContentProviderClient(); } @Test @@ -184,38 +179,38 @@ public class ResetNetworkOperationBuilderTest { } @Test - public void restartPhoneProcess_withoutTelephonyContentProvider_shouldNotCrash() { - doThrow(new IllegalArgumentException()).when(mContentProvider).call( - anyString(), anyString(), anyString(), any()); + public void restartPhoneProcess_withoutTelephonyContentProvider_shouldNotCrash() + throws Exception { + doReturn(null).when(mBuilder).getUnstableTelephonyContentProviderClient(); mBuilder.restartPhoneProcess().build().run(); } @Test - public void restartRild_withoutTelephonyContentProvider_shouldNotCrash() { - doThrow(new IllegalArgumentException()).when(mContentProvider).call( - anyString(), anyString(), anyString(), any()); + public void restartRild_withoutTelephonyContentProvider_shouldNotCrash() + throws Exception { + doReturn(null).when(mBuilder).getUnstableTelephonyContentProviderClient(); mBuilder.restartRild().build().run(); } @Test - public void restartPhoneProcess_withTelephonyContentProvider_shouldCallRestartPhoneProcess() { + public void restartPhoneProcess_withTelephonyContentProvider_shouldCallRestartPhoneProcess() + throws Exception { mBuilder.restartPhoneProcess().build().run(); - verify(mContentProvider).call( - eq(mBuilder.getResetTelephonyContentProviderAuthority()), + verify(mContentProviderClient).call( eq(ResetNetworkOperationBuilder.METHOD_RESTART_PHONE_PROCESS), isNull(), isNull()); } @Test - public void restartRild_withTelephonyContentProvider_shouldCallRestartRild() { + public void restartRild_withTelephonyContentProvider_shouldCallRestartRild() + throws Exception { mBuilder.restartRild().build().run(); - verify(mContentProvider).call( - eq(mBuilder.getResetTelephonyContentProviderAuthority()), + verify(mContentProviderClient).call( eq(ResetNetworkOperationBuilder.METHOD_RESTART_RILD), isNull(), isNull());