1 /*
2  * Copyright (C) 2017 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 package com.android.providers.contacts;
17 
18 import android.os.Handler;
19 import android.os.HandlerThread;
20 import android.os.Looper;
21 import android.os.Message;
22 import android.util.Log;
23 
24 import com.android.internal.annotations.VisibleForTesting;
25 
26 import java.util.concurrent.atomic.AtomicInteger;
27 
28 import javax.annotation.concurrent.GuardedBy;
29 
30 /**
31  * Runs tasks in a worker thread, which is created on-demand and shuts down after a timeout.
32  */
33 public abstract class ContactsTaskScheduler {
34     private static final String TAG = "ContactsTaskScheduler";
35 
36     public static final boolean VERBOSE_LOGGING = AbstractContactsProvider.VERBOSE_LOGGING;
37 
38     private static final int SHUTDOWN_TIMEOUT_SECONDS = 60;
39 
40     private final AtomicInteger mThreadSequenceNumber = new AtomicInteger();
41 
42     private final Object mLock = new Object();
43 
44     /**
45      * Name of this scheduler for logging.
46      */
47     private final String mName;
48 
49     @GuardedBy("mLock")
50     private HandlerThread mThread;
51 
52     @GuardedBy("mLock")
53     private MyHandler mHandler;
54 
55     private final int mShutdownTimeoutSeconds;
56 
ContactsTaskScheduler(String name)57     public ContactsTaskScheduler(String name) {
58         this(name, SHUTDOWN_TIMEOUT_SECONDS);
59     }
60 
61     /** With explicit timeout seconds, for testing. */
ContactsTaskScheduler(String name, int shutdownTimeoutSeconds)62     protected ContactsTaskScheduler(String name, int shutdownTimeoutSeconds) {
63         mName = name;
64         mShutdownTimeoutSeconds = shutdownTimeoutSeconds;
65     }
66 
67     private class MyHandler extends Handler {
MyHandler(Looper looper)68         public MyHandler(Looper looper) {
69             super(looper);
70         }
71 
72         @Override
handleMessage(Message msg)73         public void handleMessage(Message msg) {
74             if (VERBOSE_LOGGING) {
75                 Log.v(TAG, "[" + mName + "] " + mThread + " dispatching " + msg.what);
76             }
77             onPerformTask(msg.what, msg.obj);
78         }
79     }
80 
81     private final Runnable mQuitter = () -> {
82         synchronized (mLock) {
83             stopThread(/* joinOnlyForTest=*/ false);
84         }
85     };
86 
isRunning()87     private boolean isRunning() {
88         synchronized (mLock) {
89             return mThread != null;
90         }
91     }
92 
93     /** Schedule a task with no arguments. */
94     @VisibleForTesting
scheduleTask(int taskId)95     public void scheduleTask(int taskId) {
96         scheduleTask(taskId, null);
97     }
98 
99     /** Schedule a task with an argument. */
100     @VisibleForTesting
scheduleTask(int taskId, Object arg)101     public void scheduleTask(int taskId, Object arg) {
102         synchronized (mLock) {
103             if (!isRunning()) {
104                 mThread = new HandlerThread("Worker-" + mThreadSequenceNumber.incrementAndGet());
105                 mThread.start();
106                 mHandler = new MyHandler(mThread.getLooper());
107 
108                 if (VERBOSE_LOGGING) {
109                     Log.v(TAG, "[" + mName + "] " + mThread + " started.");
110                 }
111             }
112             if (arg == null) {
113                 mHandler.sendEmptyMessage(taskId);
114             } else {
115                 mHandler.sendMessage(mHandler.obtainMessage(taskId, arg));
116             }
117 
118             // Schedule thread shutdown.
119             mHandler.removeCallbacks(mQuitter);
120             mHandler.postDelayed(mQuitter, mShutdownTimeoutSeconds * 1000);
121         }
122     }
123 
onPerformTask(int taskId, Object arg)124     public abstract void onPerformTask(int taskId, Object arg);
125 
126     @VisibleForTesting
shutdownForTest()127     public void shutdownForTest() {
128         stopThread(/* joinOnlyForTest=*/ true);
129     }
130 
stopThread(boolean joinOnlyForTest)131     private void stopThread(boolean joinOnlyForTest) {
132         synchronized (mLock) {
133             if (VERBOSE_LOGGING) {
134                 Log.v(TAG, "[" + mName + "] " + mThread + " stopping...");
135             }
136             if (mThread != null) {
137                 mThread.quit();
138                 if (joinOnlyForTest) {
139                     try {
140                         mThread.join();
141                     } catch (InterruptedException ignore) {
142                     }
143                 }
144             }
145             mThread = null;
146             mHandler = null;
147         }
148     }
149 
150     @VisibleForTesting
getThreadSequenceNumber()151     public int getThreadSequenceNumber() {
152         return mThreadSequenceNumber.get();
153     }
154 
155     @VisibleForTesting
isRunningForTest()156     public boolean isRunningForTest() {
157         return isRunning();
158     }
159 }
160