1 /*
2  * Copyright (C) 2015 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 com.android.messaging.datamodel.action;
18 
19 import android.app.AlarmManager;
20 import android.app.PendingIntent;
21 import android.content.BroadcastReceiver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.os.Bundle;
25 import android.os.SystemClock;
26 
27 import androidx.core.app.JobIntentService;
28 
29 import com.android.messaging.Factory;
30 import com.android.messaging.datamodel.DataModel;
31 import com.android.messaging.util.LogUtil;
32 import com.android.messaging.util.LoggingTimer;
33 import com.google.common.annotations.VisibleForTesting;
34 
35 /**
36  * ActionService used to perform background processing for data model
37  */
38 public class ActionServiceImpl extends JobIntentService {
39     private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
40     private static final boolean VERBOSE = false;
41 
42     /**
43      * Unique job ID for this service.
44      */
45     public static final int JOB_ID = 1000;
46 
ActionServiceImpl()47     public ActionServiceImpl() {
48         super();
49     }
50 
51     /**
52      * Start action by sending intent to the service
53      * @param action - action to start
54      */
startAction(final Action action)55     protected static void startAction(final Action action) {
56         final Intent intent = makeIntent(OP_START_ACTION);
57         final Bundle actionBundle = new Bundle();
58         actionBundle.putParcelable(BUNDLE_ACTION, action);
59         intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
60         action.markStart();
61         startServiceWithIntent(intent);
62     }
63 
64     /**
65      * Schedule an action to run after specified delay using alarm manager to send pendingintent
66      * @param action - action to start
67      * @param requestCode - request code used to collapse requests
68      * @param delayMs - delay in ms (from now) before action will start
69      */
scheduleAction(final Action action, final int requestCode, final long delayMs)70     protected static void scheduleAction(final Action action, final int requestCode,
71             final long delayMs) {
72         final Intent intent = PendingActionReceiver.makeIntent(OP_START_ACTION);
73         final Bundle actionBundle = new Bundle();
74         actionBundle.putParcelable(BUNDLE_ACTION, action);
75         intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
76 
77         PendingActionReceiver.scheduleAlarm(intent, requestCode, delayMs);
78     }
79 
80     /**
81      * Handle response returned by BackgroundWorker
82      * @param request - request generating response
83      * @param response - response from service
84      */
handleResponseFromBackgroundWorker(final Action action, final Bundle response)85     protected static void handleResponseFromBackgroundWorker(final Action action,
86             final Bundle response) {
87         final Intent intent = makeIntent(OP_RECEIVE_BACKGROUND_RESPONSE);
88 
89         final Bundle actionBundle = new Bundle();
90         actionBundle.putParcelable(BUNDLE_ACTION, action);
91         intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
92         intent.putExtra(EXTRA_WORKER_RESPONSE, response);
93 
94         startServiceWithIntent(intent);
95     }
96 
97     /**
98      * Handle response returned by BackgroundWorker
99      * @param request - request generating failure
100      */
handleFailureFromBackgroundWorker(final Action action, final Exception exception)101     protected static void handleFailureFromBackgroundWorker(final Action action,
102             final Exception exception) {
103         final Intent intent = makeIntent(OP_RECEIVE_BACKGROUND_FAILURE);
104 
105         final Bundle actionBundle = new Bundle();
106         actionBundle.putParcelable(BUNDLE_ACTION, action);
107         intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
108         intent.putExtra(EXTRA_WORKER_EXCEPTION, exception);
109 
110         startServiceWithIntent(intent);
111     }
112 
113     // ops
114     @VisibleForTesting
115     protected static final int OP_START_ACTION = 200;
116     @VisibleForTesting
117     protected static final int OP_RECEIVE_BACKGROUND_RESPONSE = 201;
118     @VisibleForTesting
119     protected static final int OP_RECEIVE_BACKGROUND_FAILURE = 202;
120 
121     // extras
122     @VisibleForTesting
123     protected static final String EXTRA_OP_CODE = "op";
124     @VisibleForTesting
125     protected static final String EXTRA_ACTION_BUNDLE = "datamodel_action_bundle";
126     @VisibleForTesting
127     protected static final String EXTRA_WORKER_EXCEPTION = "worker_exception";
128     @VisibleForTesting
129     protected static final String EXTRA_WORKER_RESPONSE = "worker_response";
130     @VisibleForTesting
131     protected static final String EXTRA_WORKER_UPDATE = "worker_update";
132     @VisibleForTesting
133     protected static final String BUNDLE_ACTION = "bundle_action";
134 
135     private BackgroundWorker mBackgroundWorker;
136 
137     /**
138      * Allocate an intent with a specific opcode.
139      */
makeIntent(final int opcode)140     private static Intent makeIntent(final int opcode) {
141         final Intent intent = new Intent(Factory.get().getApplicationContext(),
142                 ActionServiceImpl.class);
143         intent.putExtra(EXTRA_OP_CODE, opcode);
144         return intent;
145     }
146 
147     /**
148      * Broadcast receiver for alarms scheduled through ActionService.
149      */
150     public static class PendingActionReceiver extends BroadcastReceiver {
151         static final String ACTION = "com.android.messaging.datamodel.PENDING_ACTION";
152 
153         /**
154          * Allocate an intent with a specific opcode and alarm action.
155          */
makeIntent(final int opcode)156         public static Intent makeIntent(final int opcode) {
157             final Intent intent = new Intent(Factory.get().getApplicationContext(),
158                     PendingActionReceiver.class);
159             intent.setAction(ACTION);
160             intent.putExtra(EXTRA_OP_CODE, opcode);
161             return intent;
162         }
163 
scheduleAlarm(final Intent intent, final int requestCode, final long delayMs)164         public static void scheduleAlarm(final Intent intent, final int requestCode,
165                 final long delayMs) {
166             final Context context = Factory.get().getApplicationContext();
167             final PendingIntent pendingIntent = PendingIntent.getBroadcast(
168                     context, requestCode, intent, PendingIntent.FLAG_CANCEL_CURRENT);
169 
170             final AlarmManager mgr =
171                     (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
172 
173             if (delayMs < Long.MAX_VALUE) {
174                 mgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
175                         SystemClock.elapsedRealtime() + delayMs, pendingIntent);
176             } else {
177                 mgr.cancel(pendingIntent);
178             }
179         }
180 
181         /**
182          * {@inheritDoc}
183          */
184         @Override
onReceive(final Context context, final Intent intent)185         public void onReceive(final Context context, final Intent intent) {
186             ActionServiceImpl.startServiceWithIntent(intent);
187         }
188     }
189 
190     /**
191      * Creates a pending intent that will trigger a data model action when the intent is
192      * triggered
193      */
makeStartActionPendingIntent(final Context context, final Action action, final int requestCode, final boolean launchesAnActivity)194     public static PendingIntent makeStartActionPendingIntent(final Context context,
195             final Action action, final int requestCode, final boolean launchesAnActivity) {
196         final Intent intent = PendingActionReceiver.makeIntent(OP_START_ACTION);
197         final Bundle actionBundle = new Bundle();
198         actionBundle.putParcelable(BUNDLE_ACTION, action);
199         intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
200         if (launchesAnActivity) {
201             intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
202         }
203         return PendingIntent.getBroadcast(context, requestCode, intent,
204                 PendingIntent.FLAG_UPDATE_CURRENT);
205     }
206 
207     /**
208      * {@inheritDoc}
209      */
210     @Override
onCreate()211     public void onCreate() {
212         super.onCreate();
213         mBackgroundWorker = DataModel.get().getBackgroundWorkerForActionService();
214     }
215 
216     @Override
onDestroy()217     public void onDestroy() {
218         super.onDestroy();
219     }
220 
221     /**
222      * Queue intent to the ActionService.
223      */
startServiceWithIntent(final Intent intent)224     private static void startServiceWithIntent(final Intent intent) {
225         final Context context = Factory.get().getApplicationContext();
226         final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0);
227         intent.setClass(context, ActionServiceImpl.class);
228         enqueueWork(context, intent);
229     }
230 
enqueueWork(Context context, Intent work)231     public static void enqueueWork(Context context, Intent work) {
232         enqueueWork(context, ActionServiceImpl.class, JOB_ID, work);
233     }
234 
235     /**
236      * {@inheritDoc}
237      */
238     @Override
onHandleWork(final Intent intent)239     protected void onHandleWork(final Intent intent) {
240         if (intent == null) {
241             // Shouldn't happen but sometimes does following another crash.
242             LogUtil.w(TAG, "ActionService.onHandleIntent: Called with null intent");
243             return;
244         }
245         final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0);
246 
247         Action action;
248         final Bundle actionBundle = intent.getBundleExtra(EXTRA_ACTION_BUNDLE);
249         actionBundle.setClassLoader(getClassLoader());
250         switch(opcode) {
251             case OP_START_ACTION: {
252                 action = (Action) actionBundle.getParcelable(BUNDLE_ACTION);
253                 executeAction(action);
254                 break;
255             }
256 
257             case OP_RECEIVE_BACKGROUND_RESPONSE: {
258                 action = (Action) actionBundle.getParcelable(BUNDLE_ACTION);
259                 final Bundle response = intent.getBundleExtra(EXTRA_WORKER_RESPONSE);
260                 processBackgroundResponse(action, response);
261                 break;
262             }
263 
264             case OP_RECEIVE_BACKGROUND_FAILURE: {
265                 action = (Action) actionBundle.getParcelable(BUNDLE_ACTION);
266                 processBackgroundFailure(action);
267                 break;
268             }
269 
270             default:
271                 throw new RuntimeException("Unrecognized opcode in ActionServiceImpl");
272         }
273 
274         action.sendBackgroundActions(mBackgroundWorker);
275     }
276 
277     private static final long EXECUTION_TIME_WARN_LIMIT_MS = 1000; // 1 second
278     /**
279      * Local execution of action on ActionService thread
280      */
executeAction(final Action action)281     private void executeAction(final Action action) {
282         action.markBeginExecute();
283 
284         final LoggingTimer timer = createLoggingTimer(action, "#executeAction");
285         timer.start();
286 
287         final Object result = action.executeAction();
288 
289         timer.stopAndLog();
290 
291         action.markEndExecute(result);
292     }
293 
294     /**
295      * Process response on ActionService thread
296      */
processBackgroundResponse(final Action action, final Bundle response)297     private void processBackgroundResponse(final Action action, final Bundle response) {
298         final LoggingTimer timer = createLoggingTimer(action, "#processBackgroundResponse");
299         timer.start();
300 
301         action.processBackgroundWorkResponse(response);
302 
303         timer.stopAndLog();
304     }
305 
306     /**
307      * Process failure on ActionService thread
308      */
processBackgroundFailure(final Action action)309     private void processBackgroundFailure(final Action action) {
310         final LoggingTimer timer = createLoggingTimer(action, "#processBackgroundFailure");
311         timer.start();
312 
313         action.processBackgroundWorkFailure();
314 
315         timer.stopAndLog();
316     }
317 
createLoggingTimer( final Action action, final String methodName)318     private static LoggingTimer createLoggingTimer(
319             final Action action, final String methodName) {
320         return new LoggingTimer(TAG, action.getClass().getSimpleName() + methodName,
321                 EXECUTION_TIME_WARN_LIMIT_MS);
322     }
323 }
324