/* * 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 com.google.android.apps.dashclock; import android.app.Service; import android.appwidget.AppWidgetManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.WakefulBroadcastReceiver; import android.text.TextUtils; import com.google.android.apps.dashclock.api.DashClockExtension; import com.google.android.apps.dashclock.api.host.DashClockHost; import com.google.android.apps.dashclock.api.DashClockSignature; import com.google.android.apps.dashclock.api.ExtensionData; import com.google.android.apps.dashclock.api.host.ExtensionListing; import com.google.android.apps.dashclock.api.internal.IDataConsumerHost; import com.google.android.apps.dashclock.api.internal.IDataConsumerHostCallback; import com.google.android.apps.dashclock.render.WidgetRenderer; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import static com.google.android.apps.dashclock.LogUtils.LOGD; /** * The primary service for DashClock. This service is in charge of updating widget UI (see {@link * #ACTION_UPDATE_WIDGETS}) and updating extension data (see {@link #ACTION_UPDATE_EXTENSIONS}). */ public class DashClockService extends Service implements ExtensionManager.OnChangeListener, SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = LogUtils.makeLogTag(DashClockService.class); /** * Intent action for updating widget views. If {@link #EXTRA_APPWIDGET_ID} is provided, updates * only that widget. Otherwise, updates all widgets. */ public static final String ACTION_UPDATE_WIDGETS = "com.google.android.apps.dashclock.action.UPDATE_WIDGETS"; public static final String EXTRA_APPWIDGET_ID = "com.google.android.apps.dashclock.extra.APPWIDGET_ID"; /** * Intent action for telling extensions to update their data. If {@link #EXTRA_COMPONENT_NAME} * is provided, updates only that extension. Otherwise, updates all active extensions. Also * optional is {@link #EXTRA_UPDATE_REASON} (see {@link DashClockExtension} for update reasons). */ public static final String ACTION_UPDATE_EXTENSIONS = "com.google.android.apps.dashclock.action.UPDATE_EXTENSIONS"; public static final String EXTRA_COMPONENT_NAME = "com.google.android.apps.dashclock.extra.COMPONENT_NAME"; public static final String EXTRA_UPDATE_REASON = "com.google.android.apps.dashclock.extra.UPDATE_REASON"; /** * Related to the Read API. */ protected static final String ACTION_EXTENSION_UPDATE_REQUESTED = "com.google.android.apps.dashclock.action.EXTENSION_UPDATE_REQUESTED"; /** * Broadcast intent action that's triggered when the set of visible extensions or their * data change. */ public static final String ACTION_EXTENSIONS_CHANGED = "com.google.android.apps.dashclock.action.EXTENSIONS_CHANGED"; /** * 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; /** * Force all extensions to be readable by external apps. */ public static final String PREF_FORCE_WORLD_READABLE = "pref_force_world_readable"; private ExtensionHost mExtensionHost; private ExtensionManager mExtensionManager; private CallbackList mCallbacks; private Map<IBinder, CallbackData> mRegisteredCallbacks; private Handler mHandler = new Handler(); private boolean mForceWorldReadable; @Override public void onCreate() { super.onCreate(); LOGD(TAG, "onCreate"); // Initialize the extensions components (host and manager) mCallbacks = new CallbackList(); mRegisteredCallbacks = new HashMap<>(); mExtensionManager = ExtensionManager.getInstance(this); mExtensionManager.addOnChangeListener(this); mExtensionHost = new ExtensionHost(this); IntentFilter filter = new IntentFilter(ACTION_EXTENSION_UPDATE_REQUESTED); LocalBroadcastManager.getInstance(this).registerReceiver(mExtensionEventsReceiver, filter); // Start a periodic refresh of all the extensions // FIXME: only do this if there are any active extensions PeriodicExtensionRefreshReceiver.updateExtensionsAndEnsurePeriodicRefresh(this); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); sp.registerOnSharedPreferenceChangeListener(this); onSharedPreferenceChanged(sp, PREF_FORCE_WORLD_READABLE); } @Override public void onDestroy() { super.onDestroy(); LOGD(TAG, "onDestroy"); LocalBroadcastManager.getInstance(this).unregisterReceiver(mExtensionEventsReceiver); PeriodicExtensionRefreshReceiver.cancelPeriodicRefresh(this); mExtensionHost.destroy(); mCallbacks.kill(); mUpdateHandler.removeCallbacksAndMessages(null); mExtensionManager.removeOnChangeListener(this); PreferenceManager.getDefaultSharedPreferences(this) .unregisterOnSharedPreferenceChangeListener(this); } @Override public int onStartCommand(Intent intent, int flags, int startId) { LOGD(TAG, "onStartCommand: " + (intent != null ? intent.toString() : "no intent")); enforceCallingPermission(DashClockExtension.PERMISSION_READ_EXTENSION_DATA); if (intent != null) { String action = intent.getAction(); if (ACTION_UPDATE_WIDGETS.equals(action)) { handleUpdateWidgets(intent); } else if (ACTION_UPDATE_EXTENSIONS.equals(action)) { handleUpdateExtensions(intent); } // If started by a wakeful broadcast receiver, release the wake lock it acquired. WakefulBroadcastReceiver.completeWakefulIntent(intent); } return START_STICKY; } private Handler mUpdateHandler = new Handler() { @Override public void handleMessage(Message msg) { LOGD(TAG, "onExtensionsChanged from " + (msg.obj != null ? "extension " + msg.obj : "DashClock")); sendBroadcast(new Intent(ACTION_EXTENSIONS_CHANGED)); handleUpdateWidgets(new Intent()); WidgetRenderer.notifyDataSetChanged(DashClockService.this); } }; /** * Updates a widget's UI. */ private void handleUpdateWidgets(Intent intent) { AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this); // Either update all app widgets, or only those which were requested. int appWidgetIds[]; if (intent.hasExtra(EXTRA_APPWIDGET_ID)) { appWidgetIds = new int[]{intent.getIntExtra(EXTRA_APPWIDGET_ID, -1)}; } else { appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName( this, WidgetProvider.class)); } StringBuilder sb = new StringBuilder(); for (int appWidgetId : appWidgetIds) { sb.append(appWidgetId).append(" "); } LOGD(TAG, "Rendering widgets with appWidgetId(s): " + sb); WidgetRenderer.renderWidgets(this, appWidgetIds); } /** * Asks extensions to provide data updates. */ private void handleUpdateExtensions(Intent intent) { int reason = intent.getIntExtra(EXTRA_UPDATE_REASON, DashClockExtension.UPDATE_REASON_UNKNOWN); String updateExtension = intent.getStringExtra(EXTRA_COMPONENT_NAME); LOGD(TAG, String.format("handleUpdateExtensions [action=%s, reason=%d, extension=%s]", intent.getAction(), reason, updateExtension == null ? "" : updateExtension)); // Either update all extensions, or only the requested one. if (!TextUtils.isEmpty(updateExtension)) { ComponentName cn = ComponentName.unflattenFromString(updateExtension); mExtensionHost.execute(cn, ExtensionHost.UPDATE_OPERATIONS.get(reason), ExtensionHost.UPDATE_COLLAPSE_TIME_MILLIS, reason); } else { for (ComponentName cn : mExtensionManager.getActiveExtensionNames()) { mExtensionHost.execute(cn, ExtensionHost.UPDATE_OPERATIONS.get(reason), ExtensionHost.UPDATE_COLLAPSE_TIME_MILLIS, reason); } } } @Override public void onSharedPreferenceChanged(SharedPreferences sp, String key) { if (PREF_FORCE_WORLD_READABLE.equals(key)) { mForceWorldReadable = sp.getBoolean(PREF_FORCE_WORLD_READABLE, false); onExtensionsChanged(null); } } /* * Read API */ private static class CallbackData { int mUid; String mPackage; boolean mHasDashClockSignature; List<ComponentName> mExtensions; } private IDataConsumerHost.Stub mBinder = new IDataConsumerHost.Stub() { @Override public void listenTo(final List<ComponentName> extensions, final IDataConsumerHostCallback cb) throws RemoteException { if (cb == null) { throw new NullPointerException("Callback must not be null"); } enforceCallingPermission(DashClockHost.BIND_DATA_CONSUMER_PERMISSION); final int callingUid = Binder.getCallingUid(); mHandler.post(new Runnable() { @Override public void run() { CallbackData data = createCallbackData(callingUid); data.mExtensions = extensions; mCallbacks.update(cb, data); } }); } @Override public void showExtensionSettings(ComponentName extension, final IDataConsumerHostCallback cb) throws RemoteException { // Check that callback was registered and that extension was enabled enforceEnabledExtensionForCallback(cb, extension); // Make sure we know about the passed in extension ExtensionListing info = findExtensionInfo(extension); if (info == null) { throw new NullPointerException("ExtensionInfo doesn't exists"); } if (info.settingsActivity() == null) { // Nothing to show return; } // Start the proxy activity Intent i = new Intent(DashClockService.this, ExtensionSettingActivityProxy.class); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); i.putExtra(EXTRA_COMPONENT_NAME, info.componentName().flattenToString()); i.putExtra(ExtensionSettingActivityProxy.EXTRA_SETTINGS_ACTIVITY, info.settingsActivity().flattenToString()); startActivity(i); } @Override public void requestExtensionUpdate(List<ComponentName> extensions, final IDataConsumerHostCallback cb) throws RemoteException { enforceRegisteredCallingCallback(cb); internalRequestUpdateData(cb, extensions); } @Override public List<ExtensionListing> getAvailableExtensions() throws RemoteException { return mExtensionManager.getAvailableExtensions(); } @Override public boolean areNonWorldReadableExtensionsVisible() throws RemoteException { return mForceWorldReadable; } }; private class CallbackList extends RemoteCallbackList<IDataConsumerHostCallback> { public void update(IDataConsumerHostCallback cb, CallbackData data) { final IBinder binder = cb.asBinder(); if (data.mExtensions == null) { if (mRegisteredCallbacks.containsKey(binder)) { unregister(cb); mRegisteredCallbacks.remove(binder); } } else { boolean isNewCallback = false; if (!mRegisteredCallbacks.containsKey(binder)) { isNewCallback = true; register(cb); } // Notify callback of data for extensions that it newly registered List<ComponentName> prevExtensions = isNewCallback ? new ArrayList<ComponentName>() : mRegisteredCallbacks.get(binder).mExtensions; Map<ComponentName, ExtensionManager.ExtensionWithData> availableData = determineDataForAlreadyActiveExtensions(data.mExtensions, prevExtensions); try { for (ComponentName cn : availableData.keySet()) { ExtensionManager.ExtensionWithData e = availableData.get(cn); // Do not leak data if extension expressly denied access // to non-dashclock apps if (e != null && e.latestData != null && isExtensionReadableByHost(e, data)) { cb.notifyUpdate(e.listing.componentName(), e.latestData); } else { final ExtensionData notData = new ExtensionData(); cb.notifyUpdate(cn, notData); } } } catch (RemoteException e) { // ignored, cb is dead anyway } mRegisteredCallbacks.put(binder, data); } recalculateActiveExtensions(); } @Override public void onCallbackDied(IDataConsumerHostCallback cb) { super.onCallbackDied(cb); mRegisteredCallbacks.remove(cb.asBinder()); recalculateActiveExtensions(); } } private boolean isExtensionReadableByHost(ExtensionManager.ExtensionWithData e, CallbackData data) { return mForceWorldReadable || e.listing.worldReadable() || (!e.listing.worldReadable() && data.mHasDashClockSignature); } @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public void onExtensionsChanged(ComponentName sourceExtension) { LOGD(TAG, "onExtensionsChanged: source = " + sourceExtension); mUpdateHandler.removeCallbacksAndMessages(null); mUpdateHandler.sendMessageDelayed( mUpdateHandler.obtainMessage(0, sourceExtension), UPDATE_COLLAPSE_TIME_MILLIS); if (sourceExtension == null) { broadcastExtensionListChange( mExtensionManager.getAvailableExtensions()); } else { ExtensionManager.ExtensionWithData data = mExtensionManager.getExtensionWithData(sourceExtension); if (data != null && data.latestData != null) { broadcastDataChange(sourceExtension, data); } } } private void broadcastExtensionListChange(List<ExtensionListing> extensions) { int count = mCallbacks.beginBroadcast(); for (int i = 0; i < count; i++) { try { mCallbacks.getBroadcastItem(i).notifyAvailableExtensionChanged( extensions, mForceWorldReadable); } catch (RemoteException e) { // ignored } } mCallbacks.finishBroadcast(); } private void broadcastDataChange(ComponentName source, ExtensionManager.ExtensionWithData ewd) { int count = mCallbacks.beginBroadcast(); for (int i = 0; i < count; i++) { try { IDataConsumerHostCallback cb = mCallbacks.getBroadcastItem(i); CallbackData cbData = mRegisteredCallbacks.get(cb.asBinder()); List<ComponentName> extension = cbData.mExtensions; if (extension != null && extension.contains(source)) { // Do not leak data if extension expressly denied access // to non-dashclock apps if (isExtensionReadableByHost(ewd, cbData)) { cb.notifyUpdate(source, ewd.latestData); } } } catch (RemoteException e) { // ignored } } mCallbacks.finishBroadcast(); } private Map<ComponentName, ExtensionManager.ExtensionWithData> determineDataForAlreadyActiveExtensions( List<ComponentName> extensions, List<ComponentName> excludedExtensions) { Map<ComponentName, ExtensionManager.ExtensionWithData> result = new HashMap<>(); HashMap<ComponentName, ExtensionManager.ExtensionWithData> map = new HashMap<>(); for (ExtensionManager.ExtensionWithData e : mExtensionManager.getActiveExtensionsWithData()) { if (e.latestData != null) { map.put(e.listing.componentName(), e); } } for (ComponentName extension : extensions) { if (excludedExtensions != null && excludedExtensions.contains(extension)) { continue; } result.put(extension, map.get(extension)); } return result; } private void recalculateActiveExtensions() { HashSet<ComponentName> extensions = new HashSet<>(); for (CallbackData entry : mRegisteredCallbacks.values()) { for (ComponentName extension : entry.mExtensions) { if (extension != null) { extensions.add(extension); } } } LOGD(TAG, "recalculateActiveExtensions: determined list = " + extensions); mExtensionManager.setActiveExtensions(extensions); } private ExtensionListing findExtensionInfo(ComponentName extension) { for (ExtensionListing info : mExtensionManager.getAvailableExtensions()) { if (extension.equals(info.componentName())) { return info; } } return null; } private void enforceRegisteredCallingCallback(IDataConsumerHostCallback cb) { if (cb == null || !mRegisteredCallbacks.containsKey(cb.asBinder())) { throw new SecurityException("Caller should provide a registered callback."); } } private void enforceEnabledExtensionForCallback(IDataConsumerHostCallback cb, ComponentName extension) { enforceRegisteredCallingCallback(cb); List<ComponentName> extensions = mRegisteredCallbacks.get(cb.asBinder()).mExtensions; for (ComponentName ext : extensions) { if (ext.equals(extension)) { return; } } throw new SecurityException("Extension is not enabled for caller."); } private void enforceCallingPermission(String permission) throws SecurityException { // We need to check that any of the packages of the caller has // the request permission final PackageManager pm = getPackageManager(); try { String[] packages = pm.getPackagesForUid(Binder.getCallingUid()); if (packages != null) { for (String pkg : packages) { PackageInfo pi = pm.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS); if (pi.requestedPermissions != null) { for (String requestedPermission : pi.requestedPermissions) { if (requestedPermission.equals(permission)) { // The caller has the request permission return; } } } } } } catch (PackageManager.NameNotFoundException ex) { // Ignore. Package wasn't found } throw new SecurityException("Caller doesn't have the request permission \"" + permission + "\""); } private CallbackData createCallbackData(int uid) { boolean hasDashClockSignature = false; String packageName = null; PackageManager pm = getPackageManager(); String[] packages = pm.getPackagesForUid(uid); if (packages != null && packages.length > 0) { try { PackageInfo pi = pm.getPackageInfo(packages[0], PackageManager.GET_SIGNATURES); packageName = pi.packageName; if (pi.signatures != null && pi.signatures.length == 1 && DashClockSignature.SIGNATURE.equals(pi.signatures[0])) { hasDashClockSignature = true; } } catch (PackageManager.NameNotFoundException ignored) { } } CallbackData data = new CallbackData(); data.mUid = uid; data.mPackage = packageName; data.mHasDashClockSignature = hasDashClockSignature; return data; } private void internalRequestUpdateData(final IDataConsumerHostCallback cb, List<ComponentName> extensions) { // Recover the updatable extensions for this caller List<ComponentName> updatableExtensions = new ArrayList<>(); List<ComponentName> registeredExtensions = mRegisteredCallbacks.get(cb.asBinder()).mExtensions; if (extensions == null) { updatableExtensions.addAll(registeredExtensions); } else { for (ComponentName extension : extensions) { if (registeredExtensions.contains(extension)) { updatableExtensions.add(extension); } } } // Request an update of all the extensions in the list final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); for (ComponentName updatableExtension: updatableExtensions) { Intent intent = new Intent(ACTION_EXTENSION_UPDATE_REQUESTED); intent.putExtra(EXTRA_COMPONENT_NAME, updatableExtension.flattenToString()); intent.putExtra(EXTRA_UPDATE_REASON, DashClockExtension.UPDATE_REASON_MANUAL); lbm.sendBroadcast(intent); } } private final BroadcastReceiver mExtensionEventsReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (ACTION_EXTENSION_UPDATE_REQUESTED.equals(intent.getAction())) { handleUpdateExtensions(intent); } } }; }