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