/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.settings.slices; import android.annotation.MainThread; import android.annotation.Nullable; import android.content.Context; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.util.ArrayMap; import android.util.Log; import java.io.Closeable; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; /** * The Slice background worker is used to make Settings Slices be able to work with data that is * changing continuously, e.g. available Wi-Fi networks. * * The background worker will be started at {@link SettingsSliceProvider#onSlicePinned(Uri)}, be * stopped at {@link SettingsSliceProvider#onSliceUnpinned(Uri)}, and be closed at {@link * SettingsSliceProvider#shutdown()}. * * {@link SliceBackgroundWorker} caches the results, uses the cache to compare if there is any data * changed, and then notifies the Slice {@link Uri} to update. * * It also stores all instances of all workers to ensure each worker is a Singleton. */ public abstract class SliceBackgroundWorker implements Closeable { private static final String TAG = "SliceBackgroundWorker"; private static final long SLICE_UPDATE_THROTTLE_INTERVAL = 300L; private static final Map LIVE_WORKERS = new ArrayMap<>(); private final Context mContext; private final Uri mUri; private List mCachedResults; protected SliceBackgroundWorker(Context context, Uri uri) { mContext = context; mUri = uri; } protected Uri getUri() { return mUri; } protected Context getContext() { return mContext; } /** * Returns the singleton instance of {@link SliceBackgroundWorker} for specified {@link Uri} if * exists */ @Nullable @SuppressWarnings("TypeParameterUnusedInFormals") public static T getInstance(Uri uri) { return (T) LIVE_WORKERS.get(uri); } /** * Returns the singleton instance of {@link SliceBackgroundWorker} for specified {@link * CustomSliceable} */ static SliceBackgroundWorker getInstance(Context context, Sliceable sliceable, Uri uri) { SliceBackgroundWorker worker = getInstance(uri); if (worker == null) { final Class workerClass = sliceable.getBackgroundWorkerClass(); worker = createInstance(context.getApplicationContext(), uri, workerClass); LIVE_WORKERS.put(uri, worker); } return worker; } private static SliceBackgroundWorker createInstance(Context context, Uri uri, Class clazz) { Log.d(TAG, "create instance: " + clazz); try { return clazz.getConstructor(Context.class, Uri.class).newInstance(context, uri); } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) { throw new IllegalStateException( "Invalid slice background worker: " + clazz, e); } } static void shutdown() { for (SliceBackgroundWorker worker : LIVE_WORKERS.values()) { try { worker.close(); } catch (IOException e) { Log.w(TAG, "Shutting down worker failed", e); } } LIVE_WORKERS.clear(); } /** * Called when the Slice is pinned. This is the place to register callbacks or initialize scan * tasks. */ @MainThread protected abstract void onSlicePinned(); /** * Called when the Slice is unpinned. This is the place to unregister callbacks or perform any * final cleanup. */ @MainThread protected abstract void onSliceUnpinned(); /** * @return a {@link List} of cached results */ public final List getResults() { return mCachedResults == null ? null : new ArrayList<>(mCachedResults); } /** * Update the results when data changes */ protected final void updateResults(List results) { boolean needNotify = false; if (results == null) { if (mCachedResults != null) { needNotify = true; } } else { needNotify = !areListsTheSame(results, mCachedResults); } if (needNotify) { mCachedResults = results; notifySliceChange(); } } protected boolean areListsTheSame(List a, List b) { return a.equals(b); } /** * Notify that data was updated and attempt to sync changes to the Slice. */ protected final void notifySliceChange() { NotifySliceChangeHandler.getInstance().updateSlice(this); } void pin() { onSlicePinned(); } void unpin() { onSliceUnpinned(); NotifySliceChangeHandler.getInstance().cancelSliceUpdate(this); } private static class NotifySliceChangeHandler extends Handler { private static final int MSG_UPDATE_SLICE = 1000; private static NotifySliceChangeHandler sHandler; private final Map mLastUpdateTimeLookup = Collections.synchronizedMap( new ArrayMap<>()); private static NotifySliceChangeHandler getInstance() { if (sHandler == null) { final HandlerThread workerThread = new HandlerThread("NotifySliceChangeHandler", Process.THREAD_PRIORITY_BACKGROUND); workerThread.start(); sHandler = new NotifySliceChangeHandler(workerThread.getLooper()); } return sHandler; } private NotifySliceChangeHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { if (msg.what != MSG_UPDATE_SLICE) { return; } final SliceBackgroundWorker worker = (SliceBackgroundWorker) msg.obj; final Uri uri = worker.getUri(); final Context context = worker.getContext(); mLastUpdateTimeLookup.put(uri, SystemClock.uptimeMillis()); context.getContentResolver().notifyChange(uri, null); } private void updateSlice(SliceBackgroundWorker worker) { if (hasMessages(MSG_UPDATE_SLICE, worker)) { return; } final Message message = obtainMessage(MSG_UPDATE_SLICE, worker); final long lastUpdateTime = mLastUpdateTimeLookup.getOrDefault(worker.getUri(), 0L); if (lastUpdateTime == 0L) { // Postpone the first update triggering by onSlicePinned() to avoid being too close // to the first Slice bind. sendMessageDelayed(message, SLICE_UPDATE_THROTTLE_INTERVAL); } else if (SystemClock.uptimeMillis() - lastUpdateTime > SLICE_UPDATE_THROTTLE_INTERVAL) { sendMessage(message); } else { sendMessageAtTime(message, lastUpdateTime + SLICE_UPDATE_THROTTLE_INTERVAL); } } private void cancelSliceUpdate(SliceBackgroundWorker worker) { removeMessages(MSG_UPDATE_SLICE, worker); mLastUpdateTimeLookup.remove(worker.getUri()); } }; }