summaryrefslogtreecommitdiffstats
path: root/src/org/cyanogenmod/launcher/dashclock
diff options
context:
space:
mode:
Diffstat (limited to 'src/org/cyanogenmod/launcher/dashclock')
-rw-r--r--src/org/cyanogenmod/launcher/dashclock/ExtensionHost.java479
-rw-r--r--src/org/cyanogenmod/launcher/dashclock/ExtensionManager.java390
-rw-r--r--src/org/cyanogenmod/launcher/dashclock/ExtensionPackageChangeReceiver.java76
3 files changed, 945 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>>();
+ }
+}
diff --git a/src/org/cyanogenmod/launcher/dashclock/ExtensionManager.java b/src/org/cyanogenmod/launcher/dashclock/ExtensionManager.java
new file mode 100644
index 000000000..a64b0be2e
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/dashclock/ExtensionManager.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2013 Google Inc.
+ * Modified 2014 for the CyanogenMod 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 org.cyanogenmod.launcher.dashclock;
+
+import android.app.backup.BackupManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.google.android.apps.dashclock.api.DashClockExtension;
+import com.google.android.apps.dashclock.api.ExtensionData;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A singleton class in charge of extension registration, activation (change in user-specified
+ * 'active' extensions), and data caching.
+ */
+public class ExtensionManager {
+ private static final String TAG = "ExtensionManager";
+
+ private static final String PREF_ACTIVE_EXTENSIONS = "active_extensions";
+
+ // No default extensions for now. TODO: include dashclock's default extensions
+ private static final Class[] DEFAULT_EXTENSIONS = {};
+
+ private final Context mContext;
+
+ private final List<ExtensionWithData> mActiveExtensions = new ArrayList<ExtensionWithData>();
+ private Map<ComponentName, ExtensionWithData> mExtensionInfoMap
+ = new HashMap<ComponentName, ExtensionWithData>();
+ private List<OnChangeListener> mOnChangeListeners = new ArrayList<OnChangeListener>();
+
+ private SharedPreferences mDefaultPreferences;
+ private SharedPreferences mValuesPreferences;
+ private Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+
+ private static ExtensionManager sInstance;
+
+ public static ExtensionManager getInstance(Context context, Context hostActivityContext) {
+ if (sInstance == null) {
+ sInstance = new ExtensionManager(context, hostActivityContext);
+ }
+
+ return sInstance;
+ }
+
+ private ExtensionManager(Context context, Context hostActivityContext) {
+ mContext = context;
+ mDefaultPreferences = PreferenceManager.getDefaultSharedPreferences(hostActivityContext);
+ mValuesPreferences = hostActivityContext.getSharedPreferences("extension_data", 0);
+ loadActiveExtensionList();
+ }
+
+ /**
+ * De-activates active extensions that are unsupported or are no longer installed.
+ */
+ public boolean cleanupExtensions() {
+ Set<ComponentName> availableExtensions = new HashSet<ComponentName>();
+ for (ExtensionListing listing : getAvailableExtensions()) {
+ // Ensure the extension protocol version is supported. If it isn't, don't allow its use.
+ if (!ExtensionHost.supportsProtocolVersion(listing.protocolVersion)) {
+ Log.w(TAG, "Extension '" + listing.title + "' using unsupported protocol version "
+ + listing.protocolVersion + ".");
+ continue;
+ }
+ availableExtensions.add(listing.componentName);
+ }
+
+ boolean cleanupRequired = false;
+ ArrayList<ComponentName> newActiveExtensions = new ArrayList<ComponentName>();
+
+ synchronized (mActiveExtensions) {
+ for (ExtensionWithData ewd : mActiveExtensions) {
+ if (availableExtensions.contains(ewd.listing.componentName)) {
+ newActiveExtensions.add(ewd.listing.componentName);
+ } else {
+ cleanupRequired = true;
+ }
+ }
+ }
+
+ if (cleanupRequired) {
+ setActiveExtensions(newActiveExtensions);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void loadActiveExtensionList() {
+ List<ComponentName> activeExtensions = new ArrayList<ComponentName>();
+ String extensions;
+ if (mDefaultPreferences.contains(PREF_ACTIVE_EXTENSIONS)) {
+ extensions = mDefaultPreferences.getString(PREF_ACTIVE_EXTENSIONS, "");
+ } else {
+ extensions = createDefaultExtensionList();
+ }
+ String[] componentNameStrings = extensions.split(",");
+ for (String componentNameString : componentNameStrings) {
+ if (TextUtils.isEmpty(componentNameString)) {
+ continue;
+ }
+ activeExtensions.add(ComponentName.unflattenFromString(componentNameString));
+ }
+ setActiveExtensions(activeExtensions, false);
+ }
+
+ private String createDefaultExtensionList() {
+ StringBuilder sb = new StringBuilder();
+
+ for (Class cls : DEFAULT_EXTENSIONS) {
+ if (sb.length() > 0) {
+ sb.append(",");
+ }
+ sb.append(new ComponentName(mContext, cls).flattenToString());
+ }
+
+ return sb.toString();
+ }
+
+ private void saveActiveExtensionList() {
+ StringBuilder sb = new StringBuilder();
+
+ synchronized (mActiveExtensions) {
+ for (ExtensionWithData ci : mActiveExtensions) {
+ if (sb.length() > 0) {
+ sb.append(",");
+ }
+ sb.append(ci.listing.componentName.flattenToString());
+ }
+ }
+
+ mDefaultPreferences.edit()
+ .putString(PREF_ACTIVE_EXTENSIONS, sb.toString())
+ .commit();
+ new BackupManager(mContext).dataChanged();
+ }
+
+ /**
+ * Replaces the set of active extensions with the given list.
+ */
+ public void setActiveExtensions(List<ComponentName> extensions) {
+ setActiveExtensions(extensions, true);
+ }
+
+ private void setActiveExtensions(List<ComponentName> extensionNames, boolean saveAndNotify) {
+ Map<ComponentName, ExtensionListing> listings
+ = new HashMap<ComponentName, ExtensionListing>();
+ for (ExtensionListing listing : getAvailableExtensions()) {
+ listings.put(listing.componentName, listing);
+ }
+
+ List<ComponentName> activeExtensionNames = getActiveExtensionNames();
+ if (activeExtensionNames.equals(extensionNames)) {
+ Log.d(TAG, "No change to list of active extensions.");
+ return;
+ }
+
+ // Clear cached data for any no-longer-active extensions.
+ for (ComponentName cn : activeExtensionNames) {
+ if (!extensionNames.contains(cn)) {
+ destroyExtensionData(cn);
+ }
+ }
+
+ // Set the new list of active extensions, loading cached data if necessary.
+ List<ExtensionWithData> newActiveExtensions = new ArrayList<ExtensionWithData>();
+
+ for (ComponentName cn : extensionNames) {
+ if (mExtensionInfoMap.containsKey(cn)) {
+ newActiveExtensions.add(mExtensionInfoMap.get(cn));
+ } else {
+ ExtensionWithData ewd = new ExtensionWithData();
+ ewd.listing = listings.get(cn);
+ if (ewd.listing == null) {
+ ewd.listing = new ExtensionListing();
+ ewd.listing.componentName = cn;
+ }
+ ewd.latestData = deserializeExtensionData(ewd.listing.componentName);
+ newActiveExtensions.add(ewd);
+ }
+ }
+
+ mExtensionInfoMap.clear();
+ for (ExtensionWithData ewd : newActiveExtensions) {
+ mExtensionInfoMap.put(ewd.listing.componentName, ewd);
+ }
+
+ synchronized (mActiveExtensions) {
+ mActiveExtensions.clear();
+ mActiveExtensions.addAll(newActiveExtensions);
+ }
+
+ if (saveAndNotify) {
+ Log.d(TAG, "List of active extensions has changed.");
+ saveActiveExtensionList();
+ notifyOnChangeListeners(null);
+ }
+ }
+
+ /**
+ * Updates and caches the user-visible data for a given extension.
+ */
+ public boolean updateExtensionData(ComponentName cn, ExtensionData data) {
+ data.clean();
+
+ ExtensionWithData ewd = mExtensionInfoMap.get(cn);
+ if (ewd != null && !ExtensionData.equals(ewd.latestData, data)) {
+ ewd.latestData = data;
+ serializeExtensionData(ewd.listing.componentName, data);
+ notifyOnChangeListeners(ewd.listing.componentName);
+ return true;
+ }
+ return false;
+ }
+
+ private ExtensionData deserializeExtensionData(ComponentName componentName) {
+ ExtensionData extensionData = new ExtensionData();
+ String val = mValuesPreferences.getString(componentName.flattenToString(), "");
+ if (!TextUtils.isEmpty(val)) {
+ try {
+ extensionData.deserialize((JSONObject) new JSONTokener(val).nextValue());
+ } catch (JSONException e) {
+ Log.e(TAG, "Error loading extension data cache for " + componentName + ".",
+ e);
+ }
+ }
+ return extensionData;
+ }
+
+ private void serializeExtensionData(ComponentName componentName, ExtensionData extensionData) {
+ try {
+ mValuesPreferences.edit()
+ .putString(componentName.flattenToString(),
+ extensionData.serialize().toString())
+ .apply();
+ } catch (JSONException e) {
+ Log.e(TAG, "Error storing extension data cache for " + componentName + ".", e);
+ }
+ }
+
+ private void destroyExtensionData(ComponentName componentName) {
+ mValuesPreferences.edit()
+ .remove(componentName.flattenToString())
+ .apply();
+ }
+
+ public List<ExtensionWithData> getActiveExtensionsWithData() {
+ ArrayList<ExtensionWithData> activeExtensions;
+ synchronized (mActiveExtensions) {
+ activeExtensions = new ArrayList<ExtensionWithData>(mActiveExtensions);
+ }
+ return activeExtensions;
+ }
+
+ public List<ExtensionWithData> getVisibleExtensionsWithData() {
+ ArrayList<ExtensionWithData> visibleExtensions = new ArrayList<ExtensionWithData>();
+ synchronized (mActiveExtensions) {
+ for (ExtensionManager.ExtensionWithData ewd : mActiveExtensions) {
+ if (ewd.latestData.visible()) {
+ visibleExtensions.add(ewd);
+ }
+ }
+ }
+ return visibleExtensions;
+ }
+
+ public List<ComponentName> getActiveExtensionNames() {
+ List<ComponentName> list = new ArrayList<ComponentName>();
+ for (ExtensionWithData ci : mActiveExtensions) {
+ list.add(ci.listing.componentName);
+ }
+ return list;
+ }
+
+ /**
+ * Returns a listing of all available (installed) extensions.
+ */
+ public List<ExtensionListing> getAvailableExtensions() {
+ List<ExtensionListing> availableExtensions = new ArrayList<ExtensionListing>();
+ PackageManager pm = mContext.getPackageManager();
+ List<ResolveInfo> resolveInfos = pm.queryIntentServices(
+ new Intent(DashClockExtension.ACTION_EXTENSION), PackageManager.GET_META_DATA);
+ for (ResolveInfo resolveInfo : resolveInfos) {
+ ExtensionListing listing = new ExtensionListing();
+ listing.componentName = new ComponentName(resolveInfo.serviceInfo.packageName,
+ resolveInfo.serviceInfo.name);
+ listing.title = resolveInfo.loadLabel(pm).toString();
+ Bundle metaData = resolveInfo.serviceInfo.metaData;
+ if (metaData != null) {
+ listing.protocolVersion = metaData.getInt("protocolVersion");
+ listing.worldReadable = metaData.getBoolean("worldReadable", false);
+ listing.description = metaData.getString("description");
+ String settingsActivity = metaData.getString("settingsActivity");
+ if (!TextUtils.isEmpty(settingsActivity)) {
+ listing.settingsActivity = ComponentName.unflattenFromString(
+ resolveInfo.serviceInfo.packageName + "/" + settingsActivity);
+ }
+ }
+
+ listing.icon = resolveInfo.loadIcon(pm);
+ availableExtensions.add(listing);
+ }
+
+ return availableExtensions;
+ }
+
+ /**
+ * Registers a listener to be triggered when either the list of active extensions changes or an
+ * extension's data changes.
+ */
+ public void addOnChangeListener(OnChangeListener onChangeListener) {
+ mOnChangeListeners.add(onChangeListener);
+ }
+
+ /**
+ * Removes a listener previously registered with {@link #addOnChangeListener}.
+ */
+ public void removeOnChangeListener(OnChangeListener onChangeListener) {
+ mOnChangeListeners.remove(onChangeListener);
+ }
+
+ private void notifyOnChangeListeners(final ComponentName sourceExtension) {
+ mMainThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (OnChangeListener listener : mOnChangeListeners) {
+ listener.onExtensionsChanged(sourceExtension);
+ }
+ }
+ });
+ }
+
+ public static interface OnChangeListener {
+ /**
+ * @param sourceExtension null if not related to any specific extension (e.g. list of
+ * extensions has changed).
+ */
+ void onExtensionsChanged(ComponentName sourceExtension);
+ }
+
+ public static class ExtensionWithData {
+ public ExtensionListing listing;
+ public ExtensionData latestData;
+ }
+
+ public static class ExtensionListing {
+ public ComponentName componentName;
+ public int protocolVersion;
+ public boolean worldReadable;
+ public String title;
+ public String description;
+ public Drawable icon;
+ public ComponentName settingsActivity;
+ }
+}
diff --git a/src/org/cyanogenmod/launcher/dashclock/ExtensionPackageChangeReceiver.java b/src/org/cyanogenmod/launcher/dashclock/ExtensionPackageChangeReceiver.java
new file mode 100644
index 000000000..ac32d1e20
--- /dev/null
+++ b/src/org/cyanogenmod/launcher/dashclock/ExtensionPackageChangeReceiver.java
@@ -0,0 +1,76 @@
+/*
+ * 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 android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v4.content.WakefulBroadcastReceiver;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Broadcast receiver used to watch for changes to installed packages on the device. This triggers
+ * a cleanup of extensions (in case one was uninstalled), or a data update request to an extension
+ * if it was updated (its package was replaced).
+ */
+public class ExtensionPackageChangeReceiver extends WakefulBroadcastReceiver {
+ private static final String TAG = "ExtensionPackageChangeReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ /*
+ ExtensionManager extensionManager = ExtensionManager.getInstance(context);
+ if (extensionManager.cleanupExtensions()) {
+ Log.d(TAG, "Extension cleanup performed and action taken.");
+
+ TODO Update CMHome with new extension info
+ Intent widgetUpdateIntent = new Intent(context, DashClockService.class);
+ widgetUpdateIntent.setAction(DashClockService.ACTION_UPDATE_WIDGETS);
+ startWakefulService(context, widgetUpdateIntent);
+ }
+
+ // If this is a replacement or change in the package, update all active extensions from
+ // this package.
+ String action = intent.getAction();
+ if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
+ || Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
+ String packageName = intent.getData().getSchemeSpecificPart();
+ if (TextUtils.isEmpty(packageName)) {
+ return;
+ }
+
+ List<ComponentName> activeExtensions = extensionManager.getActiveExtensionNames();
+ for (ComponentName cn : activeExtensions) {
+ if (packageName.equals(cn.getPackageName())) {
+ /*
+ TODO Update CMHome with new extension info
+ LOGD(TAG, "Package for extension " + cn + " changed; asking it for an update.");
+ Intent extensionUpdateIntent = new Intent(context, DashClockService.class);
+ extensionUpdateIntent.setAction(DashClockService.ACTION_UPDATE_EXTENSIONS);
+ extensionUpdateIntent.putExtra(DashClockService.EXTRA_COMPONENT_NAME,
+ cn.flattenToShortString());
+ startWakefulService(context, extensionUpdateIntent);
+ }
+ }
+ }
+ */
+ }
+}