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