Skip to content

Commit aab7ae5

Browse files
natario1rogerhu
authored andcommitted
Use JobScheduler for push (#735)
* Use JobScheduler for push * Rearrange tests * Rename PushService26 to PushServiceApi26
1 parent dfc41e8 commit aab7ae5

File tree

10 files changed

+302
-152
lines changed

10 files changed

+302
-152
lines changed

Parse/src/main/AndroidManifest.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,11 @@
1313
<uses-permission android:name="android.permission.INTERNET"/>
1414
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
1515

16-
<application />
16+
<application>
17+
<service
18+
android:name=".PushServiceApi26"
19+
android:permission="android.permission.BIND_JOB_SERVICE"
20+
android:exported="true"/>
21+
</application>
22+
1723
</manifest>

Parse/src/main/java/com/parse/GcmBroadcastReceiver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ public class GcmBroadcastReceiver extends BroadcastReceiver {
2020
@Override
2121
@CallSuper
2222
public void onReceive(Context context, Intent intent) {
23-
ServiceUtils.runWakefulIntentInService(context, intent, PushService.class);
23+
PushServiceUtils.runService(context, intent);
2424
}
2525
}

Parse/src/main/java/com/parse/ManifestInfo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ private static PushType findPushType() {
180180
return PushType.NONE;
181181
}
182182

183-
if (!PushService.isSupported()) {
183+
if (!PushServiceUtils.isSupported()) {
184184
return PushType.NONE;
185185
}
186186

Parse/src/main/java/com/parse/Parse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ public Void call() throws Exception {
413413

414414
// May need to update GCM registration ID if app version has changed.
415415
// This also primes current installation.
416-
PushService.initialize().continueWithTask(new Continuation<Void, Task<Void>>() {
416+
PushServiceUtils.initialize().continueWithTask(new Continuation<Void, Task<Void>>() {
417417
@Override
418418
public Task<Void> then(Task<Void> task) throws Exception {
419419
// Prime current user in the background

Parse/src/main/java/com/parse/PushService.java

Lines changed: 65 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@
99
package com.parse;
1010

1111
import android.app.Service;
12+
import android.content.ComponentName;
1213
import android.content.Context;
1314
import android.content.Intent;
1415
import android.os.IBinder;
16+
import android.os.PowerManager;
17+
import android.util.SparseArray;
1518

1619
import java.util.ArrayList;
1720
import java.util.List;
18-
import java.util.concurrent.Executor;
1921
import java.util.concurrent.ExecutorService;
2022
import java.util.concurrent.Executors;
2123

22-
import bolts.Task;
23-
2424
/**
2525
* A service to listen for push notifications. This operates in the same process as the parent
2626
* application.
@@ -81,10 +81,63 @@
8181
* The {@link ParsePushBroadcastReceiver} listens to this intent to track an app open event and
8282
* launch the app's launcher activity. To customize this behavior override
8383
* {@link ParsePushBroadcastReceiver#onPushOpen(Context, Intent)}.
84+
*
85+
* Starting with Android O, this is replaced by {@link PushServiceApi26}.
8486
*/
8587
public final class PushService extends Service {
8688
private static final String TAG = "com.parse.PushService";
8789

90+
//region run and dispose
91+
92+
private static final String WAKE_LOCK_EXTRA = "parseWakeLockId";
93+
private static final SparseArray<ParseWakeLock> wakeLocks = new SparseArray<>();
94+
private static int wakeLockId = 0;
95+
96+
/*
97+
* Same as Context.startService, but acquires a wake lock before starting the service. The wake
98+
* lock must later be released by calling dispose().
99+
*/
100+
static boolean run(Context context, Intent intent) {
101+
String reason = intent.toString();
102+
ParseWakeLock wl = ParseWakeLock.acquireNewWakeLock(context, PowerManager.PARTIAL_WAKE_LOCK, reason, 0);
103+
104+
synchronized (wakeLocks) {
105+
intent.putExtra(WAKE_LOCK_EXTRA, wakeLockId);
106+
wakeLocks.append(wakeLockId, wl);
107+
wakeLockId++;
108+
}
109+
110+
intent.setClass(context, PushService.class);
111+
ComponentName name = context.startService(intent);
112+
if (name == null) {
113+
PLog.e(TAG, "Could not start the service. Make sure that the XML tag "
114+
+ "<service android:name=\"" + PushService.class + "\" /> is in your "
115+
+ "AndroidManifest.xml as a child of the <application> element.");
116+
dispose(intent);
117+
return false;
118+
}
119+
return true;
120+
}
121+
122+
static void dispose(Intent intent) {
123+
if (intent != null && intent.hasExtra(WAKE_LOCK_EXTRA)) {
124+
int id = intent.getIntExtra(WAKE_LOCK_EXTRA, -1);
125+
ParseWakeLock wakeLock;
126+
127+
synchronized (wakeLocks) {
128+
wakeLock = wakeLocks.get(id);
129+
wakeLocks.remove(id);
130+
}
131+
132+
if (wakeLock == null) {
133+
PLog.e(TAG, "Got wake lock id of " + id + " in intent, but no such lock found in " +
134+
"global map. Was disposePushService called twice for the same intent?");
135+
} else {
136+
wakeLock.release();
137+
}
138+
}
139+
}
140+
88141
//region ServiceLifecycleCallbacks used for testing
89142

90143
private static List<ServiceLifecycleCallbacks> serviceLifecycleCallbacks = null;
@@ -103,45 +156,26 @@ public final class PushService extends Service {
103156
}
104157
}
105158

106-
/* package */ static void unregisterServiceLifecycleCallbacks(
107-
ServiceLifecycleCallbacks callbacks) {
159+
/* package */ static void unregisterServiceLifecycleCallbacks(ServiceLifecycleCallbacks callbacks) {
108160
synchronized (PushService.class) {
109161
serviceLifecycleCallbacks.remove(callbacks);
110-
if (serviceLifecycleCallbacks.size() <= 0) {
111-
serviceLifecycleCallbacks = null;
112-
}
113162
}
114163
}
115164

116165
private static void dispatchOnServiceCreated(Service service) {
117-
Object[] callbacks = collectServiceLifecycleCallbacks();
118-
if (callbacks != null) {
119-
for (Object callback : callbacks) {
120-
((ServiceLifecycleCallbacks) callback).onServiceCreated(service);
166+
if (serviceLifecycleCallbacks != null) {
167+
for (ServiceLifecycleCallbacks callback : serviceLifecycleCallbacks) {
168+
callback.onServiceCreated(service);
121169
}
122170
}
123171
}
124172

125173
private static void dispatchOnServiceDestroyed(Service service) {
126-
Object[] callbacks = collectServiceLifecycleCallbacks();
127-
if (callbacks != null) {
128-
for (Object callback : callbacks) {
129-
((ServiceLifecycleCallbacks) callback).onServiceDestroyed(service);
130-
}
131-
}
132-
}
133-
134-
private static Object[] collectServiceLifecycleCallbacks() {
135-
Object[] callbacks = null;
136-
synchronized (PushService.class) {
137-
if (serviceLifecycleCallbacks == null) {
138-
return null;
139-
}
140-
if (serviceLifecycleCallbacks.size() > 0) {
141-
callbacks = serviceLifecycleCallbacks.toArray();
174+
if (serviceLifecycleCallbacks != null) {
175+
for (ServiceLifecycleCallbacks callback : serviceLifecycleCallbacks) {
176+
callback.onServiceDestroyed(service);
142177
}
143178
}
144-
return callbacks;
145179
}
146180

147181
//endregion
@@ -157,10 +191,6 @@ public PushService() {
157191
super();
158192
}
159193

160-
static PushHandler createPushHandler() {
161-
return PushHandler.Factory.create(ManifestInfo.getPushType());
162-
}
163-
164194
// For tests
165195
void setPushHandler(PushHandler handler) {
166196
this.handler = handler;
@@ -174,10 +204,6 @@ static boolean isSupported() {
174204
return ManifestInfo.getServiceInfo(PushService.class) != null;
175205
}
176206

177-
static Task<Void> initialize() {
178-
// Some handlers might need initialization.
179-
return createPushHandler().initialize();
180-
}
181207

182208
/**
183209
* Client code should not call {@code onCreate} directly.
@@ -197,7 +223,7 @@ public void onCreate() {
197223
}
198224

199225
executor = Executors.newSingleThreadExecutor();
200-
handler = createPushHandler();
226+
handler = PushServiceUtils.createPushHandler();
201227
dispatchOnServiceCreated(this);
202228
}
203229

@@ -216,7 +242,7 @@ public void run() {
216242
try {
217243
handler.handlePush(intent);
218244
} finally {
219-
ServiceUtils.completeWakefulIntent(intent);
245+
dispose(intent);
220246
stopSelf(startId);
221247
}
222248
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright (c) 2015-present, Parse, LLC.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
package com.parse;
10+
11+
import android.annotation.TargetApi;
12+
import android.app.Service;
13+
import android.app.job.JobInfo;
14+
import android.app.job.JobParameters;
15+
import android.app.job.JobScheduler;
16+
import android.app.job.JobService;
17+
import android.content.ComponentName;
18+
import android.content.Context;
19+
import android.content.Intent;
20+
import android.os.Build;
21+
import android.os.Bundle;
22+
import android.os.IBinder;
23+
import android.os.PowerManager;
24+
import android.util.SparseArray;
25+
26+
import java.util.ArrayList;
27+
import java.util.List;
28+
import java.util.concurrent.ExecutorService;
29+
import java.util.concurrent.Executors;
30+
31+
/**
32+
* A JobService that is triggered by push notifications on Oreo+.
33+
* Read {@link PushServiceUtils} and {@link PushService} for info and docs.
34+
* This is already set-up in our own manifest.
35+
*/
36+
@TargetApi(Build.VERSION_CODES.O)
37+
public final class PushServiceApi26 extends JobService {
38+
private static final String TAG = PushServiceApi26.class.getSimpleName();
39+
private static final String INTENT_KEY = "intent";
40+
private static final int JOB_SERVICE_ID = 999;
41+
42+
static boolean run(Context context, Intent intent) {
43+
JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
44+
// Execute in the next second.
45+
Bundle extra = new Bundle(1);
46+
extra.putParcelable(INTENT_KEY, intent);
47+
int did = scheduler.schedule(new JobInfo.Builder(JOB_SERVICE_ID, new ComponentName(context, PushService.class))
48+
.setMinimumLatency(1L)
49+
.setOverrideDeadline(1000L)
50+
.setRequiresCharging(false)
51+
.setRequiresBatteryNotLow(false)
52+
.setRequiresStorageNotLow(false)
53+
.setTransientExtras(extra)
54+
.build());
55+
return did == JobScheduler.RESULT_SUCCESS;
56+
}
57+
58+
// We delegate the intent to a PushHandler running in a streamlined executor.
59+
private ExecutorService executor;
60+
private PushHandler handler;
61+
62+
// Our manifest file is OK.
63+
static boolean isSupported() {
64+
return true;
65+
}
66+
67+
@Override
68+
public void onCreate() {
69+
super.onCreate();
70+
executor = Executors.newSingleThreadExecutor();
71+
handler = PushServiceUtils.createPushHandler();
72+
}
73+
74+
@Override
75+
public boolean onStartJob(final JobParameters jobParameters) {
76+
if (ParsePlugins.Android.get() == null) {
77+
PLog.e(TAG, "The Parse push service cannot start because Parse.initialize "
78+
+ "has not yet been called. If you call Parse.initialize from "
79+
+ "an Activity's onCreate, that call should instead be in the "
80+
+ "Application.onCreate. Be sure your Application class is registered "
81+
+ "in your AndroidManifest.xml with the android:name property of your "
82+
+ "<application> tag.");
83+
return false;
84+
}
85+
86+
final Bundle params = jobParameters.getTransientExtras();
87+
final Intent intent = params.getParcelable(INTENT_KEY);
88+
executor.execute(new Runnable() {
89+
@Override
90+
public void run() {
91+
try {
92+
handler.handlePush(intent);
93+
} finally {
94+
jobFinished(jobParameters, false);
95+
}
96+
}
97+
});
98+
return true;
99+
}
100+
101+
@Override
102+
public boolean onStopJob(JobParameters jobParameters) {
103+
// Something went wrong before jobFinished(). Try rescheduling.
104+
return true;
105+
}
106+
107+
@Override
108+
public void onDestroy() {
109+
if (executor != null) {
110+
executor.shutdown();
111+
executor = null;
112+
handler = null;
113+
}
114+
super.onDestroy();
115+
}
116+
}

0 commit comments

Comments
 (0)