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