1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.util; 18 19 import static android.provider.Settings.System.ACCELEROMETER_ROTATION; 20 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.database.ContentObserver; 24 import android.net.Uri; 25 import android.os.Handler; 26 import android.provider.Settings; 27 28 import java.util.HashMap; 29 import java.util.List; 30 import java.util.Map; 31 import java.util.concurrent.ConcurrentHashMap; 32 import java.util.concurrent.CopyOnWriteArrayList; 33 34 /** 35 * ContentObserver over Settings keys that also has a caching layer. 36 * Consumers can register for callbacks via {@link #register(Uri, OnChangeListener)} and 37 * {@link #unregister(Uri, OnChangeListener)} methods. 38 * 39 * This can be used as a normal cache without any listeners as well via the 40 * {@link #getValue(Uri, int)} and {@link #onChange)} to update (and subsequently call 41 * get) 42 * 43 * The cache will be invalidated/updated through the normal 44 * {@link ContentObserver#onChange(boolean)} calls 45 * 46 * Cache will also be updated if a key queried is missing (even if it has no listeners registered). 47 */ 48 public class SettingsCache extends ContentObserver implements SafeCloseable { 49 50 /** Hidden field Settings.Secure.NOTIFICATION_BADGING */ 51 public static final Uri NOTIFICATION_BADGING_URI = 52 Settings.Secure.getUriFor("notification_badging"); 53 /** Hidden field Settings.Secure.ONE_HANDED_MODE_ENABLED */ 54 public static final String ONE_HANDED_ENABLED = "one_handed_mode_enabled"; 55 /** Hidden field Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED */ 56 public static final String ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED = 57 "swipe_bottom_to_notification_enabled"; 58 /** Hidden field Settings.Secure.HIDE_PRIVATESPACE_ENTRY_POINT */ 59 public static final Uri PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI = 60 Settings.Secure.getUriFor("hide_privatespace_entry_point"); 61 public static final Uri ROTATION_SETTING_URI = 62 Settings.System.getUriFor(ACCELEROMETER_ROTATION); 63 /** Hidden field {@link Settings.System#TOUCHPAD_NATURAL_SCROLLING}. */ 64 public static final Uri TOUCHPAD_NATURAL_SCROLLING = Settings.System.getUriFor( 65 "touchpad_natural_scrolling"); 66 67 private static final String SYSTEM_URI_PREFIX = Settings.System.CONTENT_URI.toString(); 68 private static final String GLOBAL_URI_PREFIX = Settings.Global.CONTENT_URI.toString(); 69 70 /** 71 * Caches the last seen value for registered keys. 72 */ 73 private Map<Uri, Boolean> mKeyCache = new ConcurrentHashMap<>(); 74 private final Map<Uri, CopyOnWriteArrayList<OnChangeListener>> mListenerMap = new HashMap<>(); 75 protected final ContentResolver mResolver; 76 77 /** 78 * Singleton instance 79 */ 80 public static MainThreadInitializedObject<SettingsCache> INSTANCE = 81 new MainThreadInitializedObject<>(SettingsCache::new); 82 SettingsCache(Context context)83 private SettingsCache(Context context) { 84 super(new Handler()); 85 mResolver = context.getContentResolver(); 86 } 87 88 @Override close()89 public void close() { 90 mResolver.unregisterContentObserver(this); 91 } 92 93 @Override onChange(boolean selfChange, Uri uri)94 public void onChange(boolean selfChange, Uri uri) { 95 // We use default of 1, but if we're getting an onChange call, can assume a non-default 96 // value will exist 97 boolean newVal = updateValue(uri, 1 /* Effectively Unused */); 98 if (!mListenerMap.containsKey(uri)) { 99 return; 100 } 101 102 for (OnChangeListener listener : mListenerMap.get(uri)) { 103 listener.onSettingsChanged(newVal); 104 } 105 } 106 107 /** 108 * Returns the value for this classes key from the cache. If not in cache, will call 109 * {@link #updateValue(Uri, int)} to fetch. 110 */ getValue(Uri keySetting)111 public boolean getValue(Uri keySetting) { 112 return getValue(keySetting, 1); 113 } 114 115 /** 116 * Returns the value for this classes key from the cache. If not in cache, will call 117 * {@link #updateValue(Uri, int)} to fetch. 118 */ getValue(Uri keySetting, int defaultValue)119 public boolean getValue(Uri keySetting, int defaultValue) { 120 if (mKeyCache.containsKey(keySetting)) { 121 return mKeyCache.get(keySetting); 122 } else { 123 return updateValue(keySetting, defaultValue); 124 } 125 } 126 127 /** 128 * Does not de-dupe if you add same listeners for the same key multiple times. 129 * Unregister once complete using {@link #unregister(Uri, OnChangeListener)} 130 */ register(Uri uri, OnChangeListener changeListener)131 public void register(Uri uri, OnChangeListener changeListener) { 132 if (mListenerMap.containsKey(uri)) { 133 mListenerMap.get(uri).add(changeListener); 134 } else { 135 CopyOnWriteArrayList<OnChangeListener> l = new CopyOnWriteArrayList<>(); 136 l.add(changeListener); 137 mListenerMap.put(uri, l); 138 mResolver.registerContentObserver(uri, false, this); 139 } 140 } 141 updateValue(Uri keyUri, int defaultValue)142 private boolean updateValue(Uri keyUri, int defaultValue) { 143 String key = keyUri.getLastPathSegment(); 144 boolean newVal; 145 if (keyUri.toString().startsWith(SYSTEM_URI_PREFIX)) { 146 newVal = Settings.System.getInt(mResolver, key, defaultValue) == 1; 147 } else if (keyUri.toString().startsWith(GLOBAL_URI_PREFIX)) { 148 newVal = Settings.Global.getInt(mResolver, key, defaultValue) == 1; 149 } else { // SETTING_SECURE 150 newVal = Settings.Secure.getInt(mResolver, key, defaultValue) == 1; 151 } 152 153 mKeyCache.put(keyUri, newVal); 154 return newVal; 155 } 156 157 /** 158 * Call to stop receiving updates on the given {@param listener}. 159 * This Uri/Listener pair must correspond to the same pair called with for 160 * {@link #register(Uri, OnChangeListener)} 161 */ unregister(Uri uri, OnChangeListener listener)162 public void unregister(Uri uri, OnChangeListener listener) { 163 List<OnChangeListener> listenersToRemoveFrom = mListenerMap.get(uri); 164 if (listenersToRemoveFrom != null) { 165 listenersToRemoveFrom.remove(listener); 166 } 167 } 168 169 public interface OnChangeListener { onSettingsChanged(boolean isEnabled)170 void onSettingsChanged(boolean isEnabled); 171 } 172 } 173