Merge "Shutdown worker thread when not needed"
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java
index c1a50b2..3d3678e 100644
--- a/src/com/android/providers/contacts/CallLogProvider.java
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -33,10 +33,6 @@
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.os.Binder;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Message;
-import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.CallLog;
@@ -46,11 +42,13 @@
 import android.telecom.TelecomManager;
 import android.text.TextUtils;
 import android.util.Log;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.providers.contacts.CallLogDatabaseHelper.DbProperties;
 import com.android.providers.contacts.CallLogDatabaseHelper.Tables;
 import com.android.providers.contacts.util.SelectionBuilder;
 import com.android.providers.contacts.util.UserUtils;
+
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -62,7 +60,7 @@
 public class CallLogProvider extends ContentProvider {
     private static final String TAG = CallLogProvider.class.getSimpleName();
 
-    public static final boolean VERBOSE_LOGGING = false; // DO NOT SUBMIT WITH TRUE
+    public static final boolean VERBOSE_LOGGING = AbstractContactsProvider.VERBOSE_LOGGING;
 
     private static final int BACKGROUND_TASK_INITIALIZE = 0;
     private static final int BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT = 1;
@@ -166,8 +164,8 @@
 
     private static Long sTimeForTestMillis;
 
-    private HandlerThread mBackgroundThread;
-    private Handler mBackgroundHandler;
+    private ContactsTaskScheduler mTaskScheduler;
+
     private volatile CountDownLatch mReadAccessLatch;
 
     private CallLogDatabaseHelper mDbHelper;
@@ -198,19 +196,16 @@
         mVoicemailPermissions = new VoicemailPermissions(context);
         mCallLogInsertionHelper = createCallLogInsertionHelper(context);
 
-        mBackgroundThread = new HandlerThread(getProviderName() + "Worker",
-                Process.THREAD_PRIORITY_BACKGROUND);
-        mBackgroundThread.start();
-        mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) {
+        mReadAccessLatch = new CountDownLatch(1);
+
+        mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) {
             @Override
-            public void handleMessage(Message msg) {
-                performBackgroundTask(msg.what, msg.obj);
+            public void onPerformTask(int taskId, Object arg) {
+                performBackgroundTask(taskId, arg);
             }
         };
 
-        mReadAccessLatch = new CountDownLatch(1);
-
-        scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE, null);
+        mTaskScheduler.scheduleTask(BACKGROUND_TASK_INITIALIZE, null);
 
         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
             Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate finish");
