com.google.android.apps.dashclock.DashClockService.java Source code

Java tutorial

Introduction

Here is the source code for com.google.android.apps.dashclock.DashClockService.java

Source

/*
 * 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);
            }
        }
    };
}