1 /* 2 * Copyright (C) 2018 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.settings.slices; 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 import android.util.Log; 31 32 import java.io.Closeable; 33 import java.io.IOException; 34 import java.lang.reflect.InvocationTargetException; 35 import java.util.ArrayList; 36 import java.util.Collections; 37 import java.util.List; 38 import java.util.Map; 39 40 /** 41 * The Slice background worker is used to make Settings Slices be able to work with data that is 42 * changing continuously, e.g. available Wi-Fi networks. 43 * 44 * The background worker will be started at {@link SettingsSliceProvider#onSlicePinned(Uri)}, be 45 * stopped at {@link SettingsSliceProvider#onSliceUnpinned(Uri)}, and be closed at {@link 46 * SettingsSliceProvider#shutdown()}. 47 * 48 * {@link SliceBackgroundWorker} caches the results, uses the cache to compare if there is any data 49 * changed, and then notifies the Slice {@link Uri} to update. 50 * 51 * It also stores all instances of all workers to ensure each worker is a Singleton. 52 */ 53 public abstract class SliceBackgroundWorker<E> implements Closeable { 54 55 private static final String TAG = "SliceBackgroundWorker"; 56 57 private static final long SLICE_UPDATE_THROTTLE_INTERVAL = 300L; 58 59 private static final Map<Uri, SliceBackgroundWorker> LIVE_WORKERS = new ArrayMap<>(); 60 61 private final Context mContext; 62 private final Uri mUri; 63 64 private List<E> mCachedResults; 65 SliceBackgroundWorker(Context context, Uri uri)66 protected SliceBackgroundWorker(Context context, Uri uri) { 67 mContext = context; 68 mUri = uri; 69 } 70 getUri()71 protected Uri getUri() { 72 return mUri; 73 } 74 getContext()75 protected Context getContext() { 76 return mContext; 77 } 78 79 /** 80 * Returns the singleton instance of {@link SliceBackgroundWorker} for specified {@link Uri} if 81 * exists 82 */ 83 @Nullable 84 @SuppressWarnings("TypeParameterUnusedInFormals") getInstance(Uri uri)85 public static <T extends SliceBackgroundWorker> T getInstance(Uri uri) { 86 return (T) LIVE_WORKERS.get(uri); 87 } 88 89 /** 90 * Returns the singleton instance of {@link SliceBackgroundWorker} for specified {@link 91 * CustomSliceable} 92 */ getInstance(Context context, Sliceable sliceable, Uri uri)93 static SliceBackgroundWorker getInstance(Context context, Sliceable sliceable, Uri uri) { 94 SliceBackgroundWorker worker = getInstance(uri); 95 if (worker == null) { 96 final Class<? extends SliceBackgroundWorker> workerClass = 97 sliceable.getBackgroundWorkerClass(); 98 worker = createInstance(context.getApplicationContext(), uri, workerClass); 99 LIVE_WORKERS.put(uri, worker); 100 } 101 return worker; 102 } 103 createInstance(Context context, Uri uri, Class<? extends SliceBackgroundWorker> clazz)104 private static SliceBackgroundWorker createInstance(Context context, Uri uri, 105 Class<? extends SliceBackgroundWorker> clazz) { 106 Log.d(TAG, "create instance: " + clazz); 107 try { 108 return clazz.getConstructor(Context.class, Uri.class).newInstance(context, uri); 109 } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | 110 InvocationTargetException e) { 111 throw new IllegalStateException( 112 "Invalid slice background worker: " + clazz, e); 113 } 114 } 115 shutdown()116 static void shutdown() { 117 for (SliceBackgroundWorker worker : LIVE_WORKERS.values()) { 118 try { 119 worker.close(); 120 } catch (IOException e) { 121 Log.w(TAG, "Shutting down worker failed", e); 122 } 123 } 124 LIVE_WORKERS.clear(); 125 } 126 127 /** 128 * Called when the Slice is pinned. This is the place to register callbacks or initialize scan 129 * tasks. 130 */ 131 @MainThread onSlicePinned()132 protected abstract void onSlicePinned(); 133 134 /** 135 * Called when the Slice is unpinned. This is the place to unregister callbacks or perform any 136 * final cleanup. 137 */ 138 @MainThread onSliceUnpinned()139 protected abstract void onSliceUnpinned(); 140 141 /** 142 * @return a {@link List} of cached results 143 */ getResults()144 public final List<E> getResults() { 145 return mCachedResults == null ? null : new ArrayList<>(mCachedResults); 146 } 147 148 /** 149 * Update the results when data changes 150 */ updateResults(List<E> results)151 protected final void updateResults(List<E> results) { 152 boolean needNotify = false; 153 154 if (results == null) { 155 if (mCachedResults != null) { 156 needNotify = true; 157 } 158 } else { 159 needNotify = !areListsTheSame(results, mCachedResults); 160 } 161 162 if (needNotify) { 163 mCachedResults = results; 164 notifySliceChange(); 165 } 166 } 167 areListsTheSame(List<E> a, List<E> b)168 protected boolean areListsTheSame(List<E> a, List<E> b) { 169 return a.equals(b); 170 } 171 172 /** 173 * Notify that data was updated and attempt to sync changes to the Slice. 174 */ notifySliceChange()175 protected final void notifySliceChange() { 176 NotifySliceChangeHandler.getInstance().updateSlice(this); 177 } 178 pin()179 void pin() { 180 onSlicePinned(); 181 } 182 unpin()183 void unpin() { 184 onSliceUnpinned(); 185 NotifySliceChangeHandler.getInstance().cancelSliceUpdate(this); 186 } 187 188 private static class NotifySliceChangeHandler extends Handler { 189 190 private static final int MSG_UPDATE_SLICE = 1000; 191 192 private static NotifySliceChangeHandler sHandler; 193 194 private final Map<Uri, Long> mLastUpdateTimeLookup = Collections.synchronizedMap( 195 new ArrayMap<>()); 196 getInstance()197 private static NotifySliceChangeHandler getInstance() { 198 if (sHandler == null) { 199 final HandlerThread workerThread = new HandlerThread("NotifySliceChangeHandler", 200 Process.THREAD_PRIORITY_BACKGROUND); 201 workerThread.start(); 202 sHandler = new NotifySliceChangeHandler(workerThread.getLooper()); 203 } 204 return sHandler; 205 } 206 NotifySliceChangeHandler(Looper looper)207 private NotifySliceChangeHandler(Looper looper) { 208 super(looper); 209 } 210 211 @Override handleMessage(Message msg)212 public void handleMessage(Message msg) { 213 if (msg.what != MSG_UPDATE_SLICE) { 214 return; 215 } 216 217 final SliceBackgroundWorker worker = (SliceBackgroundWorker) msg.obj; 218 final Uri uri = worker.getUri(); 219 final Context context = worker.getContext(); 220 mLastUpdateTimeLookup.put(uri, SystemClock.uptimeMillis()); 221 context.getContentResolver().notifyChange(uri, null); 222 } 223 updateSlice(SliceBackgroundWorker worker)224 private void updateSlice(SliceBackgroundWorker worker) { 225 if (hasMessages(MSG_UPDATE_SLICE, worker)) { 226 return; 227 } 228 229 final Message message = obtainMessage(MSG_UPDATE_SLICE, worker); 230 final long lastUpdateTime = mLastUpdateTimeLookup.getOrDefault(worker.getUri(), 0L); 231 if (lastUpdateTime == 0L) { 232 // Postpone the first update triggering by onSlicePinned() to avoid being too close 233 // to the first Slice bind. 234 sendMessageDelayed(message, SLICE_UPDATE_THROTTLE_INTERVAL); 235 } else if (SystemClock.uptimeMillis() - lastUpdateTime 236 > SLICE_UPDATE_THROTTLE_INTERVAL) { 237 sendMessage(message); 238 } else { 239 sendMessageAtTime(message, lastUpdateTime + SLICE_UPDATE_THROTTLE_INTERVAL); 240 } 241 } 242 cancelSliceUpdate(SliceBackgroundWorker worker)243 private void cancelSliceUpdate(SliceBackgroundWorker worker) { 244 removeMessages(MSG_UPDATE_SLICE, worker); 245 mLastUpdateTimeLookup.remove(worker.getUri()); 246 } 247 }; 248 } 249