Home | History | Annotate | Download | only in systemalarm
      1 /*
      2  * Copyright 2018 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 androidx.work.impl.background.systemalarm;
     18 
     19 import android.content.Context;
     20 import android.content.Intent;
     21 import android.os.Handler;
     22 import android.os.Looper;
     23 import android.os.PowerManager;
     24 import android.support.annotation.MainThread;
     25 import android.support.annotation.NonNull;
     26 import android.support.annotation.Nullable;
     27 import android.support.annotation.RestrictTo;
     28 import android.support.annotation.VisibleForTesting;
     29 import android.text.TextUtils;
     30 import android.util.Log;
     31 
     32 import androidx.work.impl.ExecutionListener;
     33 import androidx.work.impl.Processor;
     34 import androidx.work.impl.WorkManagerImpl;
     35 import androidx.work.impl.utils.WakeLocks;
     36 
     37 import java.util.ArrayList;
     38 import java.util.List;
     39 import java.util.concurrent.ExecutorService;
     40 import java.util.concurrent.Executors;
     41 
     42 /**
     43  * The dispatcher used by the background processor which is based on
     44  * {@link android.app.AlarmManager}.
     45  *
     46  * @hide
     47  */
     48 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     49 public class SystemAlarmDispatcher implements ExecutionListener {
     50 
     51     private static final String TAG = "SystemAlarmDispatcher";
     52     private static final String PROCESS_COMMAND_TAG = "ProcessCommand";
     53     private static final String KEY_START_ID = "KEY_START_ID";
     54     private static final int DEFAULT_START_ID = 0;
     55 
     56     private final Context mContext;
     57     private final WorkTimer mWorkTimer;
     58     private final Processor mProcessor;
     59     private final WorkManagerImpl mWorkManager;
     60     private final CommandHandler mCommandHandler;
     61     private final Handler mMainHandler;
     62     private final List<Intent> mIntents;
     63     // The executor service responsible for dispatching all the commands.
     64     private final ExecutorService mCommandExecutorService;
     65 
     66     @Nullable private CommandsCompletedListener mCompletedListener;
     67 
     68     SystemAlarmDispatcher(@NonNull Context context) {
     69         this(context, null, null);
     70     }
     71 
     72     @VisibleForTesting
     73     SystemAlarmDispatcher(
     74             @NonNull Context context,
     75             @Nullable Processor processor,
     76             @Nullable WorkManagerImpl workManager) {
     77 
     78         mContext = context.getApplicationContext();
     79         mCommandHandler = new CommandHandler(mContext);
     80         mWorkTimer = new WorkTimer();
     81         mWorkManager = workManager != null ? workManager : WorkManagerImpl.getInstance();
     82         mProcessor = processor != null ? processor : mWorkManager.getProcessor();
     83         mProcessor.addExecutionListener(this);
     84         // a list of pending intents which need to be processed
     85         mIntents = new ArrayList<>();
     86         mMainHandler = new Handler(Looper.getMainLooper());
     87         // Use a single thread executor for handling the actual
     88         // execution of the commands themselves
     89         mCommandExecutorService = Executors.newSingleThreadExecutor();
     90     }
     91 
     92     void onDestroy() {
     93         mProcessor.removeExecutionListener(this);
     94         mCompletedListener = null;
     95     }
     96 
     97     @Override
     98     public void onExecuted(
     99             @NonNull String workSpecId,
    100             boolean isSuccessful,
    101             boolean needsReschedule) {
    102 
    103         // When there are lots of workers completing at around the same time,
    104         // this creates lock contention for the DelayMetCommandHandlers inside the CommandHandler.
    105         // So move the actual execution of the post completion callbacks on the command executor
    106         // thread.
    107         postOnMainThread(
    108                 new AddRunnable(
    109                         this,
    110                         CommandHandler.createExecutionCompletedIntent(
    111                                 mContext,
    112                                 workSpecId,
    113                                 isSuccessful,
    114                                 needsReschedule),
    115                         DEFAULT_START_ID));
    116     }
    117 
    118     /**
    119      * Adds the {@link Intent} intent and the startId to the command processor queue.
    120      *
    121      * @param intent The {@link Intent} command that needs to be added to the command queue.
    122      * @param startId The command startId
    123      * @return <code>true</code> when the command was added to the command processor queue.
    124      */
    125     @MainThread
    126     public boolean add(@NonNull final Intent intent, final int startId) {
    127         assertMainThread();
    128         String action = intent.getAction();
    129         if (TextUtils.isEmpty(action)) {
    130             Log.w(TAG, "Unknown command. Ignoring");
    131             return false;
    132         }
    133 
    134         // If we have a constraints changed intent in the queue don't add a second one. We are
    135         // treating this intent as special because every time a worker with constraints is complete
    136         // it kicks off an update for constraint proxies.
    137         if (CommandHandler.ACTION_CONSTRAINTS_CHANGED.equals(action)
    138                 && hasIntentWithAction(CommandHandler.ACTION_CONSTRAINTS_CHANGED)) {
    139             return false;
    140         }
    141 
    142         intent.putExtra(KEY_START_ID, startId);
    143         synchronized (mIntents) {
    144             mIntents.add(intent);
    145         }
    146         processCommand();
    147         return true;
    148     }
    149 
    150     void setCompletedListener(@NonNull CommandsCompletedListener listener) {
    151         if (mCompletedListener != null) {
    152             Log.e(TAG, "A completion listener for SystemAlarmDispatcher already exists.");
    153             return;
    154         }
    155         mCompletedListener = listener;
    156     }
    157 
    158     Processor getProcessor() {
    159         return mProcessor;
    160     }
    161 
    162     WorkTimer getWorkTimer() {
    163         return mWorkTimer;
    164     }
    165 
    166     WorkManagerImpl getWorkManager() {
    167         return mWorkManager;
    168     }
    169 
    170     void postOnMainThread(@NonNull Runnable runnable) {
    171         mMainHandler.post(runnable);
    172     }
    173 
    174     @MainThread
    175     private void checkForCommandsCompleted() {
    176         assertMainThread();
    177         // if there are no more intents to process, and the command handler
    178         // has no more pending commands, stop the service.
    179         synchronized (mIntents) {
    180             if (!mCommandHandler.hasPendingCommands() && mIntents.isEmpty()) {
    181                 Log.d(TAG, "No more commands & intents.");
    182                 if (mCompletedListener != null) {
    183                     mCompletedListener.onAllCommandsCompleted();
    184                 }
    185             }
    186         }
    187     }
    188 
    189     @MainThread
    190     @SuppressWarnings("FutureReturnValueIgnored")
    191     private void processCommand() {
    192         assertMainThread();
    193         PowerManager.WakeLock processCommandLock =
    194                 WakeLocks.newWakeLock(mContext, PROCESS_COMMAND_TAG);
    195         try {
    196             processCommandLock.acquire();
    197             // Process commands on the actual executor service,
    198             // so we are no longer blocking the main thread.
    199             mCommandExecutorService.submit(new Runnable() {
    200                 @Override
    201                 public void run() {
    202                     final Intent intent;
    203                     synchronized (mIntents) {
    204                         intent = mIntents.get(0);
    205                     }
    206 
    207                     if (intent != null) {
    208                         final String action = intent.getAction();
    209                         final int startId = intent.getIntExtra(KEY_START_ID, DEFAULT_START_ID);
    210                         Log.d(TAG, String.format("Processing command %s, %s", intent, startId));
    211                         final PowerManager.WakeLock wakeLock = WakeLocks.newWakeLock(
    212                                 mContext,
    213                                 String.format("%s (%s)", action, startId));
    214                         try {
    215                             Log.d(TAG, String.format(
    216                                     "Acquiring operation wake lock (%s) %s",
    217                                     action,
    218                                     wakeLock));
    219 
    220                             wakeLock.acquire();
    221                             mCommandHandler.onHandleIntent(intent, startId,
    222                                     SystemAlarmDispatcher.this);
    223                         } finally {
    224                             // Remove the intent from the queue, only after it has been processed.
    225 
    226                             // We are doing this to avoid a race condition between completion of a
    227                             // command in the command handler, and the checkForCompletion triggered
    228                             // by a worker's onExecutionComplete().
    229                             // For e.g.
    230                             // t0 -> delay_met_intent
    231                             // t1 -> bgProcessor.startWork(workSpec)
    232                             // t2 -> constraints_changed_intent
    233                             // t3 -> bgProcessor.onExecutionCompleted(...)
    234                             // t4 -> CheckForCompletionRunnable (while constraints_changed_intent is
    235                             // still being processed).
    236 
    237                             // Note: this works only because mCommandExecutor service is a single
    238                             // threaded executor. If that assumption changes in the future, use a
    239                             // ReentrantLock, and lock the queue while command processor processes
    240                             // an intent. Synchronized to prevent ConcurrentModificationExceptions.
    241                             synchronized (mIntents) {
    242                                 mIntents.remove(0);
    243                             }
    244 
    245                             Log.d(TAG, String.format(
    246                                     "Releasing operation wake lock (%s) %s",
    247                                     action,
    248                                     wakeLock));
    249 
    250                             wakeLock.release();
    251                             // Check if we have processed all commands
    252                             postOnMainThread(
    253                                     new CheckForCompletionRunnable(SystemAlarmDispatcher.this));
    254                         }
    255                     }
    256                 }
    257             });
    258         } finally {
    259             processCommandLock.release();
    260         }
    261     }
    262 
    263     @MainThread
    264     private boolean hasIntentWithAction(@NonNull String action) {
    265         assertMainThread();
    266         synchronized (mIntents) {
    267             for (Intent intent : mIntents) {
    268                 if (action.equals(intent.getAction())) {
    269                     return true;
    270                 }
    271             }
    272             return false;
    273         }
    274     }
    275 
    276     private void assertMainThread() {
    277         if (mMainHandler.getLooper().getThread() != Thread.currentThread()) {
    278             throw new IllegalStateException("Needs to be invoked on the main thread.");
    279         }
    280     }
    281 
    282     /**
    283      * Checks if we are done executing all commands.
    284      */
    285     static class CheckForCompletionRunnable implements Runnable {
    286         private final SystemAlarmDispatcher mDispatcher;
    287 
    288         CheckForCompletionRunnable(@NonNull SystemAlarmDispatcher dispatcher) {
    289             mDispatcher = dispatcher;
    290         }
    291 
    292         @Override
    293         public void run() {
    294             mDispatcher.checkForCommandsCompleted();
    295         }
    296     }
    297 
    298     /**
    299      * Adds a new intent to the SystemAlarmDispatcher.
    300      */
    301     static class AddRunnable implements Runnable {
    302         private final SystemAlarmDispatcher mDispatcher;
    303         private final Intent mIntent;
    304         private final int mStartId;
    305 
    306         AddRunnable(@NonNull SystemAlarmDispatcher dispatcher,
    307                 @NonNull Intent intent,
    308                 int startId) {
    309             mDispatcher = dispatcher;
    310             mIntent = intent;
    311             mStartId = startId;
    312         }
    313 
    314         @Override
    315         public void run() {
    316             mDispatcher.add(mIntent, mStartId);
    317         }
    318     }
    319 
    320     /**
    321      * Used to notify interested parties when all pending commands and work is complete.
    322      */
    323     interface CommandsCompletedListener {
    324         void onAllCommandsCompleted();
    325     }
    326 }
    327