@@ -452,7 +447,7 @@
     }
 
     void adjustForNewPhoneAccount(PhoneAccountHandle handle) {
-        scheduleBackgroundTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, handle);
+        mTaskScheduler.scheduleTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, handle);
     }
 
     /**
@@ -735,10 +730,6 @@
         }
     }
 
-    private void scheduleBackgroundTask(int task, Object arg) {
-        mBackgroundHandler.obtainMessage(task, arg).sendToTarget();
-    }
-
     private void performBackgroundTask(int task, Object arg) {
         if (task == BACKGROUND_TASK_INITIALIZE) {
             try {
@@ -754,12 +745,6 @@
 
     @Override
     public void shutdown() {
-        if (mBackgroundHandler != null) {
-            mBackgroundHandler.getLooper().quit();
-            try {
-                mBackgroundThread.join();
-            } catch (InterruptedException ignore) {
-            }
-        }
+        mTaskScheduler.shutdownForTest();
     }
 }
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index f3f85cf..ceda7d0 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -56,12 +56,8 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.CancellationSignal;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Message;
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelFileDescriptor.AutoCloseInputStream;
-import android.os.Process;
 import android.os.RemoteException;
 import android.os.StrictMode;
 import android.os.SystemClock;
@@ -1532,8 +1528,7 @@
     private LocaleSet mCurrentLocales;
     private int mContactsAccountCount;
 
-    private HandlerThread mBackgroundThread;
-    private Handler mBackgroundHandler;
+    private ContactsTaskScheduler mTaskScheduler;
 
     private long mLastPhotoCleanup = 0;
 
@@ -1601,13 +1596,10 @@
         mReadAccessLatch = new CountDownLatch(1);
         mWriteAccessLatch = new CountDownLatch(1);
 
-        mBackgroundThread = new HandlerThread("ContactsProviderWorker",
-                Process.THREAD_PRIORITY_BACKGROUND);
-        mBackgroundThread.start();
-        mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) {
+        mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) {
             @Override
-            public void handleMessage(Message msg) {
-                performBackgroundTask(msg.what, msg.obj);
+            public void onPerformTask(int taskId, Object arg) {
+                performBackgroundTask(taskId, arg);
             }
         };
 
@@ -1733,11 +1725,11 @@
     }
 
     protected void scheduleBackgroundTask(int task) {
-        mBackgroundHandler.sendEmptyMessage(task);
+        scheduleBackgroundTask(task, null);
     }
 
     protected void scheduleBackgroundTask(int task, Object arg) {
-        mBackgroundHandler.sendMessage(mBackgroundHandler.obtainMessage(task, arg));
+        mTaskScheduler.scheduleTask(task, arg);
     }
 
     protected void performBackgroundTask(int task, Object arg) {
@@ -10221,13 +10213,7 @@
 
     @Override
     public void shutdown() {
-        if (mBackgroundHandler != null) {
-            mBackgroundHandler.getLooper().quit();
-            try {
-                mBackgroundThread.join();
-            } catch (InterruptedException ignore) {
-            }
-        }
+        mTaskScheduler.shutdownForTest();
     }
 
     @VisibleForTesting
diff --git a/src/com/android/providers/contacts/ContactsTaskScheduler.java b/src/com/android/providers/contacts/ContactsTaskScheduler.java
new file mode 100644
index 0000000..1628387
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactsTaskScheduler.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2017 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.providers.contacts;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Runs tasks in a worker thread, which is created on-demand and shuts down after a timeout.
+ */
+public abstract class ContactsTaskScheduler {
+    private static final String TAG = "ContactsTaskScheduler";
+
+    public static final boolean VERBOSE_LOGGING = AbstractContactsProvider.VERBOSE_LOGGING;
+
+    private static final int SHUTDOWN_TIMEOUT_SECONDS = 60;
+
+    private final AtomicInteger mThreadSequenceNumber = new AtomicInteger();
+
+    private final Object mLock = new Object();
+
+    /**
+     * Name of this scheduler for logging.
+     */
+    private final String mName;
+
+    @GuardedBy("mLock")
+    private HandlerThread mThread;
+
+    @GuardedBy("mLock")
+    private MyHandler mHandler;
+
+    private final int mShutdownTimeoutSeconds;
+
+    public ContactsTaskScheduler(String name) {
+        this(name, SHUTDOWN_TIMEOUT_SECONDS);
+    }
+
+    /** With explicit timeout seconds, for testing. */
+    protected ContactsTaskScheduler(String name, int shutdownTimeoutSeconds) {
+        mName = name;
+        mShutdownTimeoutSeconds = shutdownTimeoutSeconds;
+    }
+
+    private class MyHandler extends Handler {
+        public MyHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            if (VERBOSE_LOGGING) {
+                Log.v(TAG, "[" + mName + "] " + mThread + " dispatching " + msg.what);
+            }
+            onPerformTask(msg.what, msg.obj);
+        }
+    }
+
+    private final Runnable mQuitter = () -> {
+        synchronized (mLock) {
+            stopThread(/* joinOnlyForTest=*/ false);
+        }
+    };
+
+    private boolean isRunning() {
+        synchronized (mLock) {
+            return mThread != null;
+        }
+    }
+
+    /** Schedule a task with no arguments. */
+    @VisibleForTesting
+    public void scheduleTask(int taskId) {
+        scheduleTask(taskId, null);
+    }
+
+    /** Schedule a task with an argument. */
+    @VisibleForTesting
+    public void scheduleTask(int taskId, Object arg) {
+        synchronized (mLock) {
+            if (!isRunning()) {
+                mThread = new HandlerThread("Worker-" + mThreadSequenceNumber.incrementAndGet());
+                mThread.start();
+                mHandler = new MyHandler(mThread.getLooper());
+
+                if (VERBOSE_LOGGING) {
+                    Log.v(TAG, "[" + mName + "] " + mThread + " started.");
+                }
+            }
+            if (arg == null) {
+                mHandler.sendEmptyMessage(taskId);
+            } else {
+                mHandler.sendMessage(mHandler.obtainMessage(taskId, arg));
+            }
+
+            // Schedule thread shutdown.
+            mHandler.removeCallbacks(mQuitter);
+            mHandler.postDelayed(mQuitter, mShutdownTimeoutSeconds * 1000);
+        }
+    }
+
+    public abstract void onPerformTask(int taskId, Object arg);
+
+    @VisibleForTesting
+    public void shutdownForTest() {
+        stopThread(/* joinOnlyForTest=*/ true);
+    }
+
+    private void stopThread(boolean joinOnlyForTest) {
+        synchronized (mLock) {
+            if (VERBOSE_LOGGING) {
+                Log.v(TAG, "[" + mName + "] " + mThread + " stopping...");
+            }
+            if (mThread != null) {
+                mThread.quit();
+                if (joinOnlyForTest) {
+                    try {
+                        mThread.join();
+                    } catch (InterruptedException ignore) {
+                    }
+                }
+            }
+            mThread = null;
+            mHandler = null;
+        }
+    }
+
+    @VisibleForTesting
+    public int getThreadSequenceNumber() {
+        return mThreadSequenceNumber.get();
+    }
+
+    @VisibleForTesting
+    public boolean isRunningForTest() {
+        return isRunning();
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactsTaskSchedulerTest.java b/tests/src/com/android/providers/contacts/ContactsTaskSchedulerTest.java
new file mode 100644
index 0000000..df7196d
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactsTaskSchedulerTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2017 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.providers.contacts;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@LargeTest
+public class ContactsTaskSchedulerTest extends AndroidTestCase {
+    private static final int SHUTDOWN_SECONDS = 3;
+
+    private static class MyContactsTaskScheduler extends ContactsTaskScheduler {
+        final CountDownLatch latch;
+
+        final List<String> executed = new ArrayList<>();
+
+        public MyContactsTaskScheduler(int numExpectedTasks) {
+            super("Test", SHUTDOWN_SECONDS);
+            latch = new CountDownLatch(numExpectedTasks);
+        }
+
+        @Override
+        public void onPerformTask(int taskId, Object arg) {
+            executed.add("" + taskId + "," + arg);
+
+            latch.countDown();
+        }
+    }
+
+    public void testSimple() throws Exception {
+        final MyContactsTaskScheduler scheduler = new MyContactsTaskScheduler(3);
+
+        scheduler.scheduleTask(1);
+        scheduler.scheduleTask(10);
+        scheduler.scheduleTask(2, "arg");
+
+        assertTrue(scheduler.latch.await(10, TimeUnit.SECONDS));
+
+        assertEquals(Arrays.asList("1,null", "10,null", "2,arg"), scheduler.executed);
+
+        // Only one thread has been created.
+        assertEquals(1, scheduler.getThreadSequenceNumber());
+    }
+
+    public void testAutoShutdown() throws Exception {
+        final MyContactsTaskScheduler scheduler = new MyContactsTaskScheduler(7);
+
+        scheduler.scheduleTask(1);
+
+        // Wait for 10 seconds and the thread should shut down.
+        assertTrue(scheduler.isRunningForTest());
+        Thread.sleep(10 * 1000);
+        assertFalse(scheduler.isRunningForTest());
+
+        scheduler.scheduleTask(2);
+        assertTrue(scheduler.isRunningForTest());
+
+        Thread.sleep(1 * 1000);
+        scheduler.scheduleTask(3);
+
+        Thread.sleep(1 * 1000);
+        scheduler.scheduleTask(4);
+
+        Thread.sleep(1 * 1000);
+        scheduler.scheduleTask(5);
+
+        Thread.sleep(1 * 1000);
+        scheduler.scheduleTask(6);
+        assertTrue(scheduler.isRunningForTest()); // Should still alive.
+
+        // Wait for 10 seconds and the thread should shut down.
+        Thread.sleep(10 * 1000);
+        assertFalse(scheduler.isRunningForTest());
+
+        scheduler.scheduleTask(7);
+        assertTrue(scheduler.isRunningForTest());
+
+        assertTrue(scheduler.latch.await(10, TimeUnit.SECONDS));
+        assertEquals(7, scheduler.executed.size());
+
+        // Only one thread has been created.
+        assertEquals(3, scheduler.getThreadSequenceNumber());
+    }
+}