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