summaryrefslogtreecommitdiffstats
path: root/src/org/cyanogenmod/launcher/dashclock/ExtensionHost.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/org/cyanogenmod/launcher/dashclock/ExtensionHost.java')
-rw-r--r--src/org/cyanogenmod/launcher/dashclock/ExtensionHost.java479
1 files changed, 479 insertions, 0 deletions
diff --git a/src/org/cyanogenmod/launcher/dashclock/ExtensionHost.java b/src/org/cyanogenmod/launcher/dashclock/ExtensionHost.java
new file mode 100644
index 000000000..1bf2bb9e3
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/dashclock/ExtensionHost.java
@@ -0,0 +1,479 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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 org.cyanogenmod.launcher.dashclock;
+
+import com.google.android.apps.dashclock.api.DashClockExtension;
+import com.google.android.apps.dashclock.api.ExtensionData;
+import com.google.android.apps.dashclock.api.internal.IExtension;
+import com.google.android.apps.dashclock.api.internal.IExtensionHost;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * The primary local-process endpoint that deals with extensions. Instances of this class are in
+ * charge of maintaining a {@link ServiceConnection} with connected extensions. There should
+ * only be one instance of this class in the app.
+ * <p>
+ * This class is intended to be used as part of a containing service. Make sure to call
+ * {@link #destroy()} in the service's {@link android.app.Service#onDestroy()}.
+ */
+public class ExtensionHost {
+ // TODO: this class badly needs inline docs
+ private static final String TAG = "ExtensionHost";
+
+ private static final int CURRENT_EXTENSION_PROTOCOL_VERSION = 2;
+
+ /**
+ * The amount of time to wait after something has changed before recognizing it as an individual
+ * event. Any changes within this time window will be collapsed, and will further delay the
+ * handling of the event.
+ */
+ public static final int UPDATE_COLLAPSE_TIME_MILLIS = 500;
+
+ private Context mContext;
+ private Context mHostActivityContext;
+ private Handler mClientThreadHandler = new Handler();
+
+ private ExtensionManager mExtensionManager;
+
+ private Map<ComponentName, Connection> mExtensionConnections
+ = new HashMap<ComponentName, Connection>();
+
+ private final Set<ComponentName> mExtensionsToUpdateWhenScreenOn = new HashSet<ComponentName>();
+ private boolean mScreenOnReceiverRegistered = false;
+
+ private volatile Looper mAsyncLooper;
+ private volatile Handler mAsyncHandler;
+
+ public ExtensionHost(Context context, Context hostActivityContext) {
+ mContext = context;
+ mHostActivityContext = hostActivityContext;
+ init();
+ }
+
+ public void init() {
+ mExtensionManager = ExtensionManager.getInstance(mContext, mHostActivityContext);
+ mExtensionManager.addOnChangeListener(mChangeListener);
+
+ HandlerThread thread = new HandlerThread("ExtensionHost");
+ thread.start();
+ mAsyncLooper = thread.getLooper();
+ mAsyncHandler = new Handler(mAsyncLooper);
+
+ mChangeListener.onExtensionsChanged(null);
+ mExtensionManager.cleanupExtensions();
+
+ Log.d(TAG, "ExtensionHost initialized.");
+ }
+
+ public void destroy() {
+ mExtensionManager.removeOnChangeListener(mChangeListener);
+ if (mScreenOnReceiverRegistered) {
+ mContext.unregisterReceiver(mScreenOnReceiver);
+ mScreenOnReceiverRegistered = false;
+ }
+ establishAndDestroyConnections(new ArrayList<ComponentName>());
+ mAsyncLooper.quit();
+ }
+
+ private void establishAndDestroyConnections(List<ComponentName> newExtensionNames) {
+ // Get the list of active extensions
+ Set<ComponentName> activeSet = new HashSet<ComponentName>();
+ activeSet.addAll(newExtensionNames);
+
+ // Get the list of connected extensions
+ Set<ComponentName> connectedSet = new HashSet<ComponentName>();
+ connectedSet.addAll(mExtensionConnections.keySet());
+
+ for (final ComponentName cn : activeSet) {
+ if (connectedSet.contains(cn)) {
+ continue;
+ }
+
+ // Bind anything not currently connected (this is the initial connection
+ // to the now-added extension)
+ Connection conn = createConnection(cn, false);
+ if (conn != null) {
+ mExtensionConnections.put(cn, conn);
+ }
+ }
+
+ // Remove active items from the connected set, leaving only newly-inactive items
+ // to be disconnected below.
+ connectedSet.removeAll(activeSet);
+
+ for (ComponentName cn : connectedSet) {
+ Connection conn = mExtensionConnections.get(cn);
+
+ // Unbind the now-disconnected extension
+ destroyConnection(conn);
+ mExtensionConnections.remove(cn);
+ }
+ }
+
+ private Connection createConnection(final ComponentName cn, final boolean isReconnect) {
+ Log.d(TAG, "createConnection for " + cn + "; isReconnect=" + isReconnect);
+
+ final Connection conn = new Connection();
+ conn.componentName = cn;
+ conn.contentObserver = new ContentObserver(mClientThreadHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ execute(conn.componentName,
+ UPDATE_OPERATIONS.get(DashClockExtension.UPDATE_REASON_CONTENT_CHANGED),
+ UPDATE_COLLAPSE_TIME_MILLIS,
+ DashClockExtension.UPDATE_REASON_CONTENT_CHANGED);
+ }
+ };
+ conn.hostInterface = makeHostInterface(conn);
+ conn.serviceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(final ComponentName componentName, IBinder iBinder) {
+ conn.ready = true;
+ conn.binder = IExtension.Stub.asInterface(iBinder);
+
+ // Initialize the service
+ execute(conn, new Operation() {
+ @Override
+ public void run(IExtension extension) throws RemoteException {
+ // Note that this is protected from ANRs since it runs in the
+ // AsyncHandler thread. Also, since this is a 'oneway' call,
+ // when used with remote extensions, this call does not block.
+ try {
+ extension.onInitialize(conn.hostInterface, isReconnect);
+ } catch (SecurityException e) {
+ Log.e(TAG, "Error initializing extension "
+ + componentName.toString(), e);
+ }
+ }
+ }, 0, null);
+
+ if (!isReconnect) {
+ execute(conn.componentName,
+ UPDATE_OPERATIONS.get(DashClockExtension.UPDATE_REASON_INITIAL),
+ 0,
+ null);
+ }
+
+ // Execute operations that were deferred until the service was available.
+ // TODO: handle service disruptions that occur here
+ synchronized (conn.deferredOps) {
+ if (conn.ready) {
+ Set<Object> processedCollapsedTokens = new HashSet<Object>();
+ Iterator<Pair<Object, Operation>> it = conn.deferredOps.iterator();
+ while (it.hasNext()) {
+ Pair<Object, Operation> op = it.next();
+ if (op.first != null) {
+ if (processedCollapsedTokens.contains(op.first)) {
+ // An operation with this collapse token has already been
+ // processed; skip this one.
+ continue;
+ }
+
+ processedCollapsedTokens.add(op.first);
+ }
+ execute(conn, op.second, 0, null);
+ it.remove();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(final ComponentName componentName) {
+ conn.serviceConnection = null;
+ conn.binder = null;
+ conn.ready = false;
+ mClientThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mExtensionConnections.remove(componentName);
+ }
+ });
+ }
+ };
+
+ try {
+ if (!mContext.bindService(new Intent().setComponent(cn), conn.serviceConnection,
+ Context.BIND_AUTO_CREATE)) {
+ Log.e(TAG, "Error binding to extension " + cn.flattenToShortString());
+ return null;
+ }
+ } catch (SecurityException e) {
+ Log.e(TAG, "Error binding to extension " + cn.flattenToShortString(), e);
+ return null;
+ }
+
+ return conn;
+ }
+
+ private IExtensionHost makeHostInterface(final Connection conn) {
+ return new IExtensionHost.Stub() {
+ @Override
+ public void publishUpdate(ExtensionData data) throws RemoteException {
+ if (data == null) {
+ data = new ExtensionData();
+ }
+
+ // TODO: this needs to be thread-safe
+ Log.d(TAG, "publishUpdate received for extension " + conn.componentName);
+ mExtensionManager.updateExtensionData(conn.componentName, data);
+ }
+
+ @Override
+ public void addWatchContentUris(String[] contentUris) throws RemoteException {
+ if (contentUris != null && contentUris.length > 0 && conn.contentObserver != null) {
+ ContentResolver resolver = mContext.getContentResolver();
+ for (String uri : contentUris) {
+ if (TextUtils.isEmpty(uri)) {
+ continue;
+ }
+
+ resolver.registerContentObserver(Uri.parse(uri), true,
+ conn.contentObserver);
+ }
+ }
+ }
+
+ @Override
+ public void removeAllWatchContentUris() throws RemoteException {
+ ContentResolver resolver = mContext.getContentResolver();
+ resolver.unregisterContentObserver(conn.contentObserver);
+ }
+
+ @Override
+ public void setUpdateWhenScreenOn(boolean updateWhenScreenOn) throws RemoteException {
+ synchronized (mExtensionsToUpdateWhenScreenOn) {
+ if (updateWhenScreenOn) {
+ if (mExtensionsToUpdateWhenScreenOn.size() == 0) {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+ mContext.registerReceiver(mScreenOnReceiver, filter);
+ mScreenOnReceiverRegistered = true;
+ }
+
+ mExtensionsToUpdateWhenScreenOn.add(conn.componentName);
+
+ } else {
+ mExtensionsToUpdateWhenScreenOn.remove(conn.componentName);
+
+ if (mExtensionsToUpdateWhenScreenOn.size() == 0) {
+ mContext.unregisterReceiver(mScreenOnReceiver);
+ mScreenOnReceiverRegistered = false;
+ }
+ }
+ }
+ }
+ };
+ }
+
+ public void requestAllManualUpdate() {
+ for (ComponentName cn : mExtensionConnections.keySet()) {
+ execute(cn, UPDATE_OPERATIONS.get(DashClockExtension.UPDATE_REASON_MANUAL),
+ 0, null);
+ }
+ }
+
+ private void destroyConnection(Connection conn) {
+ if (conn.contentObserver != null) {
+ mContext.getContentResolver().unregisterContentObserver(conn.contentObserver);
+ conn.contentObserver = null;
+ }
+
+ conn.binder = null;
+ mContext.unbindService(conn.serviceConnection);
+ conn.serviceConnection = null;
+ }
+
+ private ExtensionManager.OnChangeListener mChangeListener
+ = new ExtensionManager.OnChangeListener() {
+ @Override
+ public void onExtensionsChanged(ComponentName sourceExtension) {
+ if (sourceExtension != null) {
+ // If the extension change is a result of a single extension, don't do anything,
+ // since we're only interested in events triggered by the system overall (e.g.
+ // extensions added or removed).
+ return;
+ }
+ Log.d(TAG, "onExtensionsChanged; calling establishAndDestroyConnections.");
+ establishAndDestroyConnections(mExtensionManager.getActiveExtensionNames());
+ }
+ };
+
+ private void execute(final Connection conn, final Operation operation,
+ int collapseDelayMillis, final Object collapseToken) {
+ final Object collapseTokenForConn;
+ if (collapseDelayMillis > 0 && collapseToken != null) {
+ collapseTokenForConn = new Pair<ComponentName, Object>(conn.componentName,
+ collapseToken);
+ } else {
+ collapseTokenForConn = null;
+ }
+
+ final Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (conn.binder == null) {
+ throw new RemoteException("Binder is unavailable.");
+ }
+ operation.run(conn.binder);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't execute operation; scheduling for retry upon service "
+ + "reconnection.", e);
+ // TODO: exponential backoff for retrying the same operation, or fail after
+ // n attempts (in case the remote service consistently crashes when
+ // executing this operation)
+ synchronized (conn.deferredOps) {
+ conn.deferredOps.add(new Pair<Object, Operation>(
+ collapseTokenForConn, operation));
+ }
+ }
+ }
+ };
+
+ if (conn.ready) {
+ if (collapseTokenForConn != null) {
+ mAsyncHandler.removeCallbacksAndMessages(collapseTokenForConn);
+ }
+
+ if (collapseDelayMillis > 0) {
+ mAsyncHandler.postAtTime(runnable, collapseTokenForConn,
+ SystemClock.uptimeMillis() + collapseDelayMillis);
+ } else {
+ mAsyncHandler.post(runnable);
+ }
+ } else {
+ mAsyncHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (conn.deferredOps) {
+ conn.deferredOps.add(new Pair<Object, Operation>(
+ collapseTokenForConn, operation));
+ }
+ }
+ });
+ }
+ }
+
+ public void execute(ComponentName cn, Operation operation,
+ int collapseDelayMillis, final Object collapseToken) {
+ Connection conn = mExtensionConnections.get(cn);
+ if (conn == null) {
+ conn = createConnection(cn, true);
+ if (conn != null) {
+ mExtensionConnections.put(cn, conn);
+ } else {
+ Log.e(TAG, "Couldn't connect to extension to perform operation; operation "
+ + "canceled.");
+ return;
+ }
+ }
+
+ execute(conn, operation, collapseDelayMillis, collapseToken);
+ }
+
+ private final BroadcastReceiver mScreenOnReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ synchronized (mExtensionsToUpdateWhenScreenOn) {
+ for (ComponentName cn : mExtensionsToUpdateWhenScreenOn) {
+ execute(cn, UPDATE_OPERATIONS.get(DashClockExtension.UPDATE_REASON_SCREEN_ON),
+ 0, null);
+ }
+ }
+ }
+ };
+
+ static final SparseArray<Operation> UPDATE_OPERATIONS = new SparseArray<Operation>();
+
+ static {
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_UNKNOWN);
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_INITIAL);
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_PERIODIC);
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_SETTINGS_CHANGED);
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_CONTENT_CHANGED);
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_SCREEN_ON);
+ _createUpdateOperation(DashClockExtension.UPDATE_REASON_MANUAL);
+ }
+
+ private static void _createUpdateOperation(final int reason) {
+ UPDATE_OPERATIONS.put(reason, new ExtensionHost.Operation() {
+ @Override
+ public void run(IExtension extension) throws RemoteException {
+ // Note that this is protected from ANRs since it runs in the AsyncHandler thread.
+ // Also, since this is a 'oneway' call, when used with remote extensions, this call
+ // does not block.
+ extension.onUpdate(reason);
+ }
+ });
+ }
+
+ public static boolean supportsProtocolVersion(int protocolVersion) {
+ return protocolVersion > 0 && protocolVersion <= CURRENT_EXTENSION_PROTOCOL_VERSION;
+ }
+
+ /**
+ * Will be run on a worker thread.
+ */
+ public static interface Operation {
+ void run(IExtension extension) throws RemoteException;
+ }
+
+ private static class Connection {
+ boolean ready = false;
+ ComponentName componentName;
+ ServiceConnection serviceConnection;
+ IExtension binder;
+ IExtensionHost hostInterface;
+ ContentObserver contentObserver;
+
+ /**
+ * Only access on the async thread. The pair is (collapse token, operation)
+ */
+ final Queue<Pair<Object, Operation>> deferredOps
+ = new LinkedList<Pair<Object, Operation>>();
+ }
+}