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