From f337bf1fad459dfe708ef2cb81a8b9a3011c63f2 Mon Sep 17 00:00:00 2001 From: Riley Jones Date: Mon, 9 Dec 2024 19:02:26 +0000 Subject: [PATCH 1/6] Cleaning up deprecated QS tooltip code Flag is rolled out, so tooltips other than font size shouldn't show anymore. Much code can be removed. Flag: EXEMPT code cleanup Test: atest com.android.settings.accessibility Bug: 367414968 Change-Id: I55084982fee0d0537aade49d36f52e121e24574b --- .../AccessibilityHearingAidsFragment.java | 11 --- ...cessibilityShortcutPreferenceFragment.java | 69 ------------------ ...cessibilityActivityPreferenceFragment.java | 5 -- ...oggleColorInversionPreferenceFragment.java | 4 -- .../ToggleDaltonizerPreferenceFragment.java | 4 -- .../ToggleFeaturePreferenceFragment.java | 72 ++----------------- ...eReduceBrightColorsPreferenceFragment.java | 3 - .../settings/gestures/OneHandedSettings.java | 28 +------- ...ibilityShortcutPreferenceFragmentTest.java | 28 -------- .../ToggleFeaturePreferenceFragmentTest.java | 8 --- .../gestures/OneHandedSettingsTest.java | 18 ----- 11 files changed, 8 insertions(+), 242 deletions(-) diff --git a/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java b/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java index 09e2d97e48b..3bda864a105 100644 --- a/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java +++ b/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java @@ -96,17 +96,6 @@ public class AccessibilityHearingAidsFragment extends AccessibilityShortcutPrefe return mFeatureName; } - @Override - protected ComponentName getTileComponentName() { - return AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_TILE_COMPONENT_NAME; - } - - @Override - protected CharSequence getTileTooltipContent(int type) { - // No tooltip to be shown - return null; - } - @Override protected boolean showGeneralCategory() { // Have static preference under dynamically created PreferenceCategory KEY_GENERAL_CATEGORY. diff --git a/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java b/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java index 23c3fd4ebb6..2a22902a196 100644 --- a/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java +++ b/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java @@ -20,10 +20,8 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh import static com.android.settings.accessibility.AccessibilityDialogUtils.DialogEnums; import static com.android.settings.accessibility.AccessibilityUtil.getShortcutSummaryList; import static com.android.settings.accessibility.ToggleFeaturePreferenceFragment.KEY_GENERAL_CATEGORY; -import static com.android.settings.accessibility.ToggleFeaturePreferenceFragment.KEY_SAVED_QS_TOOLTIP_TYPE; import android.annotation.SuppressLint; -import android.app.Activity; import android.app.Dialog; import android.app.settings.SettingsEnums; import android.content.ComponentName; @@ -44,7 +42,6 @@ import androidx.preference.PreferenceScreen; import com.android.internal.accessibility.common.ShortcutConstants; import com.android.settings.R; -import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType; import com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment; import com.android.settings.dashboard.RestrictedDashboardFragment; @@ -60,16 +57,12 @@ import java.util.Set; public abstract class AccessibilityShortcutPreferenceFragment extends RestrictedDashboardFragment implements ShortcutPreference.OnClickCallback { private static final String KEY_SHORTCUT_PREFERENCE = "shortcut_preference"; - protected static final String KEY_SAVED_QS_TOOLTIP_RESHOW = "qs_tooltip_reshow"; protected ShortcutPreference mShortcutPreference; protected Dialog mDialog; private AccessibilityManager.TouchExplorationStateChangeListener mTouchExplorationStateChangeListener; private AccessibilitySettingsContentObserver mSettingsContentObserver; - private AccessibilityQuickSettingsTooltipWindow mTooltipWindow; - private boolean mNeedsQSTooltipReshow = false; - private int mNeedsQSTooltipType = QuickSettingsTooltipType.GUIDE_TO_EDIT; public AccessibilityShortcutPreferenceFragment(String restrictionKey) { super(restrictionKey); @@ -81,26 +74,10 @@ public abstract class AccessibilityShortcutPreferenceFragment extends Restricted /** Returns the accessibility feature name. */ protected abstract CharSequence getLabelName(); - /** Returns the accessibility tile component name. */ - protected abstract ComponentName getTileComponentName(); - - /** Returns the accessibility tile tooltip content. */ - protected abstract CharSequence getTileTooltipContent(@QuickSettingsTooltipType int type); - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Restore the user shortcut type and tooltip. - if (savedInstanceState != null) { - if (savedInstanceState.containsKey(KEY_SAVED_QS_TOOLTIP_RESHOW)) { - mNeedsQSTooltipReshow = savedInstanceState.getBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW); - } - if (savedInstanceState.containsKey(KEY_SAVED_QS_TOOLTIP_TYPE)) { - mNeedsQSTooltipType = savedInstanceState.getInt(KEY_SAVED_QS_TOOLTIP_TYPE); - } - } - final int resId = getPreferenceScreenResId(); if (resId <= 0) { final PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen( @@ -141,21 +118,6 @@ public abstract class AccessibilityShortcutPreferenceFragment extends Restricted return super.onCreateView(inflater, container, savedInstanceState); } - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - // Reshow tooltip when activity recreate, such as rotate device. - if (mNeedsQSTooltipReshow) { - view.post(() -> { - final Activity activity = getActivity(); - if (activity != null && !activity.isFinishing()) { - showQuickSettingsTooltipIfNeeded(); - } - }); - } - } - @Override public void onResume() { super.onResume(); @@ -177,16 +139,6 @@ public abstract class AccessibilityShortcutPreferenceFragment extends Restricted super.onPause(); } - @Override - public void onSaveInstanceState(Bundle outState) { - final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing(); - if (mNeedsQSTooltipReshow || isTooltipWindowShowing) { - outState.putBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW, /* value= */ true); - outState.putInt(KEY_SAVED_QS_TOOLTIP_TYPE, mNeedsQSTooltipType); - } - super.onSaveInstanceState(outState); - } - @Override public Dialog onCreateDialog(int dialogId) { switch (dialogId) { @@ -289,7 +241,6 @@ public abstract class AccessibilityShortcutPreferenceFragment extends Restricted */ private void callOnTutorialDialogButtonClicked(DialogInterface dialog, int which) { dialog.dismiss(); - showQuickSettingsTooltipIfNeeded(); } @VisibleForTesting @@ -362,26 +313,6 @@ public abstract class AccessibilityShortcutPreferenceFragment extends Restricted mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); } - /** - * Shows the quick settings tooltip if the quick settings feature is assigned. The tooltip only - * shows once. - * - * @param type The quick settings tooltip type - */ - protected void showQuickSettingsTooltipIfNeeded(@QuickSettingsTooltipType int type) { - mNeedsQSTooltipType = type; - showQuickSettingsTooltipIfNeeded(); - } - - /** - * @deprecated made obsolete by quick settings rollout. - * - * (TODO 367414968: finish removal.) - */ - @Deprecated - private void showQuickSettingsTooltipIfNeeded() { - } - /** * Returns the user preferred shortcut types or the default shortcut types if not set */ diff --git a/src/com/android/settings/accessibility/LaunchAccessibilityActivityPreferenceFragment.java b/src/com/android/settings/accessibility/LaunchAccessibilityActivityPreferenceFragment.java index e49078bbd71..f729cc7a2cc 100644 --- a/src/com/android/settings/accessibility/LaunchAccessibilityActivityPreferenceFragment.java +++ b/src/com/android/settings/accessibility/LaunchAccessibilityActivityPreferenceFragment.java @@ -71,11 +71,6 @@ public class LaunchAccessibilityActivityPreferenceFragment extends ToggleFeature return view; } - @Override - protected void onPreferenceToggled(String preferenceKey, boolean enabled) { - // Do nothing. - } - @Override protected void onProcessArguments(Bundle arguments) { super.onProcessArguments(arguments); diff --git a/src/com/android/settings/accessibility/ToggleColorInversionPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleColorInversionPreferenceFragment.java index e28622501ed..b779f9d2d6a 100644 --- a/src/com/android/settings/accessibility/ToggleColorInversionPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleColorInversionPreferenceFragment.java @@ -107,10 +107,6 @@ public class ToggleColorInversionPreferenceFragment extends ToggleFeaturePrefere if (enabled == isEnabled) { return; } - - if (enabled) { - showQuickSettingsTooltipIfNeeded(QuickSettingsTooltipType.GUIDE_TO_DIRECT_USE); - } logAccessibilityServiceEnabled(mComponentName, enabled); Settings.Secure.putInt(getContentResolver(), ENABLED, enabled ? ON : OFF); } diff --git a/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java index 5b2df5aeb2f..fe51e69845b 100644 --- a/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java @@ -158,10 +158,6 @@ public class ToggleDaltonizerPreferenceFragment extends ToggleFeaturePreferenceF if (enabled == isEnabled) { return; } - - if (enabled) { - showQuickSettingsTooltipIfNeeded(QuickSettingsTooltipType.GUIDE_TO_DIRECT_USE); - } logAccessibilityServiceEnabled(mComponentName, enabled); Settings.Secure.putInt(getContentResolver(), ENABLED, enabled ? ON : OFF); } diff --git a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java index 65a1cd4b532..49f22bf5994 100644 --- a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java @@ -21,7 +21,6 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh import static com.android.settings.accessibility.AccessibilityDialogUtils.DialogEnums; import static com.android.settings.accessibility.AccessibilityUtil.getShortcutSummaryList; -import android.app.Activity; import android.app.Dialog; import android.app.settings.SettingsEnums; import android.content.ComponentName; @@ -85,8 +84,6 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment public static final String KEY_SHORTCUT_PREFERENCE = "shortcut_preference"; protected static final String KEY_TOP_INTRO_PREFERENCE = "top_intro"; protected static final String KEY_HTML_DESCRIPTION_PREFERENCE = "html_description"; - protected static final String KEY_SAVED_QS_TOOLTIP_RESHOW = "qs_tooltip_reshow"; - protected static final String KEY_SAVED_QS_TOOLTIP_TYPE = "qs_tooltip_type"; protected static final String KEY_ANIMATED_IMAGE = "animated_image"; // For html description of accessibility service, must follow the rule, such as // , a11y settings will get the resources successfully. @@ -112,10 +109,6 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment private CharSequence mDescription; private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener; private AccessibilitySettingsContentObserver mSettingsContentObserver; - - private AccessibilityQuickSettingsTooltipWindow mTooltipWindow; - private boolean mNeedsQSTooltipReshow = false; - private int mNeedsQSTooltipType = QuickSettingsTooltipType.GUIDE_TO_EDIT; private ImageView mImageGetterCacheView; protected final Html.ImageGetter mImageGetter = (String str) -> { if (str != null && str.startsWith(IMG_PREFIX)) { @@ -133,16 +126,6 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment super.onCreate(savedInstanceState); onProcessArguments(getArguments()); - // Restore the user shortcut type and tooltip. - if (savedInstanceState != null) { - if (savedInstanceState.containsKey(KEY_SAVED_QS_TOOLTIP_RESHOW)) { - mNeedsQSTooltipReshow = savedInstanceState.getBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW); - } - if (savedInstanceState.containsKey(KEY_SAVED_QS_TOOLTIP_TYPE)) { - mNeedsQSTooltipType = savedInstanceState.getInt(KEY_SAVED_QS_TOOLTIP_TYPE); - } - } - final int resId = getPreferenceScreenResId(); if (resId <= 0) { final PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen( @@ -227,16 +210,6 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment final SettingsMainSwitchBar switchBar = settingsActivity.getSwitchBar(); switchBar.hide(); - // Reshow tooltip when activity recreate, such as rotate device. - if (mNeedsQSTooltipReshow) { - view.post(() -> { - final Activity activity = getActivity(); - if (activity != null && !activity.isFinishing()) { - showQuickSettingsTooltipIfNeeded(); - } - }); - } - writeConfigDefaultAccessibilityServiceIntoShortcutTargetServiceIfNeeded(getContext()); } @@ -261,24 +234,10 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment super.onPause(); } - @Override - public void onSaveInstanceState(Bundle outState) { - final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing(); - if (mNeedsQSTooltipReshow || isTooltipWindowShowing) { - outState.putBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW, /* value= */ true); - outState.putInt(KEY_SAVED_QS_TOOLTIP_TYPE, mNeedsQSTooltipType); - } - super.onSaveInstanceState(outState); - } - @Override public void onDestroyView() { super.onDestroyView(); removeActionBarToggleSwitch(); - final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing(); - if (isTooltipWindowShowing) { - mTooltipWindow.dismiss(); - } } @Override @@ -314,7 +273,12 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment /** Returns the accessibility tile component name. */ abstract ComponentName getTileComponentName(); - /** Returns the accessibility tile tooltip content. */ + /** Returns the accessibility tile component name. + * + * @deprecated unused, as this class no longer displays tile tooltips. + * + * (TODO 367414968: finish removal.)*/ + @Deprecated abstract CharSequence getTileTooltipContent(@QuickSettingsTooltipType int type); protected void updateToggleServiceTitle(SettingsMainSwitchPreference switchPreference) { @@ -332,9 +296,6 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment } protected void onPreferenceToggled(String preferenceKey, boolean enabled) { - if (enabled) { - showQuickSettingsTooltipIfNeeded(); - } } protected void onInstallSwitchPreferenceToggleSwitch() { @@ -646,7 +607,6 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment */ private void callOnTutorialDialogButtonClicked(DialogInterface dialog, int which) { dialog.dismiss(); - showQuickSettingsTooltipIfNeeded(); } protected void updateShortcutPreferenceData() { @@ -737,26 +697,6 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment } } - /** - * Shows the quick settings tooltip if the quick settings feature is assigned. The tooltip only - * shows once. - * - * @param type The quick settings tooltip type - */ - protected void showQuickSettingsTooltipIfNeeded(@QuickSettingsTooltipType int type) { - mNeedsQSTooltipType = type; - showQuickSettingsTooltipIfNeeded(); - } - - /** - * @deprecated made obsolete by quick settings rollout. - * - * (TODO 367414968: finish removal.) - */ - @Deprecated - private void showQuickSettingsTooltipIfNeeded() { - } - /** Returns user visible name of the tile by given {@link ComponentName}. */ protected CharSequence loadTileLabel(Context context, ComponentName componentName) { final PackageManager packageManager = context.getPackageManager(); diff --git a/src/com/android/settings/accessibility/ToggleReduceBrightColorsPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleReduceBrightColorsPreferenceFragment.java index 16911f6abb8..ff14021108d 100644 --- a/src/com/android/settings/accessibility/ToggleReduceBrightColorsPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleReduceBrightColorsPreferenceFragment.java @@ -149,9 +149,6 @@ public class ToggleReduceBrightColorsPreferenceFragment extends ToggleFeaturePre @Override protected void onPreferenceToggled(String preferenceKey, boolean enabled) { - if (enabled) { - showQuickSettingsTooltipIfNeeded(QuickSettingsTooltipType.GUIDE_TO_DIRECT_USE); - } logAccessibilityServiceEnabled(mComponentName, enabled); mColorDisplayManager.setReduceBrightColorsActivated(enabled); } diff --git a/src/com/android/settings/gestures/OneHandedSettings.java b/src/com/android/settings/gestures/OneHandedSettings.java index 03788889e8b..0a2599e4650 100644 --- a/src/com/android/settings/gestures/OneHandedSettings.java +++ b/src/com/android/settings/gestures/OneHandedSettings.java @@ -22,9 +22,9 @@ import android.content.ComponentName; import android.content.Context; import android.os.Bundle; import android.os.UserHandle; -import android.util.Log; import android.view.LayoutInflater; import android.view.ViewGroup; +import android.widget.CompoundButton; import androidx.recyclerview.widget.RecyclerView; @@ -33,7 +33,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; import com.android.settings.accessibility.AccessibilityFragmentUtils; import com.android.settings.accessibility.AccessibilityShortcutPreferenceFragment; -import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.search.SearchIndexableRaw; @@ -82,12 +81,7 @@ public class OneHandedSettings extends AccessibilityShortcutPreferenceFragment { final MainSwitchPreference mainSwitchPreference = getPreferenceScreen().findPreference(ONE_HANDED_MAIN_SWITCH_KEY); - mainSwitchPreference.addOnSwitchChangeListener((switchView, isChecked) -> { - switchView.setChecked(isChecked); - if (isChecked) { - showQuickSettingsTooltipIfNeeded(QuickSettingsTooltipType.GUIDE_TO_DIRECT_USE); - } - }); + mainSwitchPreference.addOnSwitchChangeListener(CompoundButton::setChecked); } @Override @@ -145,24 +139,6 @@ public class OneHandedSettings extends AccessibilityShortcutPreferenceFragment { return mFeatureName; } - @Override - protected ComponentName getTileComponentName() { - return AccessibilityShortcutController.ONE_HANDED_TILE_COMPONENT_NAME; - } - - @Override - protected CharSequence getTileTooltipContent(@QuickSettingsTooltipType int type) { - final Context context = getContext(); - if (context == null) { - Log.w(TAG, "OneHandedSettings not attached to a context."); - return null; - } - return type == QuickSettingsTooltipType.GUIDE_TO_EDIT - ? context.getText(R.string.accessibility_one_handed_mode_qs_tooltip_content) - : context.getText( - R.string.accessibility_one_handed_mode_auto_added_qs_tooltip_content); - } - @Override protected int getPreferenceScreenResId() { return R.xml.one_handed_settings; diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragmentTest.java index 5973d265a92..552a4a71a71 100644 --- a/tests/robotests/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragmentTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragmentTest.java @@ -19,7 +19,6 @@ package com.android.settings.accessibility; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; -import static com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType; import static com.google.common.truth.Truth.assertThat; @@ -37,7 +36,6 @@ import android.content.Context; import android.content.Intent; import android.icu.text.CaseMap; import android.os.Bundle; -import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.view.LayoutInflater; import android.view.View; @@ -57,7 +55,6 @@ import com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragm import com.android.settings.testutils.shadow.ShadowFragment; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; @@ -83,18 +80,11 @@ public class AccessibilityShortcutPreferenceFragmentTest { PLACEHOLDER_PACKAGE_NAME + "tile.placeholder"; private static final ComponentName PLACEHOLDER_COMPONENT_NAME = new ComponentName( PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_CLASS_NAME); - private static final ComponentName PLACEHOLDER_TILE_COMPONENT_NAME = new ComponentName( - PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_TILE_CLASS_NAME); - private static final String PLACEHOLDER_TILE_TOOLTIP_CONTENT = - PLACEHOLDER_PACKAGE_NAME + "tooltip_content"; - private static final String PLACEHOLDER_DIALOG_TITLE = "title"; private static final String SOFTWARE_SHORTCUT_KEY = Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS; private static final String HARDWARE_SHORTCUT_KEY = Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private TestAccessibilityShortcutPreferenceFragment mFragment; private PreferenceScreen mScreen; private Context mContext = ApplicationProvider.getApplicationContext(); @@ -151,14 +141,6 @@ public class AccessibilityShortcutPreferenceFragmentTest { assertThat(expectedType).isEqualTo(HARDWARE); } - @Test - @Config(shadows = ShadowFragment.class) - public void showQuickSettingsTooltipIfNeeded_dontShowTooltipView() { - mFragment.showQuickSettingsTooltipIfNeeded(QuickSettingsTooltipType.GUIDE_TO_EDIT); - - assertThat(getLatestPopupWindow()).isNull(); - } - @Test @Config(shadows = ShadowFragment.class) public void showGeneralCategory_shouldInitCategory() { @@ -260,16 +242,6 @@ public class AccessibilityShortcutPreferenceFragmentTest { return PLACEHOLDER_PACKAGE_NAME; } - @Override - protected ComponentName getTileComponentName() { - return PLACEHOLDER_TILE_COMPONENT_NAME; - } - - @Override - protected CharSequence getTileTooltipContent(@QuickSettingsTooltipType int type) { - return PLACEHOLDER_TILE_TOOLTIP_CONTENT; - } - @Override public int getUserShortcutTypes() { return 0; diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java index 86322f916b0..6dbb8b58354 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java @@ -315,14 +315,6 @@ public class ToggleFeaturePreferenceFragmentTest { .isEqualTo(PLACEHOLDER_COMPONENT_NAME.flattenToString()); } - @Test - @Config(shadows = ShadowFragment.class) - public void showQuickSettingsTooltipIfNeeded_dontShowTooltipView() { - mFragment.showQuickSettingsTooltipIfNeeded(QuickSettingsTooltipType.GUIDE_TO_EDIT); - - assertThat(getLatestPopupWindow()).isNull(); - } - @Test public void getShortcutTypeSummary_shortcutSummaryIsCorrectlySet() { final PreferredShortcut userPreferredShortcut = new PreferredShortcut( diff --git a/tests/robotests/src/com/android/settings/gestures/OneHandedSettingsTest.java b/tests/robotests/src/com/android/settings/gestures/OneHandedSettingsTest.java index a03ca6192b6..1ce6c3f5499 100644 --- a/tests/robotests/src/com/android/settings/gestures/OneHandedSettingsTest.java +++ b/tests/robotests/src/com/android/settings/gestures/OneHandedSettingsTest.java @@ -21,7 +21,6 @@ import static com.android.settings.gestures.OneHandedSettings.ONE_HANDED_SHORTCU import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; import android.content.Context; import android.os.SystemProperties; @@ -32,8 +31,6 @@ import android.provider.SearchIndexableResource; import androidx.test.core.app.ApplicationProvider; -import com.android.settings.R; -import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType; import com.android.settingslib.search.SearchIndexableRaw; import org.junit.Before; @@ -62,21 +59,6 @@ public class OneHandedSettingsTest { SystemProperties.set(OneHandedSettingsUtils.SUPPORT_ONE_HANDED_MODE, "true"); } - @Test - public void getTileTooltipContent_returnsExpectedValues() { - // Simulate to call getTileTooltipContent after onDetach - assertThat(mSettings.getTileTooltipContent(QuickSettingsTooltipType.GUIDE_TO_EDIT)) - .isNull(); - // Simulate to call getTileTooltipContent after onAttach - when(mSettings.getContext()).thenReturn(mContext); - assertThat(mSettings.getTileTooltipContent(QuickSettingsTooltipType.GUIDE_TO_EDIT)) - .isEqualTo(mContext.getText( - R.string.accessibility_one_handed_mode_qs_tooltip_content)); - assertThat(mSettings.getTileTooltipContent(QuickSettingsTooltipType.GUIDE_TO_DIRECT_USE)) - .isEqualTo(mContext.getText( - R.string.accessibility_one_handed_mode_auto_added_qs_tooltip_content)); - } - @Test public void getLogTag_returnsCorrectTag() { assertThat(mSettings.getLogTag()).isEqualTo("OneHandedSettings"); From aad7bf5ea8c67405be2d16d62ddebd9b6a05e55f Mon Sep 17 00:00:00 2001 From: David Liu Date: Wed, 11 Dec 2024 23:37:15 +0000 Subject: [PATCH 2/6] Fixed battery chart flickering - Hide the hourly chart view by default to avoid the calculate the space. - Remove the alpha animation in chart group which causes the flickering when open the battery page. Bug: 378997970 Flag: EXEMPT bugfix Test: atest BatteryChartPreferenceControllerTest Change-Id: I1b7e5f74fe63ebcae9f9737b0797d870fd56b134 --- res/layout/battery_chart_graph.xml | 5 ++--- .../batteryusage/BatteryChartPreferenceController.java | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/res/layout/battery_chart_graph.xml b/res/layout/battery_chart_graph.xml index 9e816ed9ba1..b84f38a55c5 100644 --- a/res/layout/battery_chart_graph.xml +++ b/res/layout/battery_chart_graph.xml @@ -36,8 +36,7 @@ android:id="@+id/battery_chart_group" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" - android:alpha="0"> + android:orientation="vertical"> diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java index 5e17f4b4871..2edbf99f55a 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java @@ -274,7 +274,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll } if (mDailyChartView != dailyChartView || mHourlyChartView != hourlyChartView) { mHandler.post(() -> setBatteryChartViewInner(dailyChartView, hourlyChartView)); - animateBatteryChartViewGroup(); } if (mBatteryChartViewGroup != null) { final View grandparentView = (View) mBatteryChartViewGroup.getParent(); From 2575e89c8a4b6d4b320a07e2aaffec72dc1faf92 Mon Sep 17 00:00:00 2001 From: songferngwang Date: Fri, 13 Dec 2024 10:39:45 +0000 Subject: [PATCH 3/6] Waiting for the psim subscriptionInfo ready When the pSIM is inserted for the first time, this subscriptionInfo arrives late. It causes the sim onboarding is closed. We add the timer to wait the psim's subscriptionInfo. Bug: 377171470 Flag: EXEMPT bugfix Test: insert the psim and showing the sim onboarding flow. Change-Id: Ib50c28d1bb1372fb822b3cf10cfa3fb22c457b3b --- .../telephony/SubscriptionRepository.kt | 13 + .../sim/receivers/SimSlotChangeHandler.java | 432 ---------------- .../sim/receivers/SimSlotChangeHandler.kt | 480 ++++++++++++++++++ 3 files changed, 493 insertions(+), 432 deletions(-) delete mode 100644 src/com/android/settings/sim/receivers/SimSlotChangeHandler.java create mode 100644 src/com/android/settings/sim/receivers/SimSlotChangeHandler.kt diff --git a/src/com/android/settings/network/telephony/SubscriptionRepository.kt b/src/com/android/settings/network/telephony/SubscriptionRepository.kt index 43bba0759a2..33c1e655040 100644 --- a/src/com/android/settings/network/telephony/SubscriptionRepository.kt +++ b/src/com/android/settings/network/telephony/SubscriptionRepository.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn +import java.util.stream.Collectors private const val TAG = "SubscriptionRepository" @@ -154,6 +155,18 @@ class SubscriptionRepository(private val context: Context) { .conflate() .flowOn(Dispatchers.Default) + fun removableSubscriptionInfoListFlow(): Flow> { + return subscriptionsChangedFlow() + .map { + subscriptionManager.availableSubscriptionInfoList?.stream() + ?.filter { sub: SubscriptionInfo -> !sub.isEmbedded } + ?.collect(Collectors.toList()) ?: emptyList() + } + .conflate() + .onEach { Log.d(TAG, "getRemovableSubscriptionVisibleFlow: $it") } + .flowOn(Dispatchers.Default) + } + @OptIn(ExperimentalCoroutinesApi::class) fun phoneNumberFlow(subId: Int): Flow = activeSubscriptionInfoFlow(subId).flatMapLatest { subInfo -> diff --git a/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java b/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java deleted file mode 100644 index f808924fa41..00000000000 --- a/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Copyright (C) 2020 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.sim.receivers; - -import static android.content.Context.MODE_PRIVATE; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Looper; -import android.os.UserHandle; -import android.provider.Settings; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; -import android.telephony.TelephonyManager; -import android.telephony.UiccCardInfo; -import android.telephony.UiccPortInfo; -import android.telephony.UiccSlotInfo; -import android.util.Log; - -import com.android.settings.flags.Flags; -import com.android.settings.network.SubscriptionUtil; -import com.android.settings.network.UiccSlotUtil; -import com.android.settings.network.UiccSlotsException; -import com.android.settings.sim.ChooseSimActivity; -import com.android.settings.sim.DsdsDialogActivity; -import com.android.settings.sim.SimActivationNotifier; -import com.android.settings.sim.SimNotificationService; -import com.android.settings.sim.SwitchToEsimConfirmDialogActivity; - -import com.google.common.collect.ImmutableList; - -import java.util.List; -import java.util.stream.Collectors; - -import javax.annotation.Nullable; - -/** Perform actions after a slot change event is triggered. */ -public class SimSlotChangeHandler { - private static final String TAG = "SimSlotChangeHandler"; - - private static final String EUICC_PREFS = "euicc_prefs"; - // Shared preference keys - private static final String KEY_REMOVABLE_SLOT_STATE = "removable_slot_state"; - private static final String KEY_SUW_PSIM_ACTION = "suw_psim_action"; - // User's last removable SIM insertion / removal action during SUW. - private static final int LAST_USER_ACTION_IN_SUW_NONE = 0; - private static final int LAST_USER_ACTION_IN_SUW_INSERT = 1; - private static final int LAST_USER_ACTION_IN_SUW_REMOVE = 2; - - private static volatile SimSlotChangeHandler sSlotChangeHandler; - - /** Returns a SIM slot change handler singleton. */ - public static SimSlotChangeHandler get() { - if (sSlotChangeHandler == null) { - synchronized (SimSlotChangeHandler.class) { - if (sSlotChangeHandler == null) { - sSlotChangeHandler = new SimSlotChangeHandler(); - } - } - } - return sSlotChangeHandler; - } - - private SubscriptionManager mSubMgr; - private TelephonyManager mTelMgr; - private Context mContext; - - void onSlotsStatusChange(Context context) { - init(context); - - if (Looper.myLooper() == Looper.getMainLooper()) { - throw new IllegalStateException("Cannot be called from main thread."); - } - - UiccSlotInfo removableSlotInfo = getRemovableUiccSlotInfo(); - if (removableSlotInfo == null) { - Log.e(TAG, "Unable to find the removable slot. Do nothing."); - return; - } - Log.i(TAG, "The removableSlotInfo: " + removableSlotInfo); - int lastRemovableSlotState = getLastRemovableSimSlotState(mContext); - int currentRemovableSlotState = removableSlotInfo.getCardStateInfo(); - Log.d(TAG, - "lastRemovableSlotState: " + lastRemovableSlotState + ",currentRemovableSlotState: " - + currentRemovableSlotState); - boolean isRemovableSimInserted = - lastRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_ABSENT - && currentRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_PRESENT; - boolean isRemovableSimRemoved = - lastRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_PRESENT - && currentRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_ABSENT; - - // Sets the current removable slot state. - setRemovableSimSlotState(mContext, currentRemovableSlotState); - - if (mTelMgr.getActiveModemCount() > 1) { - if (!isRemovableSimInserted) { - Log.d(TAG, "Removable Sim is not inserted in DSDS mode. Do nothing."); - return; - } - - if (Flags.isDualSimOnboardingEnabled()) { - // ForNewUi, when the user inserts the psim, showing the sim onboarding for the user - // to setup the sim switching or the default data subscription in DSDS. - // Will show dialog for below case. - // 1. the psim slot is not active. - // 2. there are one or more active sim. - handleRemovableSimInsertWhenDsds(removableSlotInfo); - return; - } else if (!isMultipleEnabledProfilesSupported()) { - Log.d(TAG, "The device is already in DSDS mode and no MEP. Do nothing."); - return; - } else if (isMultipleEnabledProfilesSupported()) { - handleRemovableSimInsertUnderDsdsMep(removableSlotInfo); - return; - } - } - - if (isRemovableSimInserted) { - handleSimInsert(removableSlotInfo); - return; - } - if (isRemovableSimRemoved) { - handleSimRemove(removableSlotInfo); - return; - } - Log.i(TAG, "Do nothing on slot status changes."); - } - - void onSuwFinish(Context context) { - init(context); - - if (Looper.myLooper() == Looper.getMainLooper()) { - throw new IllegalStateException("Cannot be called from main thread."); - } - - if (mTelMgr.getActiveModemCount() > 1) { - Log.i(TAG, "The device is already in DSDS mode. Do nothing."); - return; - } - - UiccSlotInfo removableSlotInfo = getRemovableUiccSlotInfo(); - if (removableSlotInfo == null) { - Log.e(TAG, "Unable to find the removable slot. Do nothing."); - return; - } - - boolean embeddedSimExist = getGroupedEmbeddedSubscriptions().size() != 0; - int removableSlotAction = getSuwRemovableSlotAction(mContext); - setSuwRemovableSlotAction(mContext, LAST_USER_ACTION_IN_SUW_NONE); - - if (embeddedSimExist - && removableSlotInfo.getCardStateInfo() == UiccSlotInfo.CARD_STATE_INFO_PRESENT) { - if (mTelMgr.isMultiSimSupported() == TelephonyManager.MULTISIM_ALLOWED) { - Log.i(TAG, "DSDS condition satisfied. Show notification."); - SimNotificationService.scheduleSimNotification( - mContext, SimActivationNotifier.NotificationType.ENABLE_DSDS); - } else if (removableSlotAction == LAST_USER_ACTION_IN_SUW_INSERT) { - Log.i( - TAG, - "Both removable SIM and eSIM are present. DSDS condition doesn't" - + " satisfied. User inserted pSIM during SUW. Show choose SIM" - + " screen."); - startChooseSimActivity(true); - } - } else if (removableSlotAction == LAST_USER_ACTION_IN_SUW_REMOVE) { - handleSimRemove(removableSlotInfo); - } - } - - private void init(Context context) { - mSubMgr = - (SubscriptionManager) - context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE); - mTelMgr = context.getSystemService(TelephonyManager.class); - mContext = context; - } - - private void handleSimInsert(UiccSlotInfo removableSlotInfo) { - Log.i(TAG, "Handle SIM inserted."); - if (!isSuwFinished(mContext)) { - Log.i(TAG, "Still in SUW. Handle SIM insertion after SUW is finished"); - setSuwRemovableSlotAction(mContext, LAST_USER_ACTION_IN_SUW_INSERT); - return; - } - if (removableSlotInfo.getPorts().stream().findFirst().get().isActive()) { - Log.i(TAG, "The removable slot is already active. Do nothing."); - return; - } - - if (hasActiveEsimSubscription()) { - if (mTelMgr.isMultiSimSupported() == TelephonyManager.MULTISIM_ALLOWED) { - Log.i(TAG, "Enabled profile exists. DSDS condition satisfied."); - if (Flags.isDualSimOnboardingEnabled()) { - // enable dsds by sim onboarding flow - handleRemovableSimInsertWhenDsds(removableSlotInfo); - } else { - startDsdsDialogActivity(); - } - } else { - Log.i(TAG, "Enabled profile exists. DSDS condition not satisfied."); - startChooseSimActivity(true); - } - return; - } - - Log.i( - TAG, - "No enabled eSIM profile. Ready to switch to removable slot and show" - + " notification."); - try { - UiccSlotUtil.switchToRemovableSlot( - UiccSlotUtil.INVALID_PHYSICAL_SLOT_ID, mContext.getApplicationContext()); - } catch (UiccSlotsException e) { - Log.e(TAG, "Failed to switch to removable slot."); - return; - } - SimNotificationService.scheduleSimNotification( - mContext, SimActivationNotifier.NotificationType.SWITCH_TO_REMOVABLE_SLOT); - } - - private void handleSimRemove(UiccSlotInfo removableSlotInfo) { - Log.i(TAG, "Handle SIM removed."); - - if (!isSuwFinished(mContext)) { - Log.i(TAG, "Still in SUW. Handle SIM removal after SUW is finished"); - setSuwRemovableSlotAction(mContext, LAST_USER_ACTION_IN_SUW_REMOVE); - return; - } - - List groupedEmbeddedSubscriptions = getGroupedEmbeddedSubscriptions(); - if (groupedEmbeddedSubscriptions.size() == 0 || !removableSlotInfo.getPorts().stream() - .findFirst().get().isActive()) { - Log.i(TAG, "eSIM slot is active or no subscriptions exist. Do nothing." - + " The removableSlotInfo: " + removableSlotInfo - + ", groupedEmbeddedSubscriptions: " + groupedEmbeddedSubscriptions); - return; - } - - // If there is only 1 eSIM profile exists, we ask the user if they want to switch to that - // profile. - if (groupedEmbeddedSubscriptions.size() == 1) { - Log.i(TAG, "Only 1 eSIM profile found. Ask user's consent to switch."); - startSwitchSlotConfirmDialogActivity(groupedEmbeddedSubscriptions.get(0)); - return; - } - - // If there are more than 1 eSIM profiles installed, we show a screen to let users to choose - // the number they want to use. - Log.i(TAG, "Multiple eSIM profiles found. Ask user which subscription to use."); - startChooseSimActivity(false); - } - - private boolean hasOtherActiveSubInfo(int psimSubId) { - List activeSubs = SubscriptionUtil.getActiveSubscriptions(mSubMgr); - return activeSubs.stream() - .anyMatch(subscriptionInfo -> subscriptionInfo.getSubscriptionId() != psimSubId); - } - - private boolean hasAnyPortActiveInSlot(UiccSlotInfo removableSlotInfo) { - return removableSlotInfo.getPorts().stream().anyMatch(UiccPortInfo::isActive); - } - - private void handleRemovableSimInsertWhenDsds(UiccSlotInfo removableSlotInfo) { - Log.i(TAG, "ForNewUi: Handle Removable SIM inserted"); - List subscriptionInfos = getAvailableRemovableSubscription(); - if (subscriptionInfos.isEmpty()) { - Log.e(TAG, "Unable to find the removable subscriptionInfo. Do nothing."); - return; - } - Log.d(TAG, "getAvailableRemovableSubscription:" + subscriptionInfos); - int psimSubId = subscriptionInfos.get(0).getSubscriptionId(); - if (!hasAnyPortActiveInSlot(removableSlotInfo) || hasOtherActiveSubInfo(psimSubId)) { - Log.d(TAG, "ForNewUi Start Setup flow"); - startSimConfirmDialogActivity(psimSubId); - } - } - - private void handleRemovableSimInsertUnderDsdsMep(UiccSlotInfo removableSlotInfo) { - Log.i(TAG, "Handle Removable SIM inserted under DSDS+Mep."); - - if (removableSlotInfo.getPorts().stream().findFirst().get().isActive()) { - Log.i(TAG, "The removable slot is already active. Do nothing. removableSlotInfo: " - + removableSlotInfo); - return; - } - - List subscriptionInfos = getAvailableRemovableSubscription(); - if (subscriptionInfos.isEmpty()) { - Log.e(TAG, "Unable to find the removable subscriptionInfo. Do nothing."); - return; - } - Log.d(TAG, "getAvailableRemovableSubscription:" + subscriptionInfos); - startSimConfirmDialogActivity(subscriptionInfos.get(0).getSubscriptionId()); - } - - private int getLastRemovableSimSlotState(Context context) { - final SharedPreferences prefs = context.getSharedPreferences(EUICC_PREFS, MODE_PRIVATE); - return prefs.getInt(KEY_REMOVABLE_SLOT_STATE, UiccSlotInfo.CARD_STATE_INFO_ABSENT); - } - - private void setRemovableSimSlotState(Context context, int state) { - final SharedPreferences prefs = context.getSharedPreferences(EUICC_PREFS, MODE_PRIVATE); - prefs.edit().putInt(KEY_REMOVABLE_SLOT_STATE, state).apply(); - Log.d(TAG, "setRemovableSimSlotState: " + state); - } - - private int getSuwRemovableSlotAction(Context context) { - final SharedPreferences prefs = context.getSharedPreferences(EUICC_PREFS, MODE_PRIVATE); - return prefs.getInt(KEY_SUW_PSIM_ACTION, LAST_USER_ACTION_IN_SUW_NONE); - } - - private void setSuwRemovableSlotAction(Context context, int action) { - final SharedPreferences prefs = context.getSharedPreferences(EUICC_PREFS, MODE_PRIVATE); - prefs.edit().putInt(KEY_SUW_PSIM_ACTION, action).apply(); - } - - @Nullable - private UiccSlotInfo getRemovableUiccSlotInfo() { - UiccSlotInfo[] slotInfos = mTelMgr.getUiccSlotsInfo(); - if (slotInfos == null) { - Log.e(TAG, "slotInfos is null. Unable to get slot infos."); - return null; - } - for (UiccSlotInfo slotInfo : slotInfos) { - if (slotInfo != null && slotInfo.isRemovable()) { - return slotInfo; - } - } - return null; - } - - private static boolean isSuwFinished(Context context) { - try { - // DEVICE_PROVISIONED is 0 if still in setup wizard. 1 if setup completed. - return Settings.Global.getInt( - context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED) - == 1; - } catch (Settings.SettingNotFoundException e) { - Log.e(TAG, "Cannot get DEVICE_PROVISIONED from the device.", e); - return false; - } - } - - private boolean hasActiveEsimSubscription() { - List activeSubs = SubscriptionUtil.getActiveSubscriptions(mSubMgr); - return activeSubs.stream().anyMatch(SubscriptionInfo::isEmbedded); - } - - private List getGroupedEmbeddedSubscriptions() { - List groupedSubscriptions = - SubscriptionUtil.getSelectableSubscriptionInfoList(mContext); - if (groupedSubscriptions == null) { - return ImmutableList.of(); - } - return ImmutableList.copyOf( - groupedSubscriptions.stream() - .filter(sub -> sub.isEmbedded()) - .collect(Collectors.toList())); - } - - protected List getAvailableRemovableSubscription() { - List removableSubscriptions = - SubscriptionUtil.getAvailableSubscriptions(mContext); - return ImmutableList.copyOf( - removableSubscriptions.stream() - // ToDo: This condition is for psim only. If device supports removable - // esim, it needs an new condition. - .filter(sub -> !sub.isEmbedded()) - .collect(Collectors.toList())); - } - - private void startChooseSimActivity(boolean psimInserted) { - Intent intent = ChooseSimActivity.getIntent(mContext); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(ChooseSimActivity.KEY_HAS_PSIM, psimInserted); - mContext.startActivityAsUser(intent, UserHandle.SYSTEM); - } - - private void startSwitchSlotConfirmDialogActivity(SubscriptionInfo subscriptionInfo) { - Intent intent = new Intent(mContext, SwitchToEsimConfirmDialogActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(SwitchToEsimConfirmDialogActivity.KEY_SUB_TO_ENABLE, subscriptionInfo); - mContext.startActivityAsUser(intent, UserHandle.SYSTEM); - } - - private void startDsdsDialogActivity() { - Intent intent = new Intent(mContext, DsdsDialogActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivityAsUser(intent, UserHandle.SYSTEM); - } - - private void startSimConfirmDialogActivity(int subId) { - if (!isSuwFinished(mContext)) { - Log.d(TAG, "Still in SUW. Do nothing"); - return; - } - if (!SubscriptionManager.isUsableSubscriptionId(subId)) { - Log.i(TAG, "Unable to enable subscription due to invalid subscription ID."); - return; - } - Log.d(TAG, "Start ToggleSubscriptionDialogActivity with " + subId + " under DSDS+Mep."); - SubscriptionUtil.startToggleSubscriptionDialogActivity(mContext, subId, true, true); - } - - private boolean isMultipleEnabledProfilesSupported() { - List cardInfos = mTelMgr.getUiccCardsInfo(); - if (cardInfos == null) { - Log.d(TAG, "UICC cards info list is empty."); - return false; - } - return cardInfos.stream().anyMatch( - cardInfo -> cardInfo.isMultipleEnabledProfilesSupported()); - } - - private SimSlotChangeHandler() {} -} diff --git a/src/com/android/settings/sim/receivers/SimSlotChangeHandler.kt b/src/com/android/settings/sim/receivers/SimSlotChangeHandler.kt new file mode 100644 index 00000000000..940184a926a --- /dev/null +++ b/src/com/android/settings/sim/receivers/SimSlotChangeHandler.kt @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2020 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.sim.receivers + +import android.content.Context +import android.content.Intent +import android.os.Looper +import android.os.UserHandle +import android.provider.Settings +import android.provider.Settings.SettingNotFoundException +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.telephony.UiccCardInfo +import android.telephony.UiccSlotInfo +import android.util.Log +import com.android.settings.flags.Flags +import com.android.settings.network.SubscriptionUtil +import com.android.settings.network.UiccSlotUtil +import com.android.settings.network.UiccSlotsException +import com.android.settings.network.telephony.SubscriptionRepository +import com.android.settings.sim.ChooseSimActivity +import com.android.settings.sim.DsdsDialogActivity +import com.android.settings.sim.SimActivationNotifier +import com.android.settings.sim.SimNotificationService +import com.android.settings.sim.SwitchToEsimConfirmDialogActivity +import com.google.common.collect.ImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import java.util.stream.Collectors +import kotlin.concurrent.Volatile + +/** Perform actions after a slot change event is triggered. */ +class SimSlotChangeHandler private constructor() { + private var mSubMgr: SubscriptionManager? = null + private var mTelMgr: TelephonyManager? = null + private var mContext: Context? = null + + fun onSlotsStatusChange(context: Context) { + init(context) + + check(Looper.myLooper() != Looper.getMainLooper()) { "Cannot be called from main thread." } + + val removableSlotInfo = removableUiccSlotInfo + if (removableSlotInfo == null) { + Log.e(TAG, "Unable to find the removable slot. Do nothing.") + return + } + Log.i( + TAG, + "The removableSlotInfo: $removableSlotInfo" + ) + val lastRemovableSlotState = getLastRemovableSimSlotState(mContext!!) + val currentRemovableSlotState = removableSlotInfo.cardStateInfo + Log.d( + TAG, + ("lastRemovableSlotState: " + lastRemovableSlotState + ",currentRemovableSlotState: " + + currentRemovableSlotState) + ) + val isRemovableSimInserted = + lastRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_ABSENT + && currentRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_PRESENT + val isRemovableSimRemoved = + lastRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_PRESENT + && currentRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_ABSENT + + // Sets the current removable slot state. + setRemovableSimSlotState(mContext!!, currentRemovableSlotState) + + if (mTelMgr!!.activeModemCount > 1) { + if (!isRemovableSimInserted) { + Log.d(TAG, "Removable Sim is not inserted in DSDS mode. Do nothing.") + return + } + + if (Flags.isDualSimOnboardingEnabled()) { + // ForNewUi, when the user inserts the psim, showing the sim onboarding for the user + // to setup the sim switching or the default data subscription in DSDS. + // Will show dialog for below case. + // 1. the psim slot is not active. + // 2. there are one or more active sim. + handleRemovableSimInsertWhenDsds(removableSlotInfo) + return + } else if (!isMultipleEnabledProfilesSupported) { + Log.d(TAG, "The device is already in DSDS mode and no MEP. Do nothing.") + return + } else if (isMultipleEnabledProfilesSupported) { + handleRemovableSimInsertUnderDsdsMep(removableSlotInfo) + return + } + } + + if (isRemovableSimInserted) { + handleSimInsert(removableSlotInfo) + return + } + if (isRemovableSimRemoved) { + handleSimRemove(removableSlotInfo) + return + } + Log.i(TAG, "Do nothing on slot status changes.") + } + + fun onSuwFinish(context: Context) { + init(context) + + check(Looper.myLooper() != Looper.getMainLooper()) { "Cannot be called from main thread." } + + if (mTelMgr!!.activeModemCount > 1) { + Log.i(TAG, "The device is already in DSDS mode. Do nothing.") + return + } + + val removableSlotInfo = removableUiccSlotInfo + if (removableSlotInfo == null) { + Log.e(TAG, "Unable to find the removable slot. Do nothing.") + return + } + + val embeddedSimExist = groupedEmbeddedSubscriptions.size != 0 + val removableSlotAction = getSuwRemovableSlotAction(mContext!!) + setSuwRemovableSlotAction(mContext!!, LAST_USER_ACTION_IN_SUW_NONE) + + if (embeddedSimExist + && removableSlotInfo.cardStateInfo == UiccSlotInfo.CARD_STATE_INFO_PRESENT + ) { + if (mTelMgr!!.isMultiSimSupported() == TelephonyManager.MULTISIM_ALLOWED) { + Log.i(TAG, "DSDS condition satisfied. Show notification.") + SimNotificationService.scheduleSimNotification( + mContext, SimActivationNotifier.NotificationType.ENABLE_DSDS + ) + } else if (removableSlotAction == LAST_USER_ACTION_IN_SUW_INSERT) { + Log.i( + TAG, + ("Both removable SIM and eSIM are present. DSDS condition doesn't" + + " satisfied. User inserted pSIM during SUW. Show choose SIM" + + " screen.") + ) + startChooseSimActivity(true) + } + } else if (removableSlotAction == LAST_USER_ACTION_IN_SUW_REMOVE) { + handleSimRemove(removableSlotInfo) + } + } + + private fun init(context: Context) { + mSubMgr = + context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager + mTelMgr = context.getSystemService(TelephonyManager::class.java) + mContext = context + } + + private fun handleSimInsert(removableSlotInfo: UiccSlotInfo) { + Log.i(TAG, "Handle SIM inserted.") + if (!isSuwFinished(mContext!!)) { + Log.i(TAG, "Still in SUW. Handle SIM insertion after SUW is finished") + setSuwRemovableSlotAction(mContext!!, LAST_USER_ACTION_IN_SUW_INSERT) + return + } + if (removableSlotInfo.ports.stream().findFirst().get().isActive) { + Log.i(TAG, "The removable slot is already active. Do nothing.") + return + } + + if (hasActiveEsimSubscription()) { + if (mTelMgr!!.isMultiSimSupported() == TelephonyManager.MULTISIM_ALLOWED) { + Log.i(TAG, "Enabled profile exists. DSDS condition satisfied.") + if (Flags.isDualSimOnboardingEnabled()) { + // enable dsds by sim onboarding flow + handleRemovableSimInsertWhenDsds(removableSlotInfo) + } else { + startDsdsDialogActivity() + } + } else { + Log.i(TAG, "Enabled profile exists. DSDS condition not satisfied.") + startChooseSimActivity(true) + } + return + } + + Log.i( + TAG, + "No enabled eSIM profile. Ready to switch to removable slot and show" + + " notification." + ) + try { + UiccSlotUtil.switchToRemovableSlot( + UiccSlotUtil.INVALID_PHYSICAL_SLOT_ID, mContext!!.applicationContext + ) + } catch (e: UiccSlotsException) { + Log.e(TAG, "Failed to switch to removable slot.") + return + } + SimNotificationService.scheduleSimNotification( + mContext, SimActivationNotifier.NotificationType.SWITCH_TO_REMOVABLE_SLOT + ) + } + + private fun handleSimRemove(removableSlotInfo: UiccSlotInfo) { + Log.i(TAG, "Handle SIM removed.") + + if (!isSuwFinished(mContext!!)) { + Log.i(TAG, "Still in SUW. Handle SIM removal after SUW is finished") + setSuwRemovableSlotAction(mContext!!, LAST_USER_ACTION_IN_SUW_REMOVE) + return + } + + val groupedEmbeddedSubscriptions = + groupedEmbeddedSubscriptions + if (groupedEmbeddedSubscriptions.isEmpty() || !removableSlotInfo.ports.stream() + .findFirst().get().isActive + ) { + Log.i( + TAG, ("eSIM slot is active or no subscriptions exist. Do nothing." + + " The removableSlotInfo: " + removableSlotInfo + + ", groupedEmbeddedSubscriptions: " + groupedEmbeddedSubscriptions) + ) + return + } + + // If there is only 1 eSIM profile exists, we ask the user if they want to switch to that + // profile. + if (groupedEmbeddedSubscriptions.size == 1) { + Log.i(TAG, "Only 1 eSIM profile found. Ask user's consent to switch.") + startSwitchSlotConfirmDialogActivity(groupedEmbeddedSubscriptions[0]) + return + } + + // If there are more than 1 eSIM profiles installed, we show a screen to let users to choose + // the number they want to use. + Log.i(TAG, "Multiple eSIM profiles found. Ask user which subscription to use.") + startChooseSimActivity(false) + } + + private fun hasOtherActiveSubInfo(psimSubId: Int): Boolean { + val activeSubs = SubscriptionUtil.getActiveSubscriptions(mSubMgr) + return activeSubs.stream() + .anyMatch { subscriptionInfo -> subscriptionInfo.subscriptionId != psimSubId } + } + + private fun hasAnyPortActiveInSlot(removableSlotInfo: UiccSlotInfo): Boolean { + return removableSlotInfo.ports.stream().anyMatch { slot -> slot.isActive } + } + + private fun handleRemovableSimInsertWhenDsds(removableSlotInfo: UiccSlotInfo) { + Log.i(TAG, "ForNewUi: Handle Removable SIM inserted") + CoroutineScope(Dispatchers.Default + SupervisorJob()).launch { + withContext(Dispatchers.Default) { + val subscriptionInfos = + withTimeoutOrNull(DEFAULT_WAIT_AFTER_SIM_INSERTED_TIMEOUT_MILLIS) { + SubscriptionRepository(mContext!!) + .removableSubscriptionInfoListFlow() + .firstOrNull { it.isNotEmpty() } + } + + if (subscriptionInfos.isNullOrEmpty()) { + Log.e(TAG, "Unable to find the removable subscriptionInfo. Do nothing.") + return@withContext + } + Log.d( + TAG, + "getAvailableRemovableSubscription:$subscriptionInfos" + ) + val psimSubId = subscriptionInfos[0].subscriptionId + if (!hasAnyPortActiveInSlot(removableSlotInfo) + || hasOtherActiveSubInfo(psimSubId)) { + Log.d(TAG, "ForNewUi Start Setup flow") + startSimConfirmDialogActivity(psimSubId) + } + } + } + } + + private fun handleRemovableSimInsertUnderDsdsMep(removableSlotInfo: UiccSlotInfo) { + Log.i(TAG, "Handle Removable SIM inserted under DSDS+Mep.") + + if (removableSlotInfo.ports.stream().findFirst().get().isActive) { + Log.i( + TAG, "The removable slot is already active. Do nothing. removableSlotInfo: " + + removableSlotInfo + ) + return + } + + val subscriptionInfos = + availableRemovableSubscription + if (subscriptionInfos.isEmpty()) { + Log.e(TAG, "Unable to find the removable subscriptionInfo. Do nothing.") + return + } + Log.d( + TAG, + "getAvailableRemovableSubscription:$subscriptionInfos" + ) + startSimConfirmDialogActivity(subscriptionInfos[0].subscriptionId) + } + + private fun getLastRemovableSimSlotState(context: Context): Int { + val prefs = context.getSharedPreferences(EUICC_PREFS, Context.MODE_PRIVATE) + return prefs.getInt(KEY_REMOVABLE_SLOT_STATE, UiccSlotInfo.CARD_STATE_INFO_ABSENT) + } + + private fun setRemovableSimSlotState(context: Context, state: Int) { + val prefs = context.getSharedPreferences(EUICC_PREFS, Context.MODE_PRIVATE) + prefs.edit().putInt(KEY_REMOVABLE_SLOT_STATE, state).apply() + Log.d(TAG, "setRemovableSimSlotState: $state") + } + + private fun getSuwRemovableSlotAction(context: Context): Int { + val prefs = context.getSharedPreferences(EUICC_PREFS, Context.MODE_PRIVATE) + return prefs.getInt(KEY_SUW_PSIM_ACTION, LAST_USER_ACTION_IN_SUW_NONE) + } + + private fun setSuwRemovableSlotAction(context: Context, action: Int) { + val prefs = context.getSharedPreferences(EUICC_PREFS, Context.MODE_PRIVATE) + prefs.edit().putInt(KEY_SUW_PSIM_ACTION, action).apply() + } + + private val removableUiccSlotInfo: UiccSlotInfo? + get() { + val slotInfos = mTelMgr!!.uiccSlotsInfo + if (slotInfos == null) { + Log.e( + TAG, + "slotInfos is null. Unable to get slot infos." + ) + return null + } + for (slotInfo in slotInfos) { + if (slotInfo != null && slotInfo.isRemovable) { + return slotInfo + } + } + return null + } + + private fun hasActiveEsimSubscription(): Boolean { + val activeSubs = SubscriptionUtil.getActiveSubscriptions(mSubMgr) + return activeSubs.stream().anyMatch { subscriptionInfo -> subscriptionInfo.isEmbedded } + } + + private val groupedEmbeddedSubscriptions: List + get() { + val groupedSubscriptions = + SubscriptionUtil.getSelectableSubscriptionInfoList(mContext) + ?: return ImmutableList.of() + return ImmutableList.copyOf( + groupedSubscriptions.stream() + .filter { sub: SubscriptionInfo -> sub.isEmbedded } + .collect(Collectors.toList())) + } + + protected val availableRemovableSubscription: List + get() { + val removableSubscriptions = + SubscriptionUtil.getAvailableSubscriptions(mContext) + return ImmutableList.copyOf( + removableSubscriptions.stream() + // ToDo: This condition is for psim only. If device supports removable + // esim, it needs an new condition. + .filter { sub: SubscriptionInfo -> !sub.isEmbedded } + .collect(Collectors.toList())) + } + + private fun startChooseSimActivity(psimInserted: Boolean) { + val intent = ChooseSimActivity.getIntent(mContext) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra(ChooseSimActivity.KEY_HAS_PSIM, psimInserted) + mContext!!.startActivityAsUser(intent, UserHandle.SYSTEM) + } + + private fun startSwitchSlotConfirmDialogActivity(subscriptionInfo: SubscriptionInfo) { + val intent = Intent( + mContext, + SwitchToEsimConfirmDialogActivity::class.java + ) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra(SwitchToEsimConfirmDialogActivity.KEY_SUB_TO_ENABLE, subscriptionInfo) + mContext!!.startActivityAsUser(intent, UserHandle.SYSTEM) + } + + private fun startDsdsDialogActivity() { + val intent = Intent(mContext, DsdsDialogActivity::class.java) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + mContext!!.startActivityAsUser(intent, UserHandle.SYSTEM) + } + + private fun startSimConfirmDialogActivity(subId: Int) { + if (!isSuwFinished(mContext!!)) { + Log.d(TAG, "Still in SUW. Do nothing") + return + } + if (!SubscriptionManager.isUsableSubscriptionId(subId)) { + Log.i(TAG, "Unable to enable subscription due to invalid subscription ID.") + return + } + Log.d( + TAG, + "Start ToggleSubscriptionDialogActivity with $subId under DSDS+Mep." + ) + SubscriptionUtil.startToggleSubscriptionDialogActivity(mContext, subId, true, true) + } + + private val isMultipleEnabledProfilesSupported: Boolean + get() { + val cardInfos = mTelMgr!!.uiccCardsInfo + if (cardInfos == null) { + Log.d( + TAG, + "UICC cards info list is empty." + ) + return false + } + return cardInfos.stream() + .anyMatch { cardInfo: UiccCardInfo -> cardInfo.isMultipleEnabledProfilesSupported } + } + + private fun isSuwFinished(context: Context): Boolean { + try { + // DEVICE_PROVISIONED is 0 if still in setup wizard. 1 if setup completed. + return (Settings.Global.getInt( + context.contentResolver, Settings.Global.DEVICE_PROVISIONED) + == 1) + } catch (e: SettingNotFoundException) { + Log.e(TAG, "Cannot get DEVICE_PROVISIONED from the device.", e) + return false + } + } + + companion object { + private const val TAG = "SimSlotChangeHandler" + + private const val EUICC_PREFS = "euicc_prefs" + + // Shared preference keys + private const val KEY_REMOVABLE_SLOT_STATE = "removable_slot_state" + private const val KEY_SUW_PSIM_ACTION = "suw_psim_action" + + // User's last removable SIM insertion / removal action during SUW. + private const val LAST_USER_ACTION_IN_SUW_NONE = 0 + private const val LAST_USER_ACTION_IN_SUW_INSERT = 1 + private const val LAST_USER_ACTION_IN_SUW_REMOVE = 2 + + private const val DEFAULT_WAIT_AFTER_SIM_INSERTED_TIMEOUT_MILLIS: Long = 25 * 1000L + + @Volatile + private var slotChangeHandler: SimSlotChangeHandler? = null + + /** Returns a SIM slot change handler singleton. */ + @JvmStatic + fun get(): SimSlotChangeHandler? { + if (slotChangeHandler == null) { + synchronized(SimSlotChangeHandler::class.java) { + if (slotChangeHandler == null) { + slotChangeHandler = SimSlotChangeHandler() + } + } + } + return slotChangeHandler + } + } +} From 30192ba9362bac1c38135a3f4c99310c60a478fc Mon Sep 17 00:00:00 2001 From: SongFerng Wang Date: Sun, 15 Dec 2024 19:38:54 -0800 Subject: [PATCH 4/6] Disable failed tests from CellInfoUtilTest These tests are currently failing and breaking builds. Bug: 383858953 Change-Id: Idcb2f1f122b4e03990e7242b938438e1d8bb5367 Test: NA Flag: EXEMPT bugfix --- .../com/android/settings/network/telephony/CellInfoUtilTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/src/com/android/settings/network/telephony/CellInfoUtilTest.kt b/tests/unit/src/com/android/settings/network/telephony/CellInfoUtilTest.kt index 27b5c382211..c6974abf059 100644 --- a/tests/unit/src/com/android/settings/network/telephony/CellInfoUtilTest.kt +++ b/tests/unit/src/com/android/settings/network/telephony/CellInfoUtilTest.kt @@ -24,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.network.telephony.CellInfoUtil.getNetworkTitle import com.android.settings.network.telephony.CellInfoUtil.getOperatorNumeric import com.google.common.truth.Truth.assertThat +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith @@ -95,6 +96,7 @@ class CellInfoUtilTest { } @Test + @Ignore("b/383858953") fun cellInfoListToString() { val cellInfoList = listOf( From de777df241a996df71ed274407a9609c75595bf4 Mon Sep 17 00:00:00 2001 From: Jacky Wang Date: Mon, 16 Dec 2024 09:40:46 +0800 Subject: [PATCH 5/6] [Catalyst] Fix calling pid/uid in PreferenceService Bug: 379750656 Bug: 374115149 Flag: com.android.settingslib.flags.settings_catalyst Test: N/A Change-Id: Ia33141549faa68f3a9fca4648a88687974fab2e8 --- .../settings/service/PreferenceService.kt | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/com/android/settings/service/PreferenceService.kt b/src/com/android/settings/service/PreferenceService.kt index d07e78d333e..81ab5840a04 100644 --- a/src/com/android/settings/service/PreferenceService.kt +++ b/src/com/android/settings/service/PreferenceService.kt @@ -19,7 +19,6 @@ package com.android.settings.service import android.app.Application import android.os.Binder import android.os.OutcomeReceiver -import android.os.Process import android.service.settings.preferences.GetValueRequest import android.service.settings.preferences.GetValueResult import android.service.settings.preferences.MetadataRequest @@ -37,7 +36,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import java.lang.Exception class PreferenceService : SettingsPreferenceService() { @@ -49,14 +47,22 @@ class PreferenceService : SettingsPreferenceService() { override fun onGetAllPreferenceMetadata( request: MetadataRequest, - callback: OutcomeReceiver + callback: OutcomeReceiver, ) { + // MUST get pid/uid in binder thread + val callingPid = Binder.getCallingPid() + val callingUid = Binder.getCallingUid() scope.launch { - val graphProto = graphApi.invoke(application, Process.myUid(), Binder.getCallingUid(), - GetPreferenceGraphRequest( - includeValue = false, - flags = PreferenceGetterFlags.METADATA - )) + val graphProto = + graphApi.invoke( + application, + callingPid, + callingUid, + GetPreferenceGraphRequest( + includeValue = false, + flags = PreferenceGetterFlags.METADATA, + ), + ) val result = transformCatalystGetMetadataResponse(this@PreferenceService, graphProto) callback.onResult(result) } @@ -64,17 +70,16 @@ class PreferenceService : SettingsPreferenceService() { override fun onGetPreferenceValue( request: GetValueRequest, - callback: OutcomeReceiver + callback: OutcomeReceiver, ) { + // MUST get pid/uid in binder thread + val callingPid = Binder.getCallingPid() + val callingUid = Binder.getCallingUid() scope.launch { val apiRequest = transformFrameworkGetValueRequest(request) - val response = getApiHandler.invoke(application, Process.myUid(), - Binder.getCallingUid(), apiRequest) - val result = transformCatalystGetValueResponse( - this@PreferenceService, - request, - response - ) + val response = getApiHandler.invoke(application, callingPid, callingUid, apiRequest) + val result = + transformCatalystGetValueResponse(this@PreferenceService, request, response) if (result == null) { callback.onError(IllegalStateException("No response")) } else { @@ -85,8 +90,11 @@ class PreferenceService : SettingsPreferenceService() { override fun onSetPreferenceValue( request: SetValueRequest, - callback: OutcomeReceiver + callback: OutcomeReceiver, ) { + // MUST get pid/uid in binder thread + val callingPid = Binder.getCallingPid() + val callingUid = Binder.getCallingUid() scope.launch { val apiRequest = transformFrameworkSetValueRequest(request) if (apiRequest == null) { @@ -94,8 +102,7 @@ class PreferenceService : SettingsPreferenceService() { SetValueResult.Builder(SetValueResult.RESULT_INVALID_REQUEST).build() ) } else { - val response = setApiHandler.invoke(application, Process.myUid(), - Binder.getCallingUid(), apiRequest) + val response = setApiHandler.invoke(application, callingPid, callingUid, apiRequest) callback.onResult(transformCatalystSetValueResponse(response)) } @@ -106,9 +113,9 @@ class PreferenceService : SettingsPreferenceService() { private class GraphProvider(override val id: Int) : GetPreferenceGraphApiHandler(emptySet()) { override fun hasPermission( application: Application, - myUid: Int, + callingPid: Int, callingUid: Int, - request: GetPreferenceGraphRequest + request: GetPreferenceGraphRequest, ) = true } } From 807cf78a51ec247d2027060c1d7872ee7e267f05 Mon Sep 17 00:00:00 2001 From: Matthew DeVore Date: Thu, 5 Dec 2024 19:05:42 +0000 Subject: [PATCH 6/6] Show display topology in the pane Populate the topology pane with the topology as returned from DisplayManager. This adds padding but not the proper rounded corners or highlighting for the blocks. That will come later, probably after feature complete while still in dogfood. Test: add and remove overlays while external display fragment is shown - verify pane is refreshed Test: add two overlay displays, verify two blocks appear in pane with system wallpaper Test: with no freeform window displays, verify a "not enabled" message appears in pane with no display blocks Test: DisplayTopologyPreferenceTest Flag: com.android.settings.flags.display_topology_pane_in_display_list Bug: b/352648432 Change-Id: Ibb35af53c24d6feb1d763e4b2bf2ec9fee2ae24d --- res/layout/display_topology_preference.xml | 11 +- .../display/DisplayTopology.kt | 139 +++++++++++++++++- .../display/DisplayTopologyPreferenceTest.kt | 120 +++++++++++++++ 3 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt diff --git a/res/layout/display_topology_preference.xml b/res/layout/display_topology_preference.xml index d2e430027a5..beaf816e28f 100644 --- a/res/layout/display_topology_preference.xml +++ b/res/layout/display_topology_preference.xml @@ -16,8 +16,9 @@ @@ -27,14 +28,14 @@ android:layout_width="match_parent" android:src="@drawable/display_topology_background"/> + android:layout_height="match_parent"/> + android:paddingBottom="10dp" + android:paddingTop="10dp" /> diff --git a/src/com/android/settings/connecteddevice/display/DisplayTopology.kt b/src/com/android/settings/connecteddevice/display/DisplayTopology.kt index 76abc031055..9cac7727a7f 100644 --- a/src/com/android/settings/connecteddevice/display/DisplayTopology.kt +++ b/src/com/android/settings/connecteddevice/display/DisplayTopology.kt @@ -16,14 +16,32 @@ package com.android.settings.connecteddevice.display +import android.app.WallpaperManager import com.android.settings.R import android.content.Context +import android.graphics.Color import android.graphics.Point import android.graphics.PointF import android.graphics.RectF +import android.graphics.drawable.Drawable +import android.graphics.drawable.ColorDrawable +import android.hardware.display.DisplayManager +import android.hardware.display.DisplayTopology +import android.hardware.display.DisplayTopology.TreeNode.POSITION_BOTTOM +import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT +import android.hardware.display.DisplayTopology.TreeNode.POSITION_RIGHT +import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP +import android.util.Log +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.widget.Button +import android.widget.FrameLayout +import android.widget.TextView +import androidx.annotation.VisibleForTesting import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder import java.util.Locale @@ -143,11 +161,27 @@ class TopologyScale( const val PREFERENCE_KEY = "display_topology_preference" +/** dp of padding on each side of a display block. */ +const val BLOCK_PADDING = 2 + /** * DisplayTopologyPreference allows the user to change the display topology * when there is one or more extended display attached. */ -class DisplayTopologyPreference(context : Context) : Preference(context) { +class DisplayTopologyPreference(context : Context) + : Preference(context), ViewTreeObserver.OnGlobalLayoutListener { + @VisibleForTesting lateinit var mPaneContent : FrameLayout + @VisibleForTesting lateinit var mPaneHolder : FrameLayout + @VisibleForTesting lateinit var mTopologyHint : TextView + + @VisibleForTesting var injector : Injector + + /** + * This is needed to prevent a repopulation of the pane causing another + * relayout and vice-versa ad infinitum. + */ + private var mPaneNeedsRefresh = false + init { layoutResource = R.layout.display_topology_preference @@ -155,5 +189,108 @@ class DisplayTopologyPreference(context : Context) : Preference(context) { isSelectable = false key = PREFERENCE_KEY + + injector = Injector() + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + + val newPane = holder.findViewById(R.id.display_topology_pane_content) as FrameLayout + if (this::mPaneContent.isInitialized) { + if (newPane == mPaneContent) { + return + } + mPaneContent.viewTreeObserver.removeOnGlobalLayoutListener(this) + } + mPaneContent = newPane + mPaneHolder = holder.itemView as FrameLayout + mTopologyHint = holder.findViewById(R.id.topology_hint) as TextView + mPaneContent.viewTreeObserver.addOnGlobalLayoutListener(this) + } + + override fun onAttached() { + // We don't know if topology changes happened when we were detached, as it is impossible to + // listen at that time (we must remove listeners when detaching). Setting this flag makes + // the following onGlobalLayout call refresh the pane. + mPaneNeedsRefresh = true + } + + override fun onGlobalLayout() { + if (mPaneNeedsRefresh) { + mPaneNeedsRefresh = false + refreshPane() + } + } + + open class Injector { + open fun displayTopology(context : Context) : DisplayTopology? { + val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + return displayManager.displayTopology + } + + open fun wallpaper(context : Context) : Drawable { + return WallpaperManager.getInstance(context).drawable ?: ColorDrawable(Color.BLACK) + } + } + + private fun calcAbsRects( + dest : MutableMap, n : DisplayTopology.TreeNode, x : Float, y : Float) { + dest.put(n.displayId, RectF(x, y, x + n.width, y + n.height)) + + for (c in n.children) { + val (xoff, yoff) = when (c.position) { + POSITION_LEFT -> Pair(-c.width, +c.offset) + POSITION_RIGHT -> Pair(+n.width, +c.offset) + POSITION_TOP -> Pair(+c.offset, -c.height) + POSITION_BOTTOM -> Pair(+c.offset, +n.height) + else -> throw IllegalStateException("invalid position for display: ${c}") + } + calcAbsRects(dest, c, x + xoff, y + yoff) + } + } + + private fun refreshPane() { + mPaneContent.removeAllViews() + + val root = injector.displayTopology(context)?.root + if (root == null) { + // This occurs when no topology is active. + // TODO(b/352648432): show main display or mirrored displays rather than an empty pane. + mTopologyHint.text = "" + return + } + mTopologyHint.text = context.getString(R.string.external_display_topology_hint) + + val blocksPos = buildMap { calcAbsRects(this, root, x = 0f, y = 0f) } + + val scaling = TopologyScale( + mPaneContent.width, minEdgeLength = 60, maxBlockRatio = 0.12f, blocksPos.values) + mPaneHolder.layoutParams.let { + if (it.height != scaling.paneHeight) { + it.height = scaling.paneHeight + mPaneHolder.layoutParams = it + } + } + val wallpaper = injector.wallpaper(context) + blocksPos.values.forEach { p -> + Button(context).apply { + isScrollContainer = false + isVerticalScrollBarEnabled = false + isHorizontalScrollBarEnabled = false + background = wallpaper + val topLeft = scaling.displayToPaneCoor(PointF(p.left, p.top)) + val bottomRight = scaling.displayToPaneCoor(PointF(p.right, p.bottom)) + + mPaneContent.addView(this) + + val layout = layoutParams + layout.width = bottomRight.x - topLeft.x - BLOCK_PADDING * 2 + layout.height = bottomRight.y - topLeft.y - BLOCK_PADDING * 2 + layoutParams = layout + x = (topLeft.x + BLOCK_PADDING).toFloat() + y = (topLeft.y + BLOCK_PADDING).toFloat() + } + } } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt b/tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt new file mode 100644 index 00000000000..ad633cc8c8a --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt @@ -0,0 +1,120 @@ +/* + * 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.connecteddevice.display + +import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.graphics.drawable.ColorDrawable +import android.hardware.display.DisplayTopology +import android.view.View +import android.widget.FrameLayout +import androidx.preference.PreferenceViewHolder +import androidx.test.core.app.ApplicationProvider + +import com.android.settings.R +import com.google.common.truth.Truth.assertThat + +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DisplayTopologyPreferenceTest { + val context = ApplicationProvider.getApplicationContext() + val preference = DisplayTopologyPreference(context) + val injector = TestInjector() + val rootView = View.inflate(context, preference.layoutResource, /*parent=*/ null) + val holder = PreferenceViewHolder.createInstanceForTests(rootView) + val wallpaper = ColorDrawable(Color.MAGENTA) + + init { + preference.injector = injector + injector.systemWallpaper = wallpaper + preference.onBindViewHolder(holder) + } + + class TestInjector : DisplayTopologyPreference.Injector() { + var topology : DisplayTopology? = null + var systemWallpaper : Drawable? = null + + override fun displayTopology(context : Context) : DisplayTopology? { return topology } + + override fun wallpaper(context : Context) : Drawable { return systemWallpaper!! } + } + + @Test + fun disabledTopology() { + preference.onAttached() + preference.onGlobalLayout() + + assertThat(preference.mPaneContent.childCount).isEqualTo(0) + // TODO(b/352648432): update test when we show the main display even when + // a topology is not active. + assertThat(preference.mTopologyHint.text).isEqualTo("") + } + + @Test + fun twoDisplaysGenerateBlocks() { + val child = DisplayTopology.TreeNode( + /* displayId= */ 42, /* width= */ 100f, /* height= */ 80f, + POSITION_LEFT, /* offset= */ 42f) + val root = DisplayTopology.TreeNode( + /* displayId= */ 0, /* width= */ 200f, /* height= */ 160f, + POSITION_LEFT, /* offset= */ 0f) + root.addChild(child) + injector.topology = DisplayTopology(root, /*primaryDisplayId=*/ 0) + + // This layoutParams needs to be non-null for the global layout handler. + preference.mPaneHolder.layoutParams = FrameLayout.LayoutParams( + /* width= */ 640, /* height= */ 480) + + // Force pane width to have a reasonable value (hundreds of dp) so the TopologyScale is + // calculated reasonably. + preference.mPaneContent.left = 0 + preference.mPaneContent.right = 640 + + preference.onAttached() + preference.onGlobalLayout() + + assertThat(preference.mPaneContent.childCount).isEqualTo(2) + val block0 = preference.mPaneContent.getChildAt(0) + val block1 = preference.mPaneContent.getChildAt(1) + + // Block of child display is on the left. + val (childBlock, rootBlock) = if (block0.x < block1.x) + listOf(block0, block1) + else + listOf(block1, block0) + + // After accounting for padding, child should be half the length of root in each dimension. + assertThat(childBlock.layoutParams.width + BLOCK_PADDING) + .isEqualTo(rootBlock.layoutParams.width / 2) + assertThat(childBlock.layoutParams.height + BLOCK_PADDING) + .isEqualTo(rootBlock.layoutParams.height / 2) + assertThat(childBlock.y).isGreaterThan(rootBlock.y) + assertThat(block0.background).isEqualTo(wallpaper) + assertThat(block1.background).isEqualTo(wallpaper) + assertThat(rootBlock.x - BLOCK_PADDING * 2) + .isEqualTo(childBlock.x + childBlock.layoutParams.width) + + assertThat(preference.mTopologyHint.text) + .isEqualTo(context.getString(R.string.external_display_topology_hint)) + } +}