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