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.car.settings.qc;
18 
19 import static android.content.ContentResolver.NOTIFY_NO_DELAY;
20 
21 import android.annotation.MainThread;
22 import android.annotation.Nullable;
23 import android.content.Context;
24 import android.net.Uri;
25 import android.os.Handler;
26 import android.os.HandlerThread;
27 import android.os.Looper;
28 import android.os.Message;
29 import android.os.Process;
30 import android.os.SystemClock;
31 import android.os.UserManager;
32 import android.util.ArrayMap;
33 
34 import com.android.car.settings.common.Logger;
35 
36 import java.io.Closeable;
37 import java.io.IOException;
38 import java.lang.reflect.InvocationTargetException;
39 import java.util.Collections;
40 import java.util.Map;
41 
42 /**
43  * Base background worker class to allow for CarSetting Quick Control items to work with data that
44  * can change continuously.
45  * @param <E> {@link SettingsQCItem} class that the worker is operating on.
46  */
47 public abstract class SettingsQCBackgroundWorker<E extends SettingsQCItem> implements Closeable {
48 
49     private static final Logger LOG = new Logger(SettingsQCBackgroundWorker.class);
50 
51     private static final long QC_UPDATE_THROTTLE_INTERVAL = 300L;
52 
53     private static final Map<Uri, SettingsQCBackgroundWorker> LIVE_WORKERS = new ArrayMap<>();
54 
55     private final Context mContext;
56     private final Uri mUri;
57     private SettingsQCItem mQCItem;
58 
SettingsQCBackgroundWorker(Context context, Uri uri)59     protected SettingsQCBackgroundWorker(Context context, Uri uri) {
60         mContext = context;
61         mUri = uri;
62     }
63 
getUri()64     protected Uri getUri() {
65         return mUri;
66     }
67 
getContext()68     protected Context getContext() {
69         return mContext;
70     }
71 
getQCItem()72     protected E getQCItem() {
73         return (E) mQCItem;
74     }
75 
setQCItem(SettingsQCItem item)76     void setQCItem(SettingsQCItem item) {
77         mQCItem = item;
78     }
79 
80     /**
81      * Returns the singleton instance of {@link SettingsQCBackgroundWorker} for specified
82      * {@link Uri} if exists
83      */
84     @Nullable
getInstance(Uri uri)85     public static <T extends SettingsQCBackgroundWorker> T getInstance(Uri uri) {
86         return (T) LIVE_WORKERS.get(uri);
87     }
88 
89     /**
90      * Returns the singleton instance of {@link SettingsQCBackgroundWorker} for specified {@link
91      * SettingsQCItem}
92      */
getInstance(Context context, SettingsQCItem qcItem, Uri uri)93     static SettingsQCBackgroundWorker getInstance(Context context, SettingsQCItem qcItem, Uri uri) {
94         SettingsQCBackgroundWorker worker = getInstance(uri);
95         if (worker == null) {
96             Class<? extends SettingsQCBackgroundWorker> workerClass =
97                     qcItem.getBackgroundWorkerClass();
98             worker = createInstance(context.getApplicationContext(), uri, workerClass);
99             LIVE_WORKERS.put(uri, worker);
100         }
101         worker.setQCItem(qcItem);
102         return worker;
103     }
104 
createInstance(Context context, Uri uri, Class<? extends SettingsQCBackgroundWorker> clazz)105     private static SettingsQCBackgroundWorker createInstance(Context context, Uri uri,
106             Class<? extends SettingsQCBackgroundWorker> clazz) {
107         LOG.d("create instance: " + clazz);
108         try {
109             return clazz.getConstructor(Context.class, Uri.class).newInstance(context, uri);
110         } catch (NoSuchMethodException | IllegalAccessException | InstantiationException
111                 | InvocationTargetException e) {
112             throw new IllegalStateException(
113                     "Invalid qc background worker: " + clazz, e);
114         }
115     }
116 
shutdown()117     static void shutdown() {
118         for (SettingsQCBackgroundWorker worker : LIVE_WORKERS.values()) {
119             try {
120                 worker.close();
121             } catch (IOException e) {
122                 LOG.w("Shutting down worker failed", e);
123             }
124         }
125         LIVE_WORKERS.clear();
126     }
127 
shutdown(Uri uri)128     static void shutdown(Uri uri) {
129         SettingsQCBackgroundWorker worker = LIVE_WORKERS.get(uri);
130         if (worker != null) {
131             try {
132                 worker.close();
133             } catch (IOException e) {
134                 LOG.w("Shutting down worker failed", e);
135             }
136             LIVE_WORKERS.remove(uri);
137         }
138     }
139 
140     /**
141      * Called when the QCItem is subscribed to. This is the place to register callbacks or
142      * initialize scan tasks.
143      */
144     @MainThread
onQCItemSubscribe()145     protected abstract void onQCItemSubscribe();
146 
147     /**
148      * Called when the QCItem is unsubscribed from. This is the place to unregister callbacks or
149      * perform any final cleanup.
150      */
151     @MainThread
onQCItemUnsubscribe()152     protected abstract void onQCItemUnsubscribe();
153 
154     /**
155      * Notify that data was updated and attempt to sync changes to the QCItem.
156      */
notifyQCItemChange()157     protected final void notifyQCItemChange() {
158         NotifyQCItemChangeHandler.getInstance().updateQCItem(this);
159     }
160 
subscribe()161     void subscribe() {
162         onQCItemSubscribe();
163     }
164 
unsubscribe()165     void unsubscribe() {
166         onQCItemUnsubscribe();
167         NotifyQCItemChangeHandler.getInstance().cancelQCItemUpdate(this);
168     }
169 
170     private static class NotifyQCItemChangeHandler extends Handler {
171 
172         private static final int MSG_UPDATE_QCITEM = 1000;
173         private static NotifyQCItemChangeHandler sHandler;
174         private final Map<Uri, Long> mLastUpdateTimeLookup = Collections.synchronizedMap(
175                 new ArrayMap<>());
176 
getInstance()177         private static NotifyQCItemChangeHandler getInstance() {
178             if (sHandler == null) {
179                 HandlerThread workerThread = new HandlerThread("NotifyQCItemChangeHandler",
180                         Process.THREAD_PRIORITY_BACKGROUND);
181                 workerThread.start();
182                 sHandler = new NotifyQCItemChangeHandler(workerThread.getLooper());
183             }
184             return sHandler;
185         }
186 
NotifyQCItemChangeHandler(Looper looper)187         private NotifyQCItemChangeHandler(Looper looper) {
188             super(looper);
189         }
190 
191         @Override
handleMessage(Message msg)192         public void handleMessage(Message msg) {
193             if (msg.what != MSG_UPDATE_QCITEM) {
194                 return;
195             }
196 
197             SettingsQCBackgroundWorker worker = (SettingsQCBackgroundWorker) msg.obj;
198             Uri uri = worker.getUri();
199             Context context = worker.getContext();
200             mLastUpdateTimeLookup.put(uri, SystemClock.uptimeMillis());
201             if (UserManager.isVisibleBackgroundUsersEnabled()
202                     && UserManager.get(context).isUserVisible()) {
203                 context.getContentResolver().notifyChange(uri, /* observer= */ null,
204                         NOTIFY_NO_DELAY);
205             } else {
206                 context.getContentResolver().notifyChange(uri, /* observer= */ null);
207             }
208         }
209 
updateQCItem(SettingsQCBackgroundWorker worker)210         private void updateQCItem(SettingsQCBackgroundWorker worker) {
211             if (hasMessages(MSG_UPDATE_QCITEM, worker)) {
212                 return;
213             }
214 
215             Message message = obtainMessage(MSG_UPDATE_QCITEM, worker);
216             long lastUpdateTime = mLastUpdateTimeLookup.getOrDefault(worker.getUri(), 0L);
217             if (lastUpdateTime == 0L) {
218                 // Postpone the first update triggering by onQCItemSubscribe() to avoid being too
219                 // close to the first QCItem bind.
220                 sendMessageDelayed(message, QC_UPDATE_THROTTLE_INTERVAL);
221             } else if (SystemClock.uptimeMillis() - lastUpdateTime
222                     > QC_UPDATE_THROTTLE_INTERVAL) {
223                 sendMessage(message);
224             } else {
225                 sendMessageAtTime(message, lastUpdateTime + QC_UPDATE_THROTTLE_INTERVAL);
226             }
227         }
228 
cancelQCItemUpdate(SettingsQCBackgroundWorker worker)229         private void cancelQCItemUpdate(SettingsQCBackgroundWorker worker) {
230             removeMessages(MSG_UPDATE_QCITEM, worker);
231             mLastUpdateTimeLookup.remove(worker.getUri());
232         }
233     };
234 }
235