Make ThrottlingCursorLoader smarter

Now the class initially uses smaller timeout, and expand it when detecting
multiple changes in a short period.

This CL makes the UI look more responsive especially on the message list +
message view mode.  e.g. Starring on the message view will quickly be
reflected to the message list.

Bug 3024799
Bug 3027832

Change-Id: Ie2d44c3769d43e3fd0f54ee526556eb3bad5e288
This commit is contained in:
Makoto Onuki 2010-09-21 19:02:21 -07:00
parent 2dbb510657
commit 4209ea36b0
6 changed files with 144 additions and 29 deletions

View File

@ -38,8 +38,6 @@ import android.widget.TextView;
* simpler for now.) Maybe we can just use SimpleCursorAdapter.
*/
public class AccountSelectorAdapter extends CursorAdapter {
private static final int REFRESH_INTERVAL = 3000; // in ms
private static final String[] PROJECTION = new String[] {
EmailContent.RECORD_ID,
EmailContent.Account.DISPLAY_NAME,
@ -58,7 +56,7 @@ public class AccountSelectorAdapter extends CursorAdapter {
public static Loader<Cursor> createLoader(Context context) {
return new ThrottlingCursorLoader(context, EmailContent.Account.CONTENT_URI, PROJECTION,
null, null, ORDER_BY, REFRESH_INTERVAL);
null, null, ORDER_BY);
}
public AccountSelectorAdapter(Context context, Cursor c) {

View File

@ -53,8 +53,6 @@ import android.widget.TextView;
public static final int MODE_NORMAL = 0;
public static final int MODE_MOVE_TO_TARGET = 1;
private static final int AUTO_REQUERY_TIMEOUT = 3 * 1000; // in ms
private static final String[] PROJECTION = new String[] { MailboxColumns.ID,
MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE, MailboxColumns.UNREAD_COUNT,
MailboxColumns.MESSAGE_COUNT};
@ -177,8 +175,7 @@ import android.widget.TextView;
public MailboxesLoader(Context context, long accountId, int mode) {
super(context, EmailContent.Mailbox.CONTENT_URI,
MailboxesAdapter.PROJECTION, getSelection(mode),
new String[] { String.valueOf(accountId) },
MAILBOX_ORDER_BY, AUTO_REQUERY_TIMEOUT);
new String[] { String.valueOf(accountId) }, MAILBOX_ORDER_BY);
mContext = context;
mMode = mode;
}

View File

@ -87,10 +87,6 @@ import java.util.Set;
private final ColorStateList mTextColorPrimary;
private final ColorStateList mTextColorSecondary;
// How long we want to wait for refreshes (a good starting guess)
// I suspect this could be lowered down to even 1000 or so, but this seems ok for now
private static final int REFRESH_INTERVAL_MS = 2500;
private final java.text.DateFormat mDateFormat;
private final java.text.DateFormat mTimeFormat;
@ -309,7 +305,7 @@ import java.util.Set;
// Initialize with no where clause. We'll set it later.
super(context, EmailContent.Message.CONTENT_URI,
MESSAGE_PROJECTION, null, null,
EmailContent.MessageColumns.TIMESTAMP + " DESC", REFRESH_INTERVAL_MS);
EmailContent.MessageColumns.TIMESTAMP + " DESC");
mContext = context;
mMailboxId = mailboxId;
}

View File

