/
DashClockExtension.java
408 lines (372 loc) · 16.9 KB
/
DashClockExtension.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
/*
* 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);
}
}
}