diff --git a/src/com/android/launcher3/DeferredHandler.java b/src/com/android/launcher3/DeferredHandler.java deleted file mode 100644 index a43ab67239..0000000000 --- a/src/com/android/launcher3/DeferredHandler.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (C) 2008 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.launcher3; - -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.MessageQueue; - -import com.android.launcher3.util.Thunk; - -import java.util.LinkedList; - -/** - * Queue of things to run on a looper thread. Items posted with {@link #post} will not - * be actually enqued on the handler until after the last one has run, to keep from - * starving the thread. - * - * This class is fifo. - */ -public class DeferredHandler { - @Thunk LinkedList mQueue = new LinkedList<>(); - private MessageQueue mMessageQueue = Looper.myQueue(); - private Impl mHandler = new Impl(); - - @Thunk class Impl extends Handler implements MessageQueue.IdleHandler { - public void handleMessage(Message msg) { - Runnable r; - synchronized (mQueue) { - if (mQueue.size() == 0) { - return; - } - r = mQueue.removeFirst(); - } - r.run(); - synchronized (mQueue) { - scheduleNextLocked(); - } - } - - public boolean queueIdle() { - handleMessage(null); - return false; - } - } - - private class IdleRunnable implements Runnable { - Runnable mRunnable; - - IdleRunnable(Runnable r) { - mRunnable = r; - } - - public void run() { - mRunnable.run(); - } - } - - public DeferredHandler() { - } - - /** Schedule runnable to run after everything that's on the queue right now. */ - public void post(Runnable runnable) { - synchronized (mQueue) { - mQueue.add(runnable); - if (mQueue.size() == 1) { - scheduleNextLocked(); - } - } - } - - /** Schedule runnable to run when the queue goes idle. */ - public void postIdle(final Runnable runnable) { - post(new IdleRunnable(runnable)); - } - - public void cancelAll() { - synchronized (mQueue) { - mQueue.clear(); - } - } - - /** Runs all queued Runnables from the calling thread. */ - public void flush() { - LinkedList queue = new LinkedList<>(); - synchronized (mQueue) { - queue.addAll(mQueue); - mQueue.clear(); - } - for (Runnable r : queue) { - r.run(); - } - } - - void scheduleNextLocked() { - if (mQueue.size() > 0) { - Runnable peek = mQueue.getFirst(); - if (peek instanceof IdleRunnable) { - mMessageQueue.addIdleHandler(mHandler); - } else { - mHandler.sendEmptyMessage(1); - } - } - } -} - diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index 384f20237d..10b65a9852 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -72,6 +72,7 @@ import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.shortcuts.ShortcutInfoCompat; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.LooperIdleLock; import com.android.launcher3.util.ManagedProfileHeuristic; import com.android.launcher3.util.MultiHashMap; import com.android.launcher3.util.PackageManagerHelper; @@ -110,9 +111,9 @@ public class LauncherModel extends BroadcastReceiver private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons private static final long INVALID_SCREEN_ID = -1L; + private final MainThreadExecutor mUiExecutor = new MainThreadExecutor(); @Thunk final LauncherAppState mApp; @Thunk final Object mLock = new Object(); - @Thunk DeferredHandler mHandler = new DeferredHandler(); @Thunk LoaderTask mLoaderTask; @Thunk boolean mIsLoaderTaskRunning; @Thunk boolean mHasLoaderCompletedOnce; @@ -218,17 +219,6 @@ public class LauncherModel extends BroadcastReceiver mUserManager = UserManagerCompat.getInstance(context); } - /** Runs the specified runnable immediately if called from the main thread, otherwise it is - * posted on the main thread handler. */ - private void runOnMainThread(Runnable r) { - if (sWorkerThread.getThreadId() == Process.myTid()) { - // If we are on the worker thread, post onto the main handler - mHandler.post(r); - } else { - r.run(); - } - } - /** Runs the specified runnable immediately if called from the worker thread, otherwise it is * posted on the worker thread handler. */ private static void runOnWorkerThread(Runnable r) { @@ -378,8 +368,6 @@ public class LauncherModel extends BroadcastReceiver public void initialize(Callbacks callbacks) { synchronized (mLock) { Preconditions.assertUIThread(); - // Remove any queued UI runnables - mHandler.cancelAll(); mCallbacks = new WeakReference<>(callbacks); } } @@ -543,11 +531,11 @@ public class LauncherModel extends BroadcastReceiver if (mCallbacks != null && mCallbacks.get() != null) { final Callbacks oldCallbacks = mCallbacks.get(); // Clear any pending bind-runnables from the synchronized load process. - runOnMainThread(new Runnable() { - public void run() { - oldCallbacks.clearPendingBinds(); - } - }); + mUiExecutor.execute(new Runnable() { + public void run() { + oldCallbacks.clearPendingBinds(); + } + }); // If there is already one running, tell it to stop. stopLoaderLocked(); @@ -598,7 +586,6 @@ public class LauncherModel extends BroadcastReceiver @Thunk boolean mIsLoadingAndBindingWorkspace; private boolean mStopped; - @Thunk boolean mLoadAndBindStepFinished; LoaderTask(Context context, int pageToBindFirst) { mContext = context; @@ -610,34 +597,10 @@ public class LauncherModel extends BroadcastReceiver // This way we don't start loading all apps until the workspace has settled // down. synchronized (LoaderTask.this) { - final long workspaceWaitTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; - - mHandler.postIdle(new Runnable() { - public void run() { - synchronized (LoaderTask.this) { - mLoadAndBindStepFinished = true; - if (DEBUG_LOADERS) { - Log.d(TAG, "done with previous binding step"); - } - LoaderTask.this.notify(); - } - } - }); - - while (!mStopped && !mLoadAndBindStepFinished) { - try { - // Just in case mFlushingWorkerThread changes but we aren't woken up, - // wait no longer than 1sec at a time - this.wait(1000); - } catch (InterruptedException ex) { - // Ignore - } - } - if (DEBUG_LOADERS) { - Log.d(TAG, "waited " - + (SystemClock.uptimeMillis()-workspaceWaitTime) - + "ms for previous step to finish binding"); - } + LooperIdleLock idleLock = new LooperIdleLock(this, Looper.getMainLooper()); + // Just in case mFlushingWorkerThread changes but we aren't woken up, + // wait no longer than 1sec at a time + while (!mStopped && idleLock.awaitLocked(1000)); } } @@ -660,15 +623,6 @@ public class LauncherModel extends BroadcastReceiver } } - // XXX: Throw an exception if we are already loading (since we touch the worker thread - // data structures, we can't allow any other thread to touch that data, but because - // this call is synchronous, we can get away with not locking). - - // The LauncherModel is static in the LauncherAppState and mHandler may have queued - // operations from the previous activity. We need to ensure that all queued operations - // are executed before any synchronous binding work is done. - mHandler.flush(); - // Divide the set of loaded items into those that we are binding synchronously, and // everything else that is to be bound normally (asynchronously). bindWorkspace(synchronousBindPage); @@ -696,6 +650,7 @@ public class LauncherModel extends BroadcastReceiver } try { + long now = 0; if (DEBUG_LOADERS) Log.d(TAG, "step 1.1: loading workspace"); // Set to false in bindWorkspace() mIsLoadingAndBindingWorkspace = true; @@ -706,8 +661,12 @@ public class LauncherModel extends BroadcastReceiver bindWorkspace(mPageToBindFirst); // Take a break - if (DEBUG_LOADERS) Log.d(TAG, "step 1 completed, wait for idle"); + if (DEBUG_LOADERS) { + Log.d(TAG, "step 1 completed, wait for idle"); + now = SystemClock.uptimeMillis(); + } waitForIdle(); + if (DEBUG_LOADERS) Log.d(TAG, "Waited " + (SystemClock.uptimeMillis() - now) + "ms"); verifyNotStopped(); // second step @@ -719,8 +678,12 @@ public class LauncherModel extends BroadcastReceiver updateIconCache(); // Take a break - if (DEBUG_LOADERS) Log.d(TAG, "step 2 completed, wait for idle"); + if (DEBUG_LOADERS) { + Log.d(TAG, "step 2 completed, wait for idle"); + now = SystemClock.uptimeMillis(); + } waitForIdle(); + if (DEBUG_LOADERS) Log.d(TAG, "Waited " + (SystemClock.uptimeMillis() - now) + "ms"); verifyNotStopped(); // third step @@ -1433,7 +1396,7 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r); + mUiExecutor.execute(r); } private void bindWorkspaceItems(final Callbacks oldCallbacks, @@ -1539,11 +1502,11 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r); + mUiExecutor.execute(r); bindWorkspaceScreens(oldCallbacks, orderedScreenIds); - Executor mainExecutor = new DeferredMainThreadExecutor(); + Executor mainExecutor = mUiExecutor; // Load items on the current page. bindWorkspaceItems(oldCallbacks, currentWorkspaceItems, currentAppWidgets, mainExecutor); @@ -1553,7 +1516,7 @@ public class LauncherModel extends BroadcastReceiver // This ensures that the first screen is immediately visible (eg. during rotation) // In case of !validFirstPage, bind all pages one after other. final Executor deferredExecutor = - validFirstPage ? new ViewOnDrawExecutor(mHandler) : mainExecutor; + validFirstPage ? new ViewOnDrawExecutor(mUiExecutor) : mainExecutor; mainExecutor.execute(new Runnable() { @Override @@ -1613,7 +1576,7 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r); + mUiExecutor.execute(r); } } @@ -1663,7 +1626,7 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r); + mUiExecutor.execute(r); } private void loadAllApps() { @@ -1711,21 +1674,21 @@ public class LauncherModel extends BroadcastReceiver heuristic.processUserApps(apps); } }; - runOnMainThread(new Runnable() { + mUiExecutor.execute(new Runnable() { - @Override - public void run() { - // Check isLoadingWorkspace on the UI thread, as it is updated on - // the UI thread. - if (mIsLoadingAndBindingWorkspace) { - synchronized (mBindCompleteRunnables) { - mBindCompleteRunnables.add(r); - } - } else { - runOnWorkerThread(r); - } - } - }); + @Override + public void run() { + // Check isLoadingWorkspace on the UI thread, as it is updated on + // the UI thread. + if (mIsLoadingAndBindingWorkspace) { + synchronized (mBindCompleteRunnables) { + mBindCompleteRunnables.add(r); + } + } else { + runOnWorkerThread(r); + } + } + }); } } // Huh? Shouldn't this be inside the Runnable below? @@ -1733,7 +1696,7 @@ public class LauncherModel extends BroadcastReceiver mBgAllAppsList.added = new ArrayList(); // Post callback on main thread - mHandler.post(new Runnable() { + mUiExecutor.execute(new Runnable() { public void run() { final long bindTime = SystemClock.uptimeMillis(); @@ -1787,7 +1750,7 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r); + mUiExecutor.execute(r); } /** @@ -1838,12 +1801,12 @@ public class LauncherModel extends BroadcastReceiver public static abstract class BaseModelUpdateTask implements Runnable { private LauncherModel mModel; - private DeferredHandler mUiHandler; + private Executor mUiExecutor; /* package private */ void init(LauncherModel model) { mModel = model; - mUiHandler = mModel.mHandler; + mUiExecutor = mModel.mUiExecutor; } @Override @@ -1866,7 +1829,7 @@ public class LauncherModel extends BroadcastReceiver */ public final void scheduleCallbackTask(final CallbackTask task) { final Callbacks callbacks = mModel.getCallback(); - mUiHandler.post(new Runnable() { + mUiExecutor.execute(new Runnable() { public void run() { Callbacks cb = mModel.getCallback(); if (callbacks == cb && cb != null) { @@ -1911,7 +1874,7 @@ public class LauncherModel extends BroadcastReceiver private void bindWidgetsModel(final Callbacks callbacks) { final MultiHashMap widgets = mBgWidgetsModel.getWidgetsMap().clone(); - mHandler.post(new Runnable() { + mUiExecutor.execute(new Runnable() { @Override public void run() { Callbacks cb = getCallback(); @@ -1968,14 +1931,6 @@ public class LauncherModel extends BroadcastReceiver } } - @Thunk class DeferredMainThreadExecutor implements Executor { - - @Override - public void execute(Runnable command) { - runOnMainThread(command); - } - } - /** * @return the looper for the worker thread which can be used to start background tasks. */ diff --git a/src/com/android/launcher3/MainThreadExecutor.java b/src/com/android/launcher3/MainThreadExecutor.java index 4ca0a59d81..5094682333 100644 --- a/src/com/android/launcher3/MainThreadExecutor.java +++ b/src/com/android/launcher3/MainThreadExecutor.java @@ -18,14 +18,14 @@ package com.android.launcher3; import android.os.Looper; -import com.android.launcher3.util.LooperExecuter; +import com.android.launcher3.util.LooperExecutor; /** * An executor service that executes its tasks on the main thread. * * Shutting down this executor is not supported. */ -public class MainThreadExecutor extends LooperExecuter { +public class MainThreadExecutor extends LooperExecutor { public MainThreadExecutor() { super(Looper.getMainLooper()); diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java index 4931dca14f..032ed780d6 100644 --- a/src/com/android/launcher3/model/ModelWriter.java +++ b/src/com/android/launcher3/model/ModelWriter.java @@ -34,7 +34,7 @@ import com.android.launcher3.LauncherSettings.Settings; import com.android.launcher3.ShortcutInfo; import com.android.launcher3.util.ContentWriter; import com.android.launcher3.util.ItemInfoMatcher; -import com.android.launcher3.util.LooperExecuter; +import com.android.launcher3.util.LooperExecutor; import java.util.ArrayList; import java.util.Arrays; @@ -55,7 +55,7 @@ public class ModelWriter { public ModelWriter(Context context, BgDataModel dataModel, boolean hasVerticalHotseat) { mContext = context; mBgDataModel = dataModel; - mWorkerExecutor = new LooperExecuter(LauncherModel.getWorkerLooper()); + mWorkerExecutor = new LooperExecutor(LauncherModel.getWorkerLooper()); mHasVerticalHotseat = hasVerticalHotseat; } diff --git a/src/com/android/launcher3/util/LooperExecuter.java b/src/com/android/launcher3/util/LooperExecutor.java similarity index 95% rename from src/com/android/launcher3/util/LooperExecuter.java rename to src/com/android/launcher3/util/LooperExecutor.java index 4db999bcef..5b7c20bbfb 100644 --- a/src/com/android/launcher3/util/LooperExecuter.java +++ b/src/com/android/launcher3/util/LooperExecutor.java @@ -25,11 +25,11 @@ import java.util.concurrent.TimeUnit; /** * Extension of {@link AbstractExecutorService} which executed on a provided looper. */ -public class LooperExecuter extends AbstractExecutorService { +public class LooperExecutor extends AbstractExecutorService { private final Handler mHandler; - public LooperExecuter(Looper looper) { + public LooperExecutor(Looper looper) { mHandler = new Handler(looper); } diff --git a/src/com/android/launcher3/util/LooperIdleLock.java b/src/com/android/launcher3/util/LooperIdleLock.java new file mode 100644 index 0000000000..35cac14e37 --- /dev/null +++ b/src/com/android/launcher3/util/LooperIdleLock.java @@ -0,0 +1,71 @@ +/* + * 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.launcher3.util; + +import android.os.Looper; +import android.os.MessageQueue; + +import com.android.launcher3.Utilities; + +/** + * Utility class to block execution until the UI looper is idle. + */ +public class LooperIdleLock implements MessageQueue.IdleHandler, Runnable { + + private final Object mLock; + + private boolean mIsLocked; + + public LooperIdleLock(Object lock, Looper looper) { + mLock = lock; + mIsLocked = true; + if (Utilities.ATLEAST_MARSHMALLOW) { + looper.getQueue().addIdleHandler(this); + } else { + // Looper.myQueue() only gives the current queue. Move the execution to the UI thread + // so that the IdleHandler is attached to the correct message queue. + new LooperExecutor(looper).execute(this); + } + } + + @Override + public void run() { + Looper.myQueue().addIdleHandler(this); + } + + @Override + public boolean queueIdle() { + synchronized (mLock) { + mIsLocked = false; + mLock.notify(); + } + return false; + } + + public boolean awaitLocked(long ms) { + if (mIsLocked) { + try { + // Just in case mFlushingWorkerThread changes but we aren't woken up, + // wait no longer than 1sec at a time + mLock.wait(ms); + } catch (InterruptedException ex) { + // Ignore + } + } + return mIsLocked; + } +} diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java index 9bd2882444..4cb6ca8314 100644 --- a/src/com/android/launcher3/util/ViewOnDrawExecutor.java +++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java @@ -16,12 +16,10 @@ package com.android.launcher3.util; -import android.util.Log; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewTreeObserver.OnDrawListener; -import com.android.launcher3.DeferredHandler; import com.android.launcher3.Launcher; import java.util.ArrayList; @@ -34,7 +32,7 @@ public class ViewOnDrawExecutor implements Executor, OnDrawListener, Runnable, OnAttachStateChangeListener { private final ArrayList mTasks = new ArrayList<>(); - private final DeferredHandler mHandler; + private final Executor mExecutor; private Launcher mLauncher; private View mAttachedView; @@ -43,8 +41,8 @@ public class ViewOnDrawExecutor implements Executor, OnDrawListener, Runnable, private boolean mLoadAnimationCompleted; private boolean mFirstDrawCompleted; - public ViewOnDrawExecutor(DeferredHandler handler) { - mHandler = handler; + public ViewOnDrawExecutor(Executor executor) { + mExecutor = executor; } public void attachTo(Launcher launcher) { @@ -92,7 +90,7 @@ public class ViewOnDrawExecutor implements Executor, OnDrawListener, Runnable, // Post the pending tasks after both onDraw and onLoadAnimationCompleted have been called. if (mLoadAnimationCompleted && mFirstDrawCompleted && !mCompleted) { for (final Runnable r : mTasks) { - mHandler.post(r); + mExecutor.execute(r); } markCompleted(); } diff --git a/tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java b/tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java index b9944db98d..633c3f0879 100644 --- a/tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java +++ b/tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java @@ -16,7 +16,6 @@ import android.test.ProviderTestCase2; import com.android.launcher3.AllAppsList; import com.android.launcher3.AppFilter; import com.android.launcher3.AppInfo; -import com.android.launcher3.DeferredHandler; import com.android.launcher3.IconCache; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.ItemInfo; @@ -36,6 +35,7 @@ import java.io.InputStreamReader; import java.lang.reflect.Field; import java.util.HashMap; import java.util.List; +import java.util.concurrent.Executor; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Mockito.atLeast; @@ -102,14 +102,14 @@ public class BaseModelUpdateTaskTestCase extends ProviderTestCase2 captor = ArgumentCaptor.forClass(Runnable.class); - verify(mockHandler, atLeast(0)).post(captor.capture()); + verify(mockExecutor, atLeast(0)).execute(captor.capture()); return captor.getAllValues(); } diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java index df2b662851..97f7b505a1 100644 --- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java +++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java @@ -39,13 +39,12 @@ import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.PackageInstallerCompat; import com.android.launcher3.ui.LauncherInstrumentationTestCase; import com.android.launcher3.util.ContentWriter; -import com.android.launcher3.util.LooperExecuter; +import com.android.launcher3.util.LooperExecutor; import com.android.launcher3.widget.PendingAddWidgetInfo; import com.android.launcher3.widget.WidgetHostViewLoader; import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** @@ -340,7 +339,7 @@ public class BindWidgetTest extends LauncherInstrumentationTestCase { * Blocks the current thread until all the jobs in the main worker thread are complete. */ private void waitUntilLoaderIdle() throws Exception { - new LooperExecuter(LauncherModel.getWorkerLooper()) + new LooperExecutor(LauncherModel.getWorkerLooper()) .submit(new Runnable() { @Override public void run() { }