1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package android.app.job;
18 
19 import android.app.Service;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.os.Handler;
23 import android.os.IBinder;
24 import android.os.Looper;
25 import android.os.Message;
26 import android.os.RemoteException;
27 import android.util.Log;
28 
29 import com.android.internal.annotations.GuardedBy;
30 
31 import java.lang.ref.WeakReference;
32 
33 /**
34  * Helper for implementing a {@link android.app.Service} that interacts with
35  * {@link JobScheduler}.  This is not intended for use by regular applications, but
36  * allows frameworks built on top of the platform to create their own
37  * {@link android.app.Service} that interact with {@link JobScheduler} as well as
38  * add in additional functionality.  If you just want to execute jobs normally, you
39  * should instead be looking at {@link JobService}.
40  */
41 public abstract class JobServiceEngine {
42     private static final String TAG = "JobServiceEngine";
43 
44     /**
45      * Identifier for a message that will result in a call to
46      * {@link #onStartJob(android.app.job.JobParameters)}.
47      */
48     private static final int MSG_EXECUTE_JOB = 0;
49     /**
50      * Message that will result in a call to {@link #onStopJob(android.app.job.JobParameters)}.
51      */
52     private static final int MSG_STOP_JOB = 1;
53     /**
54      * Message that the client has completed execution of this job.
55      */
56     private static final int MSG_JOB_FINISHED = 2;
57 
58     private final IJobService mBinder;
59 
60     /**
61      * Handler we post jobs to. Responsible for calling into the client logic, and handling the
62      * callback to the system.
63      */
64     JobHandler mHandler;
65 
66     static final class JobInterface extends IJobService.Stub {
67         final WeakReference<JobServiceEngine> mService;
68 
JobInterface(JobServiceEngine service)69         JobInterface(JobServiceEngine service) {
70             mService = new WeakReference<>(service);
71         }
72 
73         @Override
startJob(JobParameters jobParams)74         public void startJob(JobParameters jobParams) throws RemoteException {
75             JobServiceEngine service = mService.get();
76             if (service != null) {
77                 Message m = Message.obtain(service.mHandler, MSG_EXECUTE_JOB, jobParams);
78                 m.sendToTarget();
79             }
80         }
81 
82         @Override
stopJob(JobParameters jobParams)83         public void stopJob(JobParameters jobParams) throws RemoteException {
84             JobServiceEngine service = mService.get();
85             if (service != null) {
86                 Message m = Message.obtain(service.mHandler, MSG_STOP_JOB, jobParams);
87                 m.sendToTarget();
88             }
89         }
90     }
91 
92     /**
93      * Runs on application's main thread - callbacks are meant to offboard work to some other
94      * (app-specified) mechanism.
95      * @hide
96      */
97     class JobHandler extends Handler {
JobHandler(Looper looper)98         JobHandler(Looper looper) {
99             super(looper);
100         }
101 
102         @Override
handleMessage(Message msg)103         public void handleMessage(Message msg) {
104             final JobParameters params = (JobParameters) msg.obj;
105             switch (msg.what) {
106                 case MSG_EXECUTE_JOB:
107                     try {
108                         boolean workOngoing = JobServiceEngine.this.onStartJob(params);
109                         ackStartMessage(params, workOngoing);
110                     } catch (Exception e) {
111                         Log.e(TAG, "Error while executing job: " + params.getJobId());
112                         throw new RuntimeException(e);
113                     }
114                     break;
115                 case MSG_STOP_JOB:
116                     try {
117                         boolean ret = JobServiceEngine.this.onStopJob(params);
118                         ackStopMessage(params, ret);
119                     } catch (Exception e) {
120                         Log.e(TAG, "Application unable to handle onStopJob.", e);
121                         throw new RuntimeException(e);
122                     }
123                     break;
124                 case MSG_JOB_FINISHED:
125                     final boolean needsReschedule = (msg.arg2 == 1);
126                     IJobCallback callback = params.getCallback();
127                     if (callback != null) {
128                         try {
129                             callback.jobFinished(params.getJobId(), needsReschedule);
130                         } catch (RemoteException e) {
131                             Log.e(TAG, "Error reporting job finish to system: binder has gone" +
132                                     "away.");
133                         }
134                     } else {
135                         Log.e(TAG, "finishJob() called for a nonexistent job id.");
136                     }
137                     break;
138                 default:
139                     Log.e(TAG, "Unrecognised message received.");
140                     break;
141             }
142         }
143 
ackStartMessage(JobParameters params, boolean workOngoing)144         private void ackStartMessage(JobParameters params, boolean workOngoing) {
145             final IJobCallback callback = params.getCallback();
146             final int jobId = params.getJobId();
147             if (callback != null) {
148                 try {
149                     callback.acknowledgeStartMessage(jobId, workOngoing);
150                 } catch(RemoteException e) {
151                     Log.e(TAG, "System unreachable for starting job.");
152                 }
153             } else {
154                 if (Log.isLoggable(TAG, Log.DEBUG)) {
155                     Log.d(TAG, "Attempting to ack a job that has already been processed.");
156                 }
157             }
158         }
159 
ackStopMessage(JobParameters params, boolean reschedule)160         private void ackStopMessage(JobParameters params, boolean reschedule) {
161             final IJobCallback callback = params.getCallback();
162             final int jobId = params.getJobId();
163             if (callback != null) {
164                 try {
165                     callback.acknowledgeStopMessage(jobId, reschedule);
166                 } catch(RemoteException e) {
167                     Log.e(TAG, "System unreachable for stopping job.");
168                 }
169             } else {
170                 if (Log.isLoggable(TAG, Log.DEBUG)) {
171                     Log.d(TAG, "Attempting to ack a job that has already been processed.");
172                 }
173             }
174         }
175     }
176 
177     /**
178      * Create a new engine, ready for use.
179      *
180      * @param service The {@link Service} that is creating this engine and in which it will run.
181      */
JobServiceEngine(Service service)182     public JobServiceEngine(Service service) {
183         mBinder = new JobInterface(this);
184         mHandler = new JobHandler(service.getMainLooper());
185     }
186 
187     /**
188      * Retrieve the engine's IPC interface that should be returned by
189      * {@link Service#onBind(Intent)}.
190      */
getBinder()191     public final IBinder getBinder() {
192         return mBinder.asBinder();
193     }
194 
195     /**
196      * Engine's report that a job has started.  See
197      * {@link JobService#onStartJob(JobParameters) JobService.onStartJob} for more information.
198      */
onStartJob(JobParameters params)199     public abstract boolean onStartJob(JobParameters params);
200 
201     /**
202      * Engine's report that a job has stopped.  See
203      * {@link JobService#onStopJob(JobParameters) JobService.onStopJob} for more information.
204      */
onStopJob(JobParameters params)205     public abstract boolean onStopJob(JobParameters params);
206 
207     /**
208      * Call in to engine to report that a job has finished executing.  See
209      * {@link JobService#jobFinished(JobParameters, boolean)}  JobService.jobFinished} for more
210      * information.
211      */
jobFinished(JobParameters params, boolean needsReschedule)212     public void jobFinished(JobParameters params, boolean needsReschedule) {
213         if (params == null) {
214             throw new NullPointerException("params");
215         }
216         Message m = Message.obtain(mHandler, MSG_JOB_FINISHED, params);
217         m.arg2 = needsReschedule ? 1 : 0;
218         m.sendToTarget();
219     }
220 }