diff --git a/Android.bp b/Android.bp index a720658f9f..9d675a4b3e 100644 --- a/Android.bp +++ b/Android.bp @@ -24,7 +24,6 @@ android_library { ], srcs: [ "tests/tapl/**/*.java", - "src/com/android/launcher3/util/SecureSettingsObserver.java", "src/com/android/launcher3/ResourceUtils.java", "src/com/android/launcher3/testing/TestProtocol.java", ], diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java index 4301377808..f99b7e6af4 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java @@ -17,14 +17,16 @@ package com.android.quickstep; import static android.content.Intent.ACTION_USER_UNLOCKED; +import static com.android.launcher3.util.SettingsCache.ONE_HANDED_ENABLED; +import static com.android.launcher3.util.SettingsCache.ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED; import static com.android.launcher3.util.DisplayController.DisplayHolder.CHANGE_ALL; import static com.android.launcher3.util.DisplayController.DisplayHolder.CHANGE_FRAME_DELAY; import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON; import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED; @@ -44,6 +46,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.graphics.Region; +import android.net.Uri; import android.os.Process; import android.os.SystemProperties; import android.os.UserManager; @@ -57,11 +60,11 @@ import androidx.annotation.BinderThread; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.util.SettingsCache; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DisplayController.DisplayHolder; import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener; import com.android.launcher3.util.DisplayController.Info; -import com.android.launcher3.util.SecureSettingsObserver; import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener; import com.android.quickstep.SysUINavigationMode.OneHandedModeChangeListener; import com.android.quickstep.util.NavBarPosition; @@ -177,33 +180,33 @@ public class RecentsAnimationDeviceState implements } } + SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext); if (mIsOneHandedModeSupported) { - SecureSettingsObserver oneHandedEnabledObserver = - SecureSettingsObserver.newOneHandedSettingsObserver( - mContext, enabled -> mIsOneHandedModeEnabled = enabled); - oneHandedEnabledObserver.register(); - oneHandedEnabledObserver.dispatchOnChange(); - runOnDestroy(oneHandedEnabledObserver::unregister); + Uri oneHandedUri = Settings.Secure.getUriFor(ONE_HANDED_ENABLED); + SettingsCache.OnChangeListener onChangeListener = + enabled -> mIsOneHandedModeEnabled = enabled; + settingsCache.register(oneHandedUri, onChangeListener); + settingsCache.dispatchOnChange(oneHandedUri); + runOnDestroy(() -> settingsCache.unregister(oneHandedUri, onChangeListener)); } else { mIsOneHandedModeEnabled = false; } - SecureSettingsObserver swipeBottomEnabledObserver = - SecureSettingsObserver.newSwipeToNotificationSettingsObserver( - mContext, enabled -> mIsSwipeToNotificationEnabled = enabled); - swipeBottomEnabledObserver.register(); - swipeBottomEnabledObserver.dispatchOnChange(); - runOnDestroy(swipeBottomEnabledObserver::unregister); - SecureSettingsObserver userSetupObserver = new SecureSettingsObserver( - context.getContentResolver(), - e -> mIsUserSetupComplete = e, - Settings.Secure.USER_SETUP_COMPLETE, - 0); - mIsUserSetupComplete = userSetupObserver.getValue(); + Uri swipeBottomNotificationUri = + Settings.Secure.getUriFor(ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED); + SettingsCache.OnChangeListener onChangeListener = + enabled -> mIsSwipeToNotificationEnabled = enabled; + settingsCache.register(swipeBottomNotificationUri, onChangeListener); + settingsCache.dispatchOnChange(swipeBottomNotificationUri); + runOnDestroy(() -> settingsCache.unregister(swipeBottomNotificationUri, onChangeListener)); + + Uri setupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE); + mIsUserSetupComplete = settingsCache.getValue(setupCompleteUri, 0); if (!mIsUserSetupComplete) { - userSetupObserver.register(); - runOnDestroy(userSetupObserver::unregister); + SettingsCache.OnChangeListener userSetupChangeListener = e -> mIsUserSetupComplete = e; + settingsCache.register(setupCompleteUri, userSetupChangeListener); + runOnDestroy(() -> settingsCache.unregister(setupCompleteUri, userSetupChangeListener)); } } diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java index 3157865f78..f336bf5260 100644 --- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java +++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java @@ -28,7 +28,7 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_DISABLED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED; import static com.android.launcher3.model.QuickstepModelDelegate.LAST_PREDICTION_ENABLED_STATE; -import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver; +import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI; import android.content.Context; import android.content.SharedPreferences; @@ -43,7 +43,7 @@ import com.android.launcher3.R; import com.android.launcher3.logging.InstanceIdSequence; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.StatsLogger; -import com.android.launcher3.util.SecureSettingsObserver; +import com.android.launcher3.util.SettingsCache; import com.android.quickstep.SysUINavigationMode; import com.android.quickstep.SysUINavigationMode.Mode; import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener; @@ -77,11 +77,10 @@ public class SettingsChangeLogger implements getPrefs(context).registerOnSharedPreferenceChangeListener(this); getDevicePrefs(context).registerOnSharedPreferenceChangeListener(this); - SecureSettingsObserver dotsObserver = - newNotificationSettingsObserver(context, this::onNotificationDotsChanged); - mNotificationDotsEnabled = dotsObserver.getValue(); - dispatchUserEvent(); - + SettingsCache mSettingsCache = SettingsCache.INSTANCE.get(context); + mSettingsCache.register(NOTIFICATION_BADGING_URI, + this::onNotificationDotsChanged); + mSettingsCache.dispatchOnChange(NOTIFICATION_BADGING_URI); } private static ArrayMap loadPrefKeys(Context context) { diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java index a3ee9122a7..215f05ae4c 100644 --- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java +++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java @@ -23,22 +23,18 @@ import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; -import static com.android.launcher3.Utilities.newContentObserver; +import static com.android.launcher3.util.SettingsCache.ROTATION_SETTING_URI; import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static java.lang.annotation.RetentionPolicy.SOURCE; -import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; -import android.database.ContentObserver; import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.Rect; -import android.os.Handler; -import android.provider.Settings; import android.util.Log; import android.view.MotionEvent; import android.view.OrientationEventListener; @@ -51,6 +47,7 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.util.SettingsCache; import com.android.launcher3.testing.TestProtocol; import com.android.launcher3.touch.PagedOrientationHandler; import com.android.launcher3.util.WindowBounds; @@ -72,9 +69,6 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre private static final String TAG = "RecentsOrientedState"; private static final boolean DEBUG = false; - private ContentObserver mSystemAutoRotateObserver = - newContentObserver(new Handler(), t -> updateAutoRotateSetting()); - @Retention(SOURCE) @IntDef({ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270}) public @interface SurfaceRotation {} @@ -118,9 +112,11 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre | FLAG_SWIPE_UP_NOT_RUNNING; private final Context mContext; - private final ContentResolver mContentResolver; private final SharedPreferences mSharedPrefs; private final OrientationEventListener mOrientationListener; + private final SettingsCache mSettingsCache; + private final SettingsCache.OnChangeListener mRotationChangeListener = + isEnabled -> updateAutoRotateSetting(); private final Matrix mTmpMatrix = new Matrix(); @@ -138,7 +134,6 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre public RecentsOrientedState(Context context, BaseActivityInterface sizeStrategy, IntConsumer rotationChangeListener) { mContext = context; - mContentResolver = context.getContentResolver(); mSharedPrefs = Utilities.getPrefs(context); mOrientationListener = new OrientationEventListener(context) { @Override @@ -162,6 +157,7 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre mFlags |= FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY; } mFlags |= FLAG_SWIPE_UP_NOT_RUNNING; + mSettingsCache = SettingsCache.INSTANCE.get(mContext); initFlags(); } @@ -271,8 +267,8 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre } private void updateAutoRotateSetting() { - setFlag(FLAG_SYSTEM_ROTATION_ALLOWED, Settings.System.getInt(mContentResolver, - Settings.System.ACCELEROMETER_ROTATION, 1) == 1); + setFlag(FLAG_SYSTEM_ROTATION_ALLOWED, + mSettingsCache.getValue(ROTATION_SETTING_URI, 1)); } private void updateHomeRotationSetting() { @@ -295,9 +291,7 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre public void initListeners() { if (isMultipleOrientationSupportedByDevice()) { mSharedPrefs.registerOnSharedPreferenceChangeListener(this); - mContentResolver.registerContentObserver( - Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), - false, mSystemAutoRotateObserver); + mSettingsCache.register(ROTATION_SETTING_URI, mRotationChangeListener); } initFlags(); } @@ -308,7 +302,7 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre public void destroyListeners() { if (isMultipleOrientationSupportedByDevice()) { mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this); - mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver); + mSettingsCache.unregister(ROTATION_SETTING_URI, mRotationChangeListener); } setRotationWatcherEnabled(false); } diff --git a/robolectric_tests/src/com/android/launcher3/util/SettingsCacheTest.java b/robolectric_tests/src/com/android/launcher3/util/SettingsCacheTest.java new file mode 100644 index 0000000000..fbf4c63e65 --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/util/SettingsCacheTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2021 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.launcher3.util; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.net.Uri; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.Collections; + +@RunWith(RobolectricTestRunner.class) +public class SettingsCacheTest { + + public static final Uri KEY_SYSTEM_URI_TEST1 = Uri.parse("content://settings/system/test1"); + public static final Uri KEY_SYSTEM_URI_TEST2 = Uri.parse("content://settings/system/test2");; + + private SettingsCache.OnChangeListener mChangeListener; + private SettingsCache mSettingsCache; + + @Before + public void setup() { + mChangeListener = mock(SettingsCache.OnChangeListener.class); + Context targetContext = RuntimeEnvironment.application; + mSettingsCache = SettingsCache.INSTANCE.get(targetContext); + mSettingsCache.register(KEY_SYSTEM_URI_TEST1, mChangeListener); + } + + @Test + public void listenerCalledOnChange() { + mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1); + verify(mChangeListener, times(1)).onSettingsChanged(true); + } + + @Test + public void getValueRespectsDefaultValue() { + // Case of key not found + boolean val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0); + assertFalse(val); + } + + @Test + public void getValueHitsCache() { + mSettingsCache.setKeyCache(Collections.singletonMap(KEY_SYSTEM_URI_TEST1, true)); + boolean val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0); + assertTrue(val); + } + + @Test + public void getValueUpdatedCache() { + // First ensure there's nothing in cache + boolean val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0); + assertFalse(val); + + mSettingsCache.setKeyCache(Collections.singletonMap(KEY_SYSTEM_URI_TEST1, true)); + val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0); + assertTrue(val); + } + + @Test + public void multipleListenersSingleKey() { + SettingsCache.OnChangeListener secondListener = mock(SettingsCache.OnChangeListener.class); + mSettingsCache.register(KEY_SYSTEM_URI_TEST1, secondListener); + + mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1); + verify(mChangeListener, times(1)).onSettingsChanged(true); + verify(secondListener, times(1)).onSettingsChanged(true); + } + + @Test + public void singleListenerMultipleKeys() { + SettingsCache.OnChangeListener secondListener = mock(SettingsCache.OnChangeListener.class); + mSettingsCache.register(KEY_SYSTEM_URI_TEST2, secondListener); + + mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1); + mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST2); + verify(mChangeListener, times(1)).onSettingsChanged(true); + verify(secondListener, times(1)).onSettingsChanged(true); + } + + @Test + public void sameListenerMultipleKeys() { + SettingsCache.OnChangeListener secondListener = mock(SettingsCache.OnChangeListener.class); + mSettingsCache.register(KEY_SYSTEM_URI_TEST2, mChangeListener); + + mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1); + mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST2); + verify(mChangeListener, times(2)).onSettingsChanged(true); + verify(secondListener, times(0)).onSettingsChanged(true); + } +} diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index a4181c53c9..57d76005a6 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -17,8 +17,8 @@ package com.android.launcher3; import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS; +import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; -import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver; import android.content.ComponentName; import android.content.Context; @@ -37,10 +37,10 @@ import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.pm.InstallSessionHelper; import com.android.launcher3.pm.InstallSessionTracker; import com.android.launcher3.pm.UserCache; +import com.android.launcher3.util.SettingsCache; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.SafeCloseable; -import com.android.launcher3.util.SecureSettingsObserver; import com.android.launcher3.util.SimpleBroadcastReceiver; import com.android.launcher3.widget.custom.CustomWidgetManager; @@ -57,8 +57,9 @@ public class LauncherAppState { private final IconCache mIconCache; private final WidgetPreviewLoader mWidgetCache; private final InvariantDeviceProfile mInvariantDeviceProfile; + private SettingsCache.OnChangeListener mNotificationSettingsChangedListener; - private SecureSettingsObserver mNotificationDotsObserver; + private SettingsCache mSettingsCache; private InstallSessionTracker mInstallSessionTracker; private SimpleBroadcastReceiver mModelChangeReceiver; private SafeCloseable mCalendarChangeTracker; @@ -108,10 +109,11 @@ public class LauncherAppState { .registerInstallTracker(mModel); // Register an observer to rebind the notification listener when dots are re-enabled. - mNotificationDotsObserver = - newNotificationSettingsObserver(mContext, this::onNotificationSettingsChanged); - mNotificationDotsObserver.register(); - mNotificationDotsObserver.dispatchOnChange(); + mSettingsCache = SettingsCache.INSTANCE.get(mContext); + mNotificationSettingsChangedListener = this::onNotificationSettingsChanged; + mSettingsCache.register(NOTIFICATION_BADGING_URI, + mNotificationSettingsChangedListener); + mSettingsCache.dispatchOnChange(NOTIFICATION_BADGING_URI); } public LauncherAppState(Context context, @Nullable String iconCacheFileName) { @@ -166,8 +168,9 @@ public class LauncherAppState { } CustomWidgetManager.INSTANCE.get(mContext).setWidgetRefreshCallback(null); - if (mNotificationDotsObserver != null) { - mNotificationDotsObserver.unregister(); + if (mSettingsCache != null) { + mSettingsCache.unregister(NOTIFICATION_BADGING_URI, + mNotificationSettingsChangedListener); } } diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java index 059ad18971..2905dc322a 100644 --- a/src/com/android/launcher3/notification/NotificationListener.java +++ b/src/com/android/launcher3/notification/NotificationListener.java @@ -16,9 +16,9 @@ package com.android.launcher3.notification; +import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; -import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver; import android.annotation.TargetApi; import android.app.Notification; @@ -37,8 +37,8 @@ import androidx.annotation.AnyThread; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; +import com.android.launcher3.util.SettingsCache; import com.android.launcher3.util.PackageUserKey; -import com.android.launcher3.util.SecureSettingsObserver; import java.util.ArrayList; import java.util.Arrays; @@ -81,7 +81,8 @@ public class NotificationListener extends NotificationListenerService { /** The last notification key that was dismissed from launcher UI */ private String mLastKeyDismissedByLauncher; - private SecureSettingsObserver mNotificationDotsObserver; + private SettingsCache mSettingsCache; + private SettingsCache.OnChangeListener mNotificationSettingsChangedListener; public NotificationListener() { mWorkerHandler = new Handler(MODEL_EXECUTOR.getLooper(), this::handleWorkerMessage); @@ -207,10 +208,12 @@ public class NotificationListener extends NotificationListenerService { super.onListenerConnected(); sIsConnected = true; - mNotificationDotsObserver = - newNotificationSettingsObserver(this, this::onNotificationSettingsChanged); - mNotificationDotsObserver.register(); - mNotificationDotsObserver.dispatchOnChange(); + // Register an observer to rebind the notification listener when dots are re-enabled. + mSettingsCache = SettingsCache.INSTANCE.get(this); + mNotificationSettingsChangedListener = this::onNotificationSettingsChanged; + mSettingsCache.register(NOTIFICATION_BADGING_URI, + mNotificationSettingsChangedListener); + mSettingsCache.dispatchOnChange(NOTIFICATION_BADGING_URI); onNotificationFullRefresh(); } @@ -229,7 +232,7 @@ public class NotificationListener extends NotificationListenerService { public void onListenerDisconnected() { super.onListenerDisconnected(); sIsConnected = false; - mNotificationDotsObserver.unregister(); + mSettingsCache.unregister(NOTIFICATION_BADGING_URI, mNotificationSettingsChangedListener); onNotificationFullRefresh(); } diff --git a/src/com/android/launcher3/settings/NotificationDotsPreference.java b/src/com/android/launcher3/settings/NotificationDotsPreference.java index a91303a529..a35416915f 100644 --- a/src/com/android/launcher3/settings/NotificationDotsPreference.java +++ b/src/com/android/launcher3/settings/NotificationDotsPreference.java @@ -35,14 +35,14 @@ import androidx.preference.PreferenceViewHolder; import com.android.launcher3.R; import com.android.launcher3.notification.NotificationListener; -import com.android.launcher3.util.SecureSettingsObserver; +import com.android.launcher3.util.SettingsCache; /** * A {@link Preference} for indicating notification dots status. * Also has utility methods for updating UI based on dots status changes. */ public class NotificationDotsPreference extends Preference - implements SecureSettingsObserver.OnChangeListener { + implements SettingsCache.OnChangeListener { private boolean mWidgetFrameVisible = false; diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java index 922425fca1..ac8dac5f7e 100644 --- a/src/com/android/launcher3/settings/SettingsActivity.java +++ b/src/com/android/launcher3/settings/SettingsActivity.java @@ -18,13 +18,13 @@ package com.android.launcher3.settings; import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS; +import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI; +import static com.android.launcher3.util.SettingsCache.NOTIFICATION_ENABLED_LISTENERS; import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY; import static com.android.launcher3.states.RotationHelper.getAllowRotationDefaultValue; -import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver; import android.content.SharedPreferences; import android.os.Bundle; -import android.provider.Settings; import android.text.TextUtils; import androidx.annotation.NonNull; @@ -45,8 +45,8 @@ import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.util.SettingsCache; import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; -import com.android.launcher3.util.SecureSettingsObserver; /** * Settings activity for Launcher. Currently implements the following setting: Allow rotation @@ -59,8 +59,6 @@ public class SettingsActivity extends FragmentActivity private static final String FLAGS_PREFERENCE_KEY = "flag_toggler"; private static final String NOTIFICATION_DOTS_PREFERENCE_KEY = "pref_icon_badging"; - /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */ - private static final String NOTIFICATION_ENABLED_LISTENERS = "enabled_notification_listeners"; public static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key"; public static final String EXTRA_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args"; @@ -126,10 +124,11 @@ public class SettingsActivity extends FragmentActivity */ public static class LauncherSettingsFragment extends PreferenceFragmentCompat { - private SecureSettingsObserver mNotificationDotsObserver; + private SettingsCache mSettingsCache; private String mHighLightKey; private boolean mPreferenceHighlighted = false; + private NotificationDotsPreference mNotificationSettingsChangedListener; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -177,14 +176,16 @@ public class SettingsActivity extends FragmentActivity } // Listen to system notification dot settings while this UI is active. - mNotificationDotsObserver = newNotificationSettingsObserver( - getActivity(), (NotificationDotsPreference) preference); - mNotificationDotsObserver.register(); + mSettingsCache = SettingsCache.INSTANCE.get(getActivity()); + mNotificationSettingsChangedListener = + ((NotificationDotsPreference) preference); + mSettingsCache.register(NOTIFICATION_BADGING_URI, + (NotificationDotsPreference) mNotificationSettingsChangedListener); // Also listen if notification permission changes - mNotificationDotsObserver.getResolver().registerContentObserver( - Settings.Secure.getUriFor(NOTIFICATION_ENABLED_LISTENERS), false, - mNotificationDotsObserver); - mNotificationDotsObserver.dispatchOnChange(); + mSettingsCache.register(NOTIFICATION_ENABLED_LISTENERS, + mNotificationSettingsChangedListener); + mSettingsCache.dispatchOnChange(NOTIFICATION_BADGING_URI); + mSettingsCache.dispatchOnChange(NOTIFICATION_ENABLED_LISTENERS); return true; case ALLOW_ROTATION_PREFERENCE_KEY: @@ -251,9 +252,11 @@ public class SettingsActivity extends FragmentActivity @Override public void onDestroy() { - if (mNotificationDotsObserver != null) { - mNotificationDotsObserver.unregister(); - mNotificationDotsObserver = null; + if (mSettingsCache != null) { + mSettingsCache.unregister(NOTIFICATION_BADGING_URI, + mNotificationSettingsChangedListener); + mSettingsCache.unregister(NOTIFICATION_ENABLED_LISTENERS, + mNotificationSettingsChangedListener); } super.onDestroy(); } diff --git a/src/com/android/launcher3/util/SecureSettingsObserver.java b/src/com/android/launcher3/util/SecureSettingsObserver.java deleted file mode 100644 index 9fe72ad80a..0000000000 --- a/src/com/android/launcher3/util/SecureSettingsObserver.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2017 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.launcher3.util; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.ContentObserver; -import android.os.Handler; -import android.provider.Settings; - -/** - * Utility class to listen for secure settings changes - */ -public class SecureSettingsObserver extends ContentObserver { - - /** Hidden field Settings.Secure.NOTIFICATION_BADGING */ - public static final String NOTIFICATION_BADGING = "notification_badging"; - /** Hidden field Settings.Secure.ONE_HANDED_MODE_ENABLED */ - public static final String ONE_HANDED_ENABLED = "one_handed_mode_enabled"; - /** Hidden field Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED */ - public static final String ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED = - "swipe_bottom_to_notification_enabled"; - - private final ContentResolver mResolver; - private final String mKeySetting; - private final int mDefaultValue; - private final OnChangeListener mOnChangeListener; - - public SecureSettingsObserver(ContentResolver resolver, OnChangeListener listener, - String keySetting, int defaultValue) { - super(new Handler()); - - mResolver = resolver; - mOnChangeListener = listener; - mKeySetting = keySetting; - mDefaultValue = defaultValue; - } - - @Override - public void onChange(boolean selfChange) { - mOnChangeListener.onSettingsChanged(getValue()); - } - - public boolean getValue() { - return Settings.Secure.getInt(mResolver, mKeySetting, mDefaultValue) == 1; - } - - public void register() { - mResolver.registerContentObserver(Settings.Secure.getUriFor(mKeySetting), false, this); - } - - public ContentResolver getResolver() { - return mResolver; - } - - public void dispatchOnChange() { - onChange(true); - } - - public void unregister() { - mResolver.unregisterContentObserver(this); - } - - public interface OnChangeListener { - void onSettingsChanged(boolean isEnabled); - } - - public static SecureSettingsObserver newNotificationSettingsObserver(Context context, - OnChangeListener listener) { - return new SecureSettingsObserver( - context.getContentResolver(), listener, NOTIFICATION_BADGING, 1); - } - - public static SecureSettingsObserver newOneHandedSettingsObserver(Context context, - OnChangeListener listener) { - return new SecureSettingsObserver( - context.getContentResolver(), listener, ONE_HANDED_ENABLED, 0); - } - - /** - * Constructs settings observer for {@link #ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED} - * preference. - */ - public static SecureSettingsObserver newSwipeToNotificationSettingsObserver(Context context, - OnChangeListener listener) { - return new SecureSettingsObserver( - context.getContentResolver(), listener, - ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 1); - } -} diff --git a/src/com/android/launcher3/util/SettingsCache.java b/src/com/android/launcher3/util/SettingsCache.java new file mode 100644 index 0000000000..22b4d38891 --- /dev/null +++ b/src/com/android/launcher3/util/SettingsCache.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2021 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.launcher3.util; + +import static android.provider.Settings.System.ACCELEROMETER_ROTATION; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * ContentObserver over Settings keys that also has a caching layer. + * Consumers can register for callbacks via {@link #register(Uri, OnChangeListener)} and + * {@link #unregister(Uri, OnChangeListener)} methods. + * + * This can be used as a normal cache without any listeners as well via the + * {@link #getValue(Uri, int)} and {@link #dispatchOnChange(Uri)} to update (and subsequently call + * get) + * + * The cache will be invalidated/updated through the normal + * {@link ContentObserver#onChange(boolean)} calls + * or can be force updated by calling {@link #dispatchOnChange(Uri)}. + * + * Cache will also be updated if a key queried is missing (even if it has no listeners registered). + */ +public class SettingsCache extends ContentObserver { + + /** Hidden field Settings.Secure.NOTIFICATION_BADGING */ + public static final Uri NOTIFICATION_BADGING_URI = + Settings.Secure.getUriFor("notification_badging"); + /** Hidden field Settings.Secure.ONE_HANDED_MODE_ENABLED */ + public static final String ONE_HANDED_ENABLED = "one_handed_mode_enabled"; + /** Hidden field Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED */ + public static final String ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED = + "swipe_bottom_to_notification_enabled"; + /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */ + public static final Uri NOTIFICATION_ENABLED_LISTENERS = + Settings.Secure.getUriFor("enabled_notification_listeners"); + public static final Uri ROTATION_SETTING_URI = + Settings.System.getUriFor(ACCELEROMETER_ROTATION); + + private static final String SYSTEM_URI_PREFIX = Settings.System.CONTENT_URI.toString(); + + /** + * Caches the last seen value for registered keys. + */ + private Map mKeyCache = new ConcurrentHashMap<>(); + private final Map> mListenerMap = new HashMap<>(); + protected final ContentResolver mResolver; + + + /** + * Singleton instance + */ + public static MainThreadInitializedObject INSTANCE = + new MainThreadInitializedObject<>(SettingsCache::new); + + private SettingsCache(Context context) { + super(new Handler()); + mResolver = context.getContentResolver(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + // We use default of 1, but if we're getting an onChange call, can assume a non-default + // value will exist + boolean newVal = updateValue(uri, 1 /* Effectively Unused */); + if (!mListenerMap.containsKey(uri)) { + return; + } + + for (OnChangeListener listener : mListenerMap.get(uri)) { + listener.onSettingsChanged(newVal); + } + } + + /** + * Returns the value for this classes key from the cache. If not in cache, will call + * {@link #updateValue(Uri, int)} to fetch. + */ + public boolean getValue(Uri keySetting, int defaultValue) { + if (mKeyCache.containsKey(keySetting)) { + return mKeyCache.get(keySetting); + } else { + return updateValue(keySetting, defaultValue); + } + } + + /** + * Does not de-dupe if you add same listeners for the same key multiple times. + * Unregister once complete using {@link #unregister(Uri, OnChangeListener)} + */ + public void register(Uri uri, OnChangeListener changeListener) { + if (mListenerMap.containsKey(uri)) { + mListenerMap.get(uri).add(changeListener); + } else { + CopyOnWriteArrayList l = new CopyOnWriteArrayList<>(); + l.add(changeListener); + mListenerMap.put(uri, l); + mResolver.registerContentObserver(uri, false, this); + } + } + + private boolean updateValue(Uri keyUri, int defaultValue) { + String key = keyUri.getLastPathSegment(); + boolean newVal; + if (keyUri.toString().startsWith(SYSTEM_URI_PREFIX)) { + newVal = Settings.System.getInt(mResolver, key, defaultValue) == 1; + } else { // SETTING_SECURE + newVal = Settings.Secure.getInt(mResolver, key, defaultValue) == 1; + } + + mKeyCache.put(keyUri, newVal); + return newVal; + } + + /** + * Force update a change for a given URI and have all listeners for that URI receive callbacks + * even if the value is unchanged. + */ + public void dispatchOnChange(Uri uri) { + onChange(true, uri); + } + + /** + * Call to stop receiving updates on the given {@param listener}. + * This Uri/Listener pair must correspond to the same pair called with for + * {@link #register(Uri, OnChangeListener)} + */ + public void unregister(Uri uri, OnChangeListener listener) { + List listenersToRemoveFrom = mListenerMap.get(uri); + if (!listenersToRemoveFrom.contains(listener)) { + return; + } + + listenersToRemoveFrom.remove(listener); + if (listenersToRemoveFrom.isEmpty()) { + mListenerMap.remove(uri); + } + } + + /** + * Don't use this. Ever. + * @param keyCache Cache to replace {@link #mKeyCache} + */ + @VisibleForTesting + void setKeyCache(Map keyCache) { + mKeyCache = keyCache; + } + + public interface OnChangeListener { + void onSettingsChanged(boolean isEnabled); + } +} diff --git a/tests/Android.mk b/tests/Android.mk index 3d9077d4bd..43d51fca01 100644 --- a/tests/Android.mk +++ b/tests/Android.mk @@ -32,7 +32,6 @@ else LOCAL_SRC_FILES := $(call all-java-files-under, tapl) \ ../src/com/android/launcher3/ResourceUtils.java \ - ../src/com/android/launcher3/util/SecureSettingsObserver.java \ ../src/com/android/launcher3/testing/TestProtocol.java endif