/*
* 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.api;
import com.google.android.apps.dashclock.api.internal.IExtension;
import com.google.android.apps.dashclock.api.internal.IExtensionHost;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
/**
* Base class for a DashClock extension. Extensions are a way for other apps to show additional
* status information within DashClock widgets that the user may add to the lockscreen or home
* screen. A limited amount of status information is supported. See the {@link ExtensionData} class
* for the types of information that can be displayed.
*
* <h3>Subclassing {@link DashClockExtension}</h3>
*
* Subclasses must implement at least the {@link #onUpdateData(int)} method, which will be called
* when DashClock requests updated data to show for this extension. Once the extension has new
* data to show, call {@link #publishUpdate(ExtensionData)} to pass the data to the main DashClock
* process. {@link #onUpdateData(int)} will by default be called roughly once per hour, but
* extensions can use methods such as {@link #setUpdateWhenScreenOn(boolean)} and
* {@link #addWatchContentUris(String[])} to request more frequent updates.
*
* <p>
* Subclasses can also override the {@link #onInitialize(boolean)} method to perform basic
* initialization each time a connection to DashClock is established or re-established.
*
* <h3>Registering extensions</h3>
* An extension is simply a service that the DashClock process binds to. Subclasses of this
* base {@link DashClockExtension} class should thus be declared as <code><service></code>
* components in the application's <code>AndroidManifest.xml</code> file.
*
* <p>
* The main DashClock app discovers available extensions using Android's {@link Intent} mechanism.
* Ensure that your <code>service</code> definition includes an <code><intent-filter></code>
* with an action of {@link #ACTION_EXTENSION}. Also make sure to require the
* {@link #PERMISSION_READ_EXTENSION_DATA} permission so that only DashClock can bind to your
* service and request updates. Lastly, there are a few <code><meta-data></code> elements that
* you should add to your service definition:
*
* <ul>
* <li><code>protocolVersion</code> (required): should be <strong>1</strong> or <strong>2</strong>.</li>
* <li><code>description</code> (required): should be a one- or two-sentence description
* of the extension, as a string.</li>
* <li><code>settingsActivity</code> (optional): if present, should be the qualified
* component name for a configuration activity in the extension's package that DashClock can offer
* to the user for customizing the extension.</li>
* <li><code>worldReadable</code> (optional): if present and true (default is false), will allow
* other apps besides DashClock to read data for this extension.</li>
* </ul>
*
* <h3>Example</h3>
*
* Below is an example extension declaration in the manifest:
*
* <pre class="prettyprint">
* <service android:name=".ExampleExtension"
* android:icon="@drawable/ic_extension_example"
* android:label="@string/extension_title"
* android:permission="com.google.android.apps.dashclock.permission.READ_EXTENSION_DATA">
* <intent-filter>
* <action android:name="com.google.android.apps.dashclock.Extension" />
* </intent-filter>
* <meta-data android:name="protocolVersion" android:value="2" />
* <meta-data android:name="worldReadable" android:value="true" />
* <meta-data android:name="description"
* android:value="@string/extension_description" />
* <!-- A settings activity is optional -->
* <meta-data android:name="settingsActivity"
* android:value=".ExampleSettingsActivity" />
* </service>
* </pre>
*
* If a <code>settingsActivity</code> meta-data element is present, an activity with the given
* component name should be defined and exported in the application's manifest as well. DashClock
* will set the {@link #EXTRA_FROM_DASHCLOCK_SETTINGS} extra to true in the launch intent for this
* activity. An example is shown below:
*
* <pre class="prettyprint">
* <activity android:name=".ExampleSettingsActivity"
* android:label="@string/title_settings"
* android:exported="true" />
* </pre>
*
* Finally, below is a simple example {@link DashClockExtension} subclass that shows static data in
* DashClock:
*
* <pre class="prettyprint">
* public class ExampleExtension extends DashClockExtension {
* protected void onUpdateData(int reason) {
* publishUpdate(new ExtensionData()
* .visible(true)
* .icon(R.drawable.ic_extension_example)
* .status("Hello")
* .expandedTitle("Hello, world!")
* .expandedBody("This is an example.")
* .clickIntent(new Intent(Intent.ACTION_VIEW,
* Uri.parse("http://www.google.com"))));
* }
* }
* </pre>
*/
public abstract class DashClockExtension extends Service {
private static final String TAG = "DashClockExtension";
/**
* Indicates that {@link #onUpdateData(int)} was triggered for an unknown reason. This should
* be treated as a generic update (similar to {@link #UPDATE_REASON_PERIODIC}.
*/
public static final int UPDATE_REASON_UNKNOWN = 0;
/**
* Indicates that this is the first call to {@link #onUpdateData(int)} since the connection to
* the main DashClock app was established. Note that updates aren't requested in response to
* reconnections after a connection is lost.
*/
public static final int UPDATE_REASON_INITIAL = 1;
/**
* Indicates that {@link #onUpdateData(int)} was triggered due to a normal perioidic refresh
* of extension data.
*/
public static final int UPDATE_REASON_PERIODIC = 2;
/**
* Indicates that {@link #onUpdateData(int)} was triggered because settings for this extension
* may have changed.
*/
public static final int UPDATE_REASON_SETTINGS_CHANGED = 3;
/**
* Indicates that {@link #onUpdateData(int)} was triggered because content changed on a content
* URI previously registered with {@link #addWatchContentUris(String[])}.
*/
public static final int UPDATE_REASON_CONTENT_CHANGED = 4;
/**
* Indicates that {@link #onUpdateData(int)} was triggered because the device screen turned on
* and the extension has called
* {@link #setUpdateWhenScreenOn(boolean) setUpdateWhenScreenOn(true)}.
*/
public static final int UPDATE_REASON_SCREEN_ON = 5;
/**
* Indicates that {@link #onUpdateData(int)} was triggered because the user explicitly requested
* that the extension be updated.
*
* @since Protocol Version 2 (API r2.x)
*/
public static final int UPDATE_REASON_MANUAL = 6;
/**
* The {@link Intent} action representing a DashClock extension. This service should
* declare an <code><intent-filter></code> for this action in order to register with
* DashClock.
*/
public static final String ACTION_EXTENSION = "com.google.android.apps.dashclock.Extension";
/**
* Boolean extra that will be set to true when DashClock starts extension settings activities.
* Check for this extra in your settings activity if you need to adjust your UI depending on
* whether or not the user came from DashClock's settings screen.
*
* @since Protocol Version 2 (API r2.x)
*/
public static final String EXTRA_FROM_DASHCLOCK_SETTINGS
= "com.google.android.apps.dashclock.extra.FROM_DASHCLOCK_SETTINGS";
/**
* The permission that DashClock extensions should require callers to have before providing
* any status updates. Permission checks are implemented automatically by the base class.
*/
public static final String PERMISSION_READ_EXTENSION_DATA
= "com.google.android.apps.dashclock.permission.READ_EXTENSION_DATA";
/**
* The protocol version with which the world readability option became available.
*
* @since Protocol Version 2 (API r2.x)
*/
private static final int PROTOCOL_VERSION_WORLD_READABILITY = 2;
private boolean mInitialized = false;
private boolean mIsWorldReadable = false;
private IExtensionHost mHost;
private volatile Looper mServiceLooper;
private volatile Handler mServiceHandler;
protected DashClockExtension() {
super();
}
@Override
public void onCreate() {
super.onCreate();
loadMetaData();
HandlerThread thread = new HandlerThread(
"DashClockExtension:" + getClass().getSimpleName());
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new Handler(mServiceLooper);
}
@Override
public void onDestroy() {
mServiceHandler.removeCallbacksAndMessages(null); // remove all callbacks
mServiceLooper.quit();
}
private void loadMetaData() {
PackageManager pm = getPackageManager();
try {
ServiceInfo si = pm.getServiceInfo(
new ComponentName(this, getClass()),
PackageManager.GET_META_DATA);
Bundle metaData = si.metaData;
if (metaData != null) {
int protocolVersion = metaData.getInt("protocolVersion");
mIsWorldReadable = protocolVersion >= PROTOCOL_VERSION_WORLD_READABILITY
&& metaData.getBoolean("worldReadable");
}
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Could not load metadata (e.g. world readable) for extension.");
}
}
@Override
public final IBinder onBind(Intent intent) {
return mBinder;
}
private IExtension.Stub mBinder = new IExtension.Stub() {
@Override
public void onInitialize(IExtensionHost host, boolean isReconnect)
throws RemoteException {
if (!mIsWorldReadable) {
// If not world readable, check the signature of the [first] package with the given
// UID against the known-good official DashClock app signature.
boolean verified = false;
PackageManager pm = getPackageManager();
String[] packages = pm.getPackagesForUid(getCallingUid());
if (packages != null && packages.length > 0) {
try {
PackageInfo pi = pm.getPackageInfo(packages[0],
PackageManager.GET_SIGNATURES);
if (pi.signatures != null
&& pi.signatures.length == 1
&& DashClockSignature.SIGNATURE.equals(pi.signatures[0])) {
verified = true;
}
} catch (PackageManager.NameNotFoundException ignored) {
}
}
if (!verified) {
Log.e(TAG, "Caller is not official DashClock app and this "
+ "extension is not world-readable.");
throw new SecurityException("Caller is not official DashClock app and this "
+ "extension is not world-readable.");
}
}
mHost = host;
if (!mInitialized) {
DashClockExtension.this.onInitialize(isReconnect);
mInitialized = true;
}
}
@Override
public void onUpdate(final int reason) throws RemoteException {
if (!mInitialized) {
return;
}
// Do this in a separate thread
mServiceHandler.post(new Runnable() {
@Override
public void run() {
DashClockExtension.this.onUpdateData(reason);
}
});
}
};
/**
* Called when a connection with the main DashClock app has been established or re-established
* after a previous one was lost. In this latter case, the parameter <code>isReconnect</code>
* will be true. Override this method to perform basic extension initialization before calls
* to {@link #onUpdateData(int)} are made. This method is called on the main thread.
*
* @param isReconnect Whether or not this call is being made after a connection was dropped and
* a new connection has been established.
*/
protected void onInitialize(boolean isReconnect) {
}
/**
* Called when the DashClock app process is requesting that the extension provide updated
* information to show to the user. Implementations can choose to do nothing, or more commonly,
* provide an update using the {@link #publishUpdate(ExtensionData)} method. Note that doing
* nothing doesn't clear existing data. To clear any existing data, call
* {@link #publishUpdate(ExtensionData)} with <code>null</code> data. This method is called
* on a background thread.
*
* @param reason The reason for the update. See {@link #UPDATE_REASON_PERIODIC} and related
* constants for more details.
*/
protected abstract void onUpdateData(int reason);
/**
* Notifies the main DashClock app that new data is available for the extension and should
* potentially be shown to the user. Note that this call does not necessarily need to be made
* from inside the {@link #onUpdateData(int)} method, but can be made only after
* {@link #onInitialize(boolean)} has been called. If you only call this from within
* {@link #onUpdateData(int)} this is already ensured.
*
* @param data The data to show, or <code>null</code> if existing data should be cleared (hiding
* the extension from view).
*/
protected final void publishUpdate(ExtensionData data) {
try {
mHost.publishUpdate(data);
} catch (RemoteException e) {
Log.e(TAG, "Couldn't publish updated extension data.", e);
}
}
/**
* Requests that the main DashClock app watch the given content URIs (using
* {@link android.content.ContentResolver#registerContentObserver(android.net.Uri, boolean,
* android.database.ContentObserver) ContentResolver.registerContentObserver})
* and call this extension's {@link #onUpdateData(int)} method when changes are observed.
* This should generally be called in the {@link #onInitialize(boolean)} method.
*
* @param uris The URIs to watch.
*/
protected final void addWatchContentUris(String[] uris) {
try {
mHost.addWatchContentUris(uris);
} catch (RemoteException e) {
Log.e(TAG, "Couldn't watch content URIs.", e);
}
}
/**
* Requests that the main DashClock app stop watching all content URIs previously registered
* with {@link #addWatchContentUris(String[])} for this extension.
*
* @since Protocol Version 2 (API r2.x)
*/
protected final void removeAllWatchContentUris() {
try {
mHost.removeAllWatchContentUris();
} catch (RemoteException e) {
Log.e(TAG, "Couldn't stop watching content URIs.", e);
}
}
/**
* Requests that the main DashClock app call (or not call) this extension's
* {@link #onUpdateData(int)} method when the screen turns on (the phone resumes from idle).
* This should generally be called in the {@link #onInitialize(boolean)} method. By default,
* extensions do not get updated when the screen turns on.
*
* @see Intent#ACTION_SCREEN_ON
* @param updateWhenScreenOn Whether or not a call to {@link #onUpdateData(int)} method when
* the screen turns on.
*/
protected final void setUpdateWhenScreenOn(boolean updateWhenScreenOn) {
try {
mHost.setUpdateWhenScreenOn(updateWhenScreenOn);
} catch (RemoteException e) {
Log.e(TAG, "Couldn't set the extension to update upon ACTION_SCREEN_ON.", e);
}
}
}