@ -16,12 +16,15 @@
package com.android.email.data;
import com.android.email.Clock;
import android.content.Context;
import android.content.CursorLoader;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import java.security.InvalidParameterException;
import java.util.Timer;
import java.util.TimerTask;
@ -30,29 +33,66 @@ import java.util.TimerTask;
*
* This class overrides {@link android.content.Loader#onContentChanged}, and instead of immediately
* requerying, it waits until the specified timeout before doing so.
*
* There are two timeout settings: {@link #mMinTimeout} and {@link #mMaxTimeout}.
* We normally use {@link #mMinTimeout}, but if we detect more than one change in
* the {@link #TIMEOUT_EXTEND_INTERVAL} period, we double it, until it reaches {@link #mMaxTimeout}.
*/
public class ThrottlingCursorLoader extends CursorLoader {
private static final boolean DEBUG = false; // Don't submit with true
/* package */ static final int TIMEOUT_EXTEND_INTERVAL = 500;
private static final int DEFAULT_MIN_TIMEOUT = 150;
private static final int DEFAULT_MAX_TIMEOUT = 2500;
private static Timer sTimer = new Timer();
private final Clock mClock;
/** Handler for the UI thread. */
private final Handler mHandler = new Handler();
/** Minimum (default) timeout */
private final int mMinTimeout;
/** Max timeout */
private final int mMaxTimeout;
/** Content change auto-requery timeout, in milliseconds. */
private final int mTimeout;
private int mTimeout;
/** When onChanged() was last called. */
private long mLastOnChangedTime;
private ForceLoadTimerTask mRunningForceLoadTimerTask;
/**
* Constructor. Same as the one of {@link CursorLoader}, but takes {@code timeoutSecond}.
*
* @param timeout Content change auto-requery timeout in milliseconds.
*/
/** Constructor with default timeout */
public ThrottlingCursorLoader(Context context, Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder, int timeout) {
String[] selectionArgs, String sortOrder) {
this(context, uri, projection, selection, selectionArgs, sortOrder, DEFAULT_MIN_TIMEOUT,
DEFAULT_MAX_TIMEOUT);
}
/** Constructor that takes custom timeout */
public ThrottlingCursorLoader(Context context, Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder, int minTimeout, int maxTimeout) {
this(context, uri, projection, selection, selectionArgs, sortOrder, minTimeout, maxTimeout,
Clock.INSTANCE);
}
/** Constructor for tests. Clock is injectable. */
/* package */ ThrottlingCursorLoader(Context context, Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder,
int minTimeout, int maxTimeout, Clock clock) {
super(context, uri, projection, selection, selectionArgs, sortOrder);
mTimeout = timeout;
mClock = clock;
if (maxTimeout < minTimeout) {
throw new InvalidParameterException();
}
mMinTimeout = minTimeout;
mMaxTimeout = maxTimeout;
mTimeout = mMinTimeout;
}
private void debugLog(String message) {
@ -98,19 +138,34 @@ public class ThrottlingCursorLoader extends CursorLoader {
super.stopLoading();
}
/* package */ void updateTimeout() {
final long now = mClock.getTime();
if ((now - mLastOnChangedTime) <= TIMEOUT_EXTEND_INTERVAL) {
if (DEBUG) debugLog("Extending timeout: " + mTimeout);
mTimeout *= 2;
if (mTimeout >= mMaxTimeout) {
mTimeout = mMaxTimeout;
}
} else {
if (DEBUG) debugLog("Resetting timeout.");
mTimeout = mMinTimeout;
}
mLastOnChangedTime = now;
}
@Override
public void onContentChanged() {
if (DEBUG) debugLog("onContentChanged");
if (mTimeout <= 0) {
forceLoad();
updateTimeout();
if (isForceLoadScheduled()) {
if (DEBUG) debugLog(" forceLoad already scheduled.");
} else {
if (isForceLoadScheduled()) {
if (DEBUG) debugLog(" forceLoad already scheduled.");
} else {
if (DEBUG) debugLog(" scheduling forceLoad.");
mRunningForceLoadTimerTask = new ForceLoadTimerTask();
sTimer.schedule(mRunningForceLoadTimerTask, mTimeout);
}
if (DEBUG) debugLog(" scheduling forceLoad.");
mRunningForceLoadTimerTask = new ForceLoadTimerTask();
sTimer.schedule(mRunningForceLoadTimerTask, mTimeout);
}
}
@ -140,4 +195,12 @@ public class ThrottlingCursorLoader extends CursorLoader {
}
}
}
/* package */ int getTimeoutForTest() {
return mTimeout;
}
/* package */ long getLastOnChangedTimeForTest() {
return mLastOnChangedTime;
}
}

View File

@ -29,4 +29,8 @@ public class MockClock extends Clock {
public void advance() {
mTime++;
}
public void advance(long milliseconds) {
mTime += milliseconds;
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright (C) 2010 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.email.data;
import com.android.email.MockClock;
import android.test.AndroidTestCase;
public class ThrottlingCursorLoaderTest extends AndroidTestCase {
public void testUpdateTimeout() {
MockClock clock = new MockClock();
ThrottlingCursorLoader l = new ThrottlingCursorLoader(getContext(), null, null, null, null,
null, 100, 500, clock);
// Check initial value
assertEquals(100, l.getTimeoutForTest());
// First call -- won't change the timeout
l.updateTimeout();
assertEquals(100, l.getTimeoutForTest());
// Call again in 10 ms -- will extend timeout.
clock.advance(10);
l.updateTimeout();
assertEquals(200, l.getTimeoutForTest());
// Call again in TIMEOUT_EXTEND_INTERAVL ms -- will extend timeout.
clock.advance(ThrottlingCursorLoader.TIMEOUT_EXTEND_INTERVAL);
l.updateTimeout();
assertEquals(400, l.getTimeoutForTest());
// Again -- timeout reaches max.
clock.advance(ThrottlingCursorLoader.TIMEOUT_EXTEND_INTERVAL);
l.updateTimeout();
assertEquals(500, l.getTimeoutForTest());
// Call in TIMEOUT_EXTEND_INTERAVL + 1 ms -- timeout will get reset.
clock.advance(ThrottlingCursorLoader.TIMEOUT_EXTEND_INTERVAL + 1);
l.updateTimeout();
assertEquals(100, l.getTimeoutForTest());
}
}