Loading app/src/main/java/foundation/e/blisslauncher/BlissLauncher.java +17 −0 Original line number Diff line number Diff line package foundation.e.blisslauncher; import static foundation.e.blisslauncher.util.SettingsCache.NOTIFICATION_BADGING_URI; import android.app.Application; import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Context; import foundation.e.blisslauncher.core.DeviceProfile; Loading @@ -9,6 +12,8 @@ import foundation.e.blisslauncher.core.IconsHandler; import foundation.e.blisslauncher.core.blur.BlurWallpaperProvider; import foundation.e.blisslauncher.core.customviews.WidgetHost; import foundation.e.blisslauncher.features.launcher.AppProvider; import foundation.e.blisslauncher.features.notification.NotificationService; import foundation.e.blisslauncher.util.SettingsCache; public class BlissLauncher extends Application { private IconsHandler iconsPackHandler; Loading @@ -29,6 +34,18 @@ public class BlissLauncher extends Application { connectAppProvider(); BlurWallpaperProvider.Companion.getInstance(this); SettingsCache settingsCache = SettingsCache.INSTANCE.get(this); SettingsCache.OnChangeListener notificationLister = this::onNotificationSettingsChanged; settingsCache.register(NOTIFICATION_BADGING_URI, notificationLister); onNotificationSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI)); } private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) { if (areNotificationDotsEnabled) { NotificationService.requestRebind(new ComponentName( this, NotificationService.class)); } } public static BlissLauncher getApplication(Context context) { Loading app/src/main/java/foundation/e/blisslauncher/features/notification/NotificationService.java +44 −2 Original line number Diff line number Diff line package foundation.e.blisslauncher.features.notification; import static foundation.e.blisslauncher.util.SettingsCache.NOTIFICATION_BADGING_URI; import android.content.Intent; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import java.util.Collections; import foundation.e.blisslauncher.core.utils.ListUtil; import foundation.e.blisslauncher.util.SettingsCache; /** * Created by falcon on 14/3/18. Loading @@ -12,31 +17,68 @@ import foundation.e.blisslauncher.core.utils.ListUtil; public class NotificationService extends NotificationListenerService { private static boolean sIsConnected = false; NotificationRepository mNotificationRepository; private boolean mDotsEnabled; private SettingsCache mSettingsCache; private SettingsCache.OnChangeListener mNotificationSettingsChangedListener; @Override public void onCreate() { super.onCreate(); mNotificationRepository = NotificationRepository.getNotificationRepository(); // 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); onNotificationSettingsChanged(mSettingsCache.getValue(NOTIFICATION_BADGING_URI)); } @Override public void onDestroy() { super.onDestroy(); mSettingsCache.unregister(NOTIFICATION_BADGING_URI, mNotificationSettingsChangedListener); mNotificationRepository.updateNotification(Collections.emptyList()); } private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) { mDotsEnabled = areNotificationDotsEnabled; if (!areNotificationDotsEnabled && sIsConnected) { requestUnbind(); updateNotifications(); } } @Override public void onListenerConnected() { mNotificationRepository.updateNotification(ListUtil.asSafeList(getActiveNotifications())); sIsConnected = true; updateNotifications(); } @Override public void onListenerDisconnected() { sIsConnected = false; } @Override public void onNotificationPosted(StatusBarNotification sbn) { mNotificationRepository.updateNotification(ListUtil.asSafeList(getActiveNotifications())); updateNotifications(); } @Override public void onNotificationRemoved(StatusBarNotification sbn) { updateNotifications(); } private void updateNotifications() { if (!mDotsEnabled) { mNotificationRepository.updateNotification(Collections.emptyList()); return; } mNotificationRepository.updateNotification(ListUtil.asSafeList(getActiveNotifications())); } Loading app/src/main/java/foundation/e/blisslauncher/util/LooperExecutor.java 0 → 100644 +118 −0 Original line number Diff line number Diff line /* * 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 foundation.e.blisslauncher.util; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Process; import java.util.List; import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.TimeUnit; /** * Extension of {@link AbstractExecutorService} which executed on a provided looper. */ public class LooperExecutor extends AbstractExecutorService { private final Handler mHandler; public LooperExecutor(Looper looper) { mHandler = new Handler(looper); } public Handler getHandler() { return mHandler; } @Override public void execute(Runnable runnable) { if (getHandler().getLooper() == Looper.myLooper()) { runnable.run(); } else { getHandler().post(runnable); } } /** * Same as execute, but never runs the action inline. */ public void post(Runnable runnable) { getHandler().post(runnable); } /** * Not supported and throws an exception when used. */ @Override @Deprecated public void shutdown() { throw new UnsupportedOperationException(); } /** * Not supported and throws an exception when used. */ @Override @Deprecated public List<Runnable> shutdownNow() { throw new UnsupportedOperationException(); } @Override public boolean isShutdown() { return false; } @Override public boolean isTerminated() { return false; } /** * Not supported and throws an exception when used. */ @Override @Deprecated public boolean awaitTermination(long l, TimeUnit timeUnit) { throw new UnsupportedOperationException(); } /** * Returns the thread for this executor */ public Thread getThread() { return getHandler().getLooper().getThread(); } /** * Returns the looper for this executor */ public Looper getLooper() { return getHandler().getLooper(); } /** * Set the priority of a thread, based on Linux priorities. * @param priority Linux priority level, from -20 for highest scheduling priority * to 19 for lowest scheduling priority. * @see Process#setThreadPriority(int, int) */ public void setThreadPriority(int priority) { Process.setThreadPriority(((HandlerThread) getThread()).getThreadId(), priority); } } app/src/main/java/foundation/e/blisslauncher/util/MainThreadInitializedObject.java 0 → 100644 +65 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 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. * * Modifications copyright 2021, Lawnchair */ package foundation.e.blisslauncher.util; import android.content.Context; import android.os.Looper; import java.util.concurrent.ExecutionException; /** * Utility class for defining singletons which are initiated on main thread. */ public class MainThreadInitializedObject<T> { private static final LooperExecutor MAIN_EXECUTOR = new LooperExecutor(Looper.getMainLooper()); private final ObjectProvider<T> mProvider; private T mValue; public MainThreadInitializedObject(ObjectProvider<T> provider) { mProvider = provider; } public T get(Context context) { if (mValue == null) { if (Looper.myLooper() == Looper.getMainLooper()) { mValue = mProvider.get(context.getApplicationContext()); onPostInit(context); } else { try { return MAIN_EXECUTOR.submit(() -> get(context)).get(); } catch (InterruptedException|ExecutionException e) { throw new RuntimeException(e); } } } return mValue; } protected void onPostInit(Context context) { } public T getNoCreate() { return mValue; } public interface ObjectProvider<T> { T get(Context context); } } app/src/main/java/foundation/e/blisslauncher/util/SettingsCache.java 0 → 100644 +179 −0 Original line number Diff line number Diff line /* * 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 foundation.e.blisslauncher.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 #onChange)} to update (and subsequently call * get) * * The cache will be invalidated/updated through the normal * {@link ContentObserver#onChange(boolean)} calls * * Cache will also be updated if a key queried is missing (even if it has no listeners registered). */ public class SettingsCache extends ContentObserver implements AutoCloseable { /** 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"; 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<Uri, Boolean> mKeyCache = new ConcurrentHashMap<>(); private final Map<Uri, CopyOnWriteArrayList<OnChangeListener>> mListenerMap = new HashMap<>(); protected final ContentResolver mResolver; /** * Singleton instance */ public static MainThreadInitializedObject<SettingsCache> INSTANCE = new MainThreadInitializedObject<>(SettingsCache::new); private SettingsCache(Context context) { super(new Handler()); mResolver = context.getContentResolver(); } @Override public void close() { mResolver.unregisterContentObserver(this); } @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) { return getValue(keySetting, 1); } /** * 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<OnChangeListener> 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; } /** * 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<OnChangeListener> 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<Uri, Boolean> keyCache) { mKeyCache = keyCache; } public interface OnChangeListener { void onSettingsChanged(boolean isEnabled); } } Loading
app/src/main/java/foundation/e/blisslauncher/BlissLauncher.java +17 −0 Original line number Diff line number Diff line package foundation.e.blisslauncher; import static foundation.e.blisslauncher.util.SettingsCache.NOTIFICATION_BADGING_URI; import android.app.Application; import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Context; import foundation.e.blisslauncher.core.DeviceProfile; Loading @@ -9,6 +12,8 @@ import foundation.e.blisslauncher.core.IconsHandler; import foundation.e.blisslauncher.core.blur.BlurWallpaperProvider; import foundation.e.blisslauncher.core.customviews.WidgetHost; import foundation.e.blisslauncher.features.launcher.AppProvider; import foundation.e.blisslauncher.features.notification.NotificationService; import foundation.e.blisslauncher.util.SettingsCache; public class BlissLauncher extends Application { private IconsHandler iconsPackHandler; Loading @@ -29,6 +34,18 @@ public class BlissLauncher extends Application { connectAppProvider(); BlurWallpaperProvider.Companion.getInstance(this); SettingsCache settingsCache = SettingsCache.INSTANCE.get(this); SettingsCache.OnChangeListener notificationLister = this::onNotificationSettingsChanged; settingsCache.register(NOTIFICATION_BADGING_URI, notificationLister); onNotificationSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI)); } private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) { if (areNotificationDotsEnabled) { NotificationService.requestRebind(new ComponentName( this, NotificationService.class)); } } public static BlissLauncher getApplication(Context context) { Loading
app/src/main/java/foundation/e/blisslauncher/features/notification/NotificationService.java +44 −2 Original line number Diff line number Diff line package foundation.e.blisslauncher.features.notification; import static foundation.e.blisslauncher.util.SettingsCache.NOTIFICATION_BADGING_URI; import android.content.Intent; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import java.util.Collections; import foundation.e.blisslauncher.core.utils.ListUtil; import foundation.e.blisslauncher.util.SettingsCache; /** * Created by falcon on 14/3/18. Loading @@ -12,31 +17,68 @@ import foundation.e.blisslauncher.core.utils.ListUtil; public class NotificationService extends NotificationListenerService { private static boolean sIsConnected = false; NotificationRepository mNotificationRepository; private boolean mDotsEnabled; private SettingsCache mSettingsCache; private SettingsCache.OnChangeListener mNotificationSettingsChangedListener; @Override public void onCreate() { super.onCreate(); mNotificationRepository = NotificationRepository.getNotificationRepository(); // 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); onNotificationSettingsChanged(mSettingsCache.getValue(NOTIFICATION_BADGING_URI)); } @Override public void onDestroy() { super.onDestroy(); mSettingsCache.unregister(NOTIFICATION_BADGING_URI, mNotificationSettingsChangedListener); mNotificationRepository.updateNotification(Collections.emptyList()); } private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) { mDotsEnabled = areNotificationDotsEnabled; if (!areNotificationDotsEnabled && sIsConnected) { requestUnbind(); updateNotifications(); } } @Override public void onListenerConnected() { mNotificationRepository.updateNotification(ListUtil.asSafeList(getActiveNotifications())); sIsConnected = true; updateNotifications(); } @Override public void onListenerDisconnected() { sIsConnected = false; } @Override public void onNotificationPosted(StatusBarNotification sbn) { mNotificationRepository.updateNotification(ListUtil.asSafeList(getActiveNotifications())); updateNotifications(); } @Override public void onNotificationRemoved(StatusBarNotification sbn) { updateNotifications(); } private void updateNotifications() { if (!mDotsEnabled) { mNotificationRepository.updateNotification(Collections.emptyList()); return; } mNotificationRepository.updateNotification(ListUtil.asSafeList(getActiveNotifications())); } Loading
app/src/main/java/foundation/e/blisslauncher/util/LooperExecutor.java 0 → 100644 +118 −0 Original line number Diff line number Diff line /* * 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 foundation.e.blisslauncher.util; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Process; import java.util.List; import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.TimeUnit; /** * Extension of {@link AbstractExecutorService} which executed on a provided looper. */ public class LooperExecutor extends AbstractExecutorService { private final Handler mHandler; public LooperExecutor(Looper looper) { mHandler = new Handler(looper); } public Handler getHandler() { return mHandler; } @Override public void execute(Runnable runnable) { if (getHandler().getLooper() == Looper.myLooper()) { runnable.run(); } else { getHandler().post(runnable); } } /** * Same as execute, but never runs the action inline. */ public void post(Runnable runnable) { getHandler().post(runnable); } /** * Not supported and throws an exception when used. */ @Override @Deprecated public void shutdown() { throw new UnsupportedOperationException(); } /** * Not supported and throws an exception when used. */ @Override @Deprecated public List<Runnable> shutdownNow() { throw new UnsupportedOperationException(); } @Override public boolean isShutdown() { return false; } @Override public boolean isTerminated() { return false; } /** * Not supported and throws an exception when used. */ @Override @Deprecated public boolean awaitTermination(long l, TimeUnit timeUnit) { throw new UnsupportedOperationException(); } /** * Returns the thread for this executor */ public Thread getThread() { return getHandler().getLooper().getThread(); } /** * Returns the looper for this executor */ public Looper getLooper() { return getHandler().getLooper(); } /** * Set the priority of a thread, based on Linux priorities. * @param priority Linux priority level, from -20 for highest scheduling priority * to 19 for lowest scheduling priority. * @see Process#setThreadPriority(int, int) */ public void setThreadPriority(int priority) { Process.setThreadPriority(((HandlerThread) getThread()).getThreadId(), priority); } }
app/src/main/java/foundation/e/blisslauncher/util/MainThreadInitializedObject.java 0 → 100644 +65 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 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. * * Modifications copyright 2021, Lawnchair */ package foundation.e.blisslauncher.util; import android.content.Context; import android.os.Looper; import java.util.concurrent.ExecutionException; /** * Utility class for defining singletons which are initiated on main thread. */ public class MainThreadInitializedObject<T> { private static final LooperExecutor MAIN_EXECUTOR = new LooperExecutor(Looper.getMainLooper()); private final ObjectProvider<T> mProvider; private T mValue; public MainThreadInitializedObject(ObjectProvider<T> provider) { mProvider = provider; } public T get(Context context) { if (mValue == null) { if (Looper.myLooper() == Looper.getMainLooper()) { mValue = mProvider.get(context.getApplicationContext()); onPostInit(context); } else { try { return MAIN_EXECUTOR.submit(() -> get(context)).get(); } catch (InterruptedException|ExecutionException e) { throw new RuntimeException(e); } } } return mValue; } protected void onPostInit(Context context) { } public T getNoCreate() { return mValue; } public interface ObjectProvider<T> { T get(Context context); } }
app/src/main/java/foundation/e/blisslauncher/util/SettingsCache.java 0 → 100644 +179 −0 Original line number Diff line number Diff line /* * 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 foundation.e.blisslauncher.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 #onChange)} to update (and subsequently call * get) * * The cache will be invalidated/updated through the normal * {@link ContentObserver#onChange(boolean)} calls * * Cache will also be updated if a key queried is missing (even if it has no listeners registered). */ public class SettingsCache extends ContentObserver implements AutoCloseable { /** 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"; 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<Uri, Boolean> mKeyCache = new ConcurrentHashMap<>(); private final Map<Uri, CopyOnWriteArrayList<OnChangeListener>> mListenerMap = new HashMap<>(); protected final ContentResolver mResolver; /** * Singleton instance */ public static MainThreadInitializedObject<SettingsCache> INSTANCE = new MainThreadInitializedObject<>(SettingsCache::new); private SettingsCache(Context context) { super(new Handler()); mResolver = context.getContentResolver(); } @Override public void close() { mResolver.unregisterContentObserver(this); } @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) { return getValue(keySetting, 1); } /** * 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<OnChangeListener> 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; } /** * 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<OnChangeListener> 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<Uri, Boolean> keyCache) { mKeyCache = keyCache; } public interface OnChangeListener { void onSettingsChanged(boolean isEnabled); } }