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 androidx.annotation.VisibleForTesting; 29 30 import java.util.HashMap; 31 import java.util.List; 32 import java.util.Map; 33 import java.util.concurrent.ConcurrentHashMap; 34 import java.util.concurrent.CopyOnWriteArrayList; 35 36 /** 37 * ContentObserver over Settings keys that also has a caching layer. 38 * Consumers can register for callbacks via {@link #register(Uri, OnChangeListener)} and 39 * {@link #unregister(Uri, OnChangeListener)} methods. 40 * 41 * This can be used as a normal cache without any listeners as well via the 42 * {@link #getValue(Uri, int)} and {@link #onChange)} to update (and subsequently call 43 * get) 44 * 45 * The cache will be invalidated/updated through the normal 46 * {@link ContentObserver#onChange(boolean)} calls 47 * 48 * Cache will also be updated if a key queried is missing (even if it has no listeners registered). 49 */ 50 public class SettingsCache extends ContentObserver implements SafeCloseable { 51 52 /** Hidden field Settings.Secure.NOTIFICATION_BADGING */ 53 public static final Uri NOTIFICATION_BADGING_URI = 54 Settings.Secure.getUriFor("notification_badging"); 55 /** Hidden field Settings.Secure.ONE_HANDED_MODE_ENABLED */ 56 public static final String ONE_HANDED_ENABLED = "one_handed_mode_enabled"; 57 /** Hidden field Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED */ 58 public static final String ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED = 59 "swipe_bottom_to_notification_enabled"; 60 public static final Uri ROTATION_SETTING_URI = 61 Settings.System.getUriFor(ACCELEROMETER_ROTATION); 62 63 private static final String SYSTEM_URI_PREFIX = Settings.System.CONTENT_URI.toString(); 64 65 /** 66 * Caches the last seen value for registered keys. 67 */ 68 private Map<Uri, Boolean> mKeyCache = new ConcurrentHashMap<>(); 69 private final Map<Uri, CopyOnWriteArrayList<OnChangeListener>> mListenerMap = new HashMap<>(); 70 protected final ContentResolver mResolver; 71 72 /** 73 * Singleton instance 74 */ 75 public static MainThreadInitializedObject<SettingsCache> INSTANCE = 76 new MainThreadInitializedObject<>(SettingsCache::new); 77 SettingsCache(Context context)78 private SettingsCache(Context context) { 79 super(new Handler()); 80 mResolver = context.getContentResolver(); 81 } 82 83 @Override close()84 public void close() { 85 mResolver.unregisterContentObserver(this); 86 } 87 88 @Override onChange(boolean selfChange, Uri uri)89 public void onChange(boolean selfChange, Uri uri) { 90 // We use default of 1, but if we're getting an onChange call, can assume a non-default 91 // value will exist 92 boolean newVal = updateValue(uri, 1 /* Effectively Unused */); 93 if (!mListenerMap.containsKey(uri)) { 94 return; 95 } 96 97 for (OnChangeListener listener : mListenerMap.get(uri)) { 98 listener.onSettingsChanged(newVal); 99 } 100 } 101 102 /** 103 * Returns the value for this classes key from the cache. If not in cache, will call 104 * {@link #updateValue(Uri, int)} to fetch. 105 */ getValue(Uri keySetting)106 public boolean getValue(Uri keySetting) { 107 return getValue(keySetting, 1); 108 } 109 110 /** 111 * Returns the value for this classes key from the cache. If not in cache, will call 112 * {@link #updateValue(Uri, int)} to fetch. 113 */ getValue(Uri keySetting, int defaultValue)114 public boolean getValue(Uri keySetting, int defaultValue) { 115 if (mKeyCache.containsKey(keySetting)) { 116 return mKeyCache.get(keySetting); 117 } else { 118 return updateValue(keySetting, defaultValue); 119 } 120 } 121 122 /** 123 * Does not de-dupe if you add same listeners for the same key multiple times. 124 * Unregister once complete using {@link #unregister(Uri, OnChangeListener)} 125 */ register(Uri uri, OnChangeListener changeListener)126 public void register(Uri uri, OnChangeListener changeListener) { 127 if (mListenerMap.containsKey(uri)) { 128 mListenerMap.get(uri).add(changeListener); 129 } else { 130 CopyOnWriteArrayList<OnChangeListener> l = new CopyOnWriteArrayList<>(); 131 l.add(changeListener); 132 mListenerMap.put(uri, l); 133 mResolver.registerContentObserver(uri, false, this); 134 } 135 } 136 updateValue(Uri keyUri, int defaultValue)137 private boolean updateValue(Uri keyUri, int defaultValue) { 138 String key = keyUri.getLastPathSegment(); 139 boolean newVal; 140 if (keyUri.toString().startsWith(SYSTEM_URI_PREFIX)) { 141 newVal = Settings.System.getInt(mResolver, key, defaultValue) == 1; 142 } else { // SETTING_SECURE 143 newVal = Settings.Secure.getInt(mResolver, key, defaultValue) == 1; 144 } 145 146 mKeyCache.put(keyUri, newVal); 147 return newVal; 148 } 149 150 /** 151 * Call to stop receiving updates on the given {@param listener}. 152 * This Uri/Listener pair must correspond to the same pair called with for 153 * {@link #register(Uri, OnChangeListener)} 154 */ unregister(Uri uri, OnChangeListener listener)155 public void unregister(Uri uri, OnChangeListener listener) { 156 List<OnChangeListener> listenersToRemoveFrom = mListenerMap.get(uri); 157 if (!listenersToRemoveFrom.contains(listener)) { 158 return; 159 } 160 161 listenersToRemoveFrom.remove(listener); 162 if (listenersToRemoveFrom.isEmpty()) { 163 mListenerMap.remove(uri); 164 } 165 } 166 167 /** 168 * Don't use this. Ever. 169 * @param keyCache Cache to replace {@link #mKeyCache} 170 */ 171 @VisibleForTesting setKeyCache(Map<Uri, Boolean> keyCache)172 void setKeyCache(Map<Uri, Boolean> keyCache) { 173 mKeyCache = keyCache; 174 } 175 176 public interface OnChangeListener { onSettingsChanged(boolean isEnabled)177 void onSettingsChanged(boolean isEnabled); 178 } 179 } 180