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