diff options
Diffstat (limited to 'src/org/cyanogenmod/launcher/dashclock')
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); + } + } + } + */ + } +} |