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 */
16package com.android.providers.contacts;
17
18import android.os.Handler;
19import android.os.HandlerThread;
20import android.os.Looper;
21import android.os.Message;
22import android.util.Log;
23
24import com.android.internal.annotations.VisibleForTesting;
25
26import java.util.concurrent.atomic.AtomicInteger;
27
28import 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 */
33public 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
57    public ContactsTaskScheduler(String name) {
58        this(name, SHUTDOWN_TIMEOUT_SECONDS);
59    }
60
61    /** With explicit timeout seconds, for testing. */
62    protected ContactsTaskScheduler(String name, int shutdownTimeoutSeconds) {
63        mName = name;
64        mShutdownTimeoutSeconds = shutdownTimeoutSeconds;
65    }
66
67    private class MyHandler extends Handler {
68        public MyHandler(Looper looper) {
69            super(looper);
70        }
71
72        @Override
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
87    private boolean isRunning() {
88        synchronized (mLock) {
89            return mThread != null;
90        }
91    }
92
93    /** Schedule a task with no arguments. */
94    @VisibleForTesting
95    public void scheduleTask(int taskId) {
96        scheduleTask(taskId, null);
97    }
98
99    /** Schedule a task with an argument. */
100    @VisibleForTesting
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
124    public abstract void onPerformTask(int taskId, Object arg);
125
126    @VisibleForTesting
127    public void shutdownForTest() {
128        stopThread(/* joinOnlyForTest=*/ true);
129    }
130
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
151    public int getThreadSequenceNumber() {
152        return mThreadSequenceNumber.get();
153    }
154
155    @VisibleForTesting
156    public boolean isRunningForTest() {
157        return isRunning();
158    }
159}
160