/* * Copyright (C) 2015 The Android Open Source Project * * 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.android.messaging.datamodel.action; import android.app.AlarmManager; import android.app.IntentService; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.SystemClock; import com.android.messaging.Factory; import com.android.messaging.datamodel.DataModel; import com.android.messaging.util.LogUtil; import com.android.messaging.util.LoggingTimer; import com.android.messaging.util.WakeLockHelper; import com.google.common.annotations.VisibleForTesting; /** * ActionService used to perform background processing for data model */ public class ActionServiceImpl extends IntentService { private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG; private static final boolean VERBOSE = false; public ActionServiceImpl() { super("ActionService"); } /** * Start action by sending intent to the service * @param action - action to start */ protected static void startAction(final Action action) { final Intent intent = makeIntent(OP_START_ACTION); final Bundle actionBundle = new Bundle(); actionBundle.putParcelable(BUNDLE_ACTION, action); intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle); action.markStart(); startServiceWithIntent(intent); } /** * Schedule an action to run after specified delay using alarm manager to send pendingintent * @param action - action to start * @param requestCode - request code used to collapse requests * @param delayMs - delay in ms (from now) before action will start */ protected static void scheduleAction(final Action action, final int requestCode, final long delayMs) { final Intent intent = PendingActionReceiver.makeIntent(OP_START_ACTION); final Bundle actionBundle = new Bundle(); actionBundle.putParcelable(BUNDLE_ACTION, action); intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle); PendingActionReceiver.scheduleAlarm(intent, requestCode, delayMs); } /** * Handle response returned by BackgroundWorker * @param request - request generating response * @param response - response from service */ protected static void handleResponseFromBackgroundWorker(final Action action, final Bundle response) { final Intent intent = makeIntent(OP_RECEIVE_BACKGROUND_RESPONSE); final Bundle actionBundle = new Bundle(); actionBundle.putParcelable(BUNDLE_ACTION, action); intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle); intent.putExtra(EXTRA_WORKER_RESPONSE, response); startServiceWithIntent(intent); } /** * Handle response returned by BackgroundWorker * @param request - request generating failure */ protected static void handleFailureFromBackgroundWorker(final Action action, final Exception exception) { final Intent intent = makeIntent(OP_RECEIVE_BACKGROUND_FAILURE); final Bundle actionBundle = new Bundle(); actionBundle.putParcelable(BUNDLE_ACTION, action); intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle); intent.putExtra(EXTRA_WORKER_EXCEPTION, exception); startServiceWithIntent(intent); } // ops @VisibleForTesting protected static final int OP_START_ACTION = 200; @VisibleForTesting protected static final int OP_RECEIVE_BACKGROUND_RESPONSE = 201; @VisibleForTesting protected static final int OP_RECEIVE_BACKGROUND_FAILURE = 202; // extras @VisibleForTesting protected static final String EXTRA_OP_CODE = "op"; @VisibleForTesting protected static final String EXTRA_ACTION_BUNDLE = "datamodel_action_bundle"; @VisibleForTesting protected static final String EXTRA_WORKER_EXCEPTION = "worker_exception"; @VisibleForTesting protected static final String EXTRA_WORKER_RESPONSE = "worker_response"; @VisibleForTesting protected static final String EXTRA_WORKER_UPDATE = "worker_update"; @VisibleForTesting protected static final String BUNDLE_ACTION = "bundle_action"; private BackgroundWorker mBackgroundWorker; /** * Allocate an intent with a specific opcode. */ private static Intent makeIntent(final int opcode) { final Intent intent = new Intent(Factory.get().getApplicationContext(), ActionServiceImpl.class); intent.putExtra(EXTRA_OP_CODE, opcode); return intent; } /** * Broadcast receiver for alarms scheduled through ActionService. */ public static class PendingActionReceiver extends BroadcastReceiver { static final String ACTION = "com.android.messaging.datamodel.PENDING_ACTION"; /** * Allocate an intent with a specific opcode and alarm action. */ public static Intent makeIntent(final int opcode) { final Intent intent = new Intent(Factory.get().getApplicationContext(), PendingActionReceiver.class); intent.setAction(ACTION); intent.putExtra(EXTRA_OP_CODE, opcode); return intent; } public static void scheduleAlarm(final Intent intent, final int requestCode, final long delayMs) { final Context context = Factory.get().getApplicationContext(); final PendingIntent pendingIntent = PendingIntent.getBroadcast( context, requestCode, intent, PendingIntent.FLAG_CANCEL_CURRENT); final AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); if (delayMs < Long.MAX_VALUE) { mgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + delayMs, pendingIntent); } else { mgr.cancel(pendingIntent); } } /** * {@inheritDoc} */ @Override public void onReceive(final Context context, final Intent intent) { ActionServiceImpl.startServiceWithIntent(intent); } } /** * Creates a pending intent that will trigger a data model action when the intent is * triggered */ public static PendingIntent makeStartActionPendingIntent(final Context context, final Action action, final int requestCode, final boolean launchesAnActivity) { final Intent intent = PendingActionReceiver.makeIntent(OP_START_ACTION); final Bundle actionBundle = new Bundle(); actionBundle.putParcelable(BUNDLE_ACTION, action); intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle); if (launchesAnActivity) { intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); } return PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); } /** * {@inheritDoc} */ @Override public void onCreate() { super.onCreate(); mBackgroundWorker = DataModel.get().getBackgroundWorkerForActionService(); DataModel.get().getConnectivityUtil().registerForSignalStrength(); } @Override public void onDestroy() { super.onDestroy(); DataModel.get().getConnectivityUtil().unregisterForSignalStrength(); } private static final String WAKELOCK_ID = "bugle_datamodel_service_wakelock"; @VisibleForTesting static WakeLockHelper sWakeLock = new WakeLockHelper(WAKELOCK_ID); /** * Queue intent to the ActionService after acquiring wake lock */ private static void startServiceWithIntent(final Intent intent) { final Context context = Factory.get().getApplicationContext(); final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0); // Increase refCount on wake lock - acquiring if necessary if (VERBOSE) { LogUtil.v(TAG, "acquiring wakelock for opcode " + opcode); } sWakeLock.acquire(context, intent, opcode); intent.setClass(context, ActionServiceImpl.class); // TODO: Note that intent will be quietly discarded if it exceeds available rpc // memory (in total around 1MB). See this article for background // http://developer.android.com/reference/android/os/TransactionTooLargeException.html // Perhaps we should keep large structures in the action monitor? if (context.startService(intent) == null) { LogUtil.e(TAG, "ActionService.startServiceWithIntent: failed to start service for intent " + intent); sWakeLock.release(intent, opcode); } } /** * {@inheritDoc} */ @Override protected void onHandleIntent(final Intent intent) { if (intent == null) { // Shouldn't happen but sometimes does following another crash. LogUtil.w(TAG, "ActionService.onHandleIntent: Called with null intent"); return; } final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0); sWakeLock.ensure(intent, opcode); try { Action action; final Bundle actionBundle = intent.getBundleExtra(EXTRA_ACTION_BUNDLE); actionBundle.setClassLoader(getClassLoader()); switch(opcode) { case OP_START_ACTION: { action = (Action) actionBundle.getParcelable(BUNDLE_ACTION); executeAction(action); break; } case OP_RECEIVE_BACKGROUND_RESPONSE: { action = (Action) actionBundle.getParcelable(BUNDLE_ACTION); final Bundle response = intent.getBundleExtra(EXTRA_WORKER_RESPONSE); processBackgroundResponse(action, response); break; } case OP_RECEIVE_BACKGROUND_FAILURE: { action = (Action) actionBundle.getParcelable(BUNDLE_ACTION); processBackgroundFailure(action); break; } default: throw new RuntimeException("Unrecognized opcode in ActionServiceImpl"); } action.sendBackgroundActions(mBackgroundWorker); } finally { // Decrease refCount on wake lock - releasing if necessary sWakeLock.release(intent, opcode); } } private static final long EXECUTION_TIME_WARN_LIMIT_MS = 1000; // 1 second /** * Local execution of action on ActionService thread */ private void executeAction(final Action action) { action.markBeginExecute(); final LoggingTimer timer = createLoggingTimer(action, "#executeAction"); timer.start(); final Object result = action.executeAction(); timer.stopAndLog(); action.markEndExecute(result); } /** * Process response on ActionService thread */ private void processBackgroundResponse(final Action action, final Bundle response) { final LoggingTimer timer = createLoggingTimer(action, "#processBackgroundResponse"); timer.start(); action.processBackgroundWorkResponse(response); timer.stopAndLog(); } /** * Process failure on ActionService thread */ private void processBackgroundFailure(final Action action) { final LoggingTimer timer = createLoggingTimer(action, "#processBackgroundFailure"); timer.start(); action.processBackgroundWorkFailure(); timer.stopAndLog(); } private static LoggingTimer createLoggingTimer( final Action action, final String methodName) { return new LoggingTimer(TAG, action.getClass().getSimpleName() + methodName, EXECUTION_TIME_WARN_LIMIT_MS); } }