2010-08-10 00:48:53 +00:00
|
|
|
/*
|
|
|
|
* 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.service;
|
|
|
|
|
2010-12-09 01:11:04 +00:00
|
|
|
import android.accounts.AccountManager;
|
2010-11-29 21:21:11 +00:00
|
|
|
import android.app.AlarmManager;
|
|
|
|
import android.app.PendingIntent;
|
2010-08-10 00:48:53 +00:00
|
|
|
import android.app.Service;
|
2010-11-29 21:21:11 +00:00
|
|
|
import android.content.BroadcastReceiver;
|
2010-08-10 00:48:53 +00:00
|
|
|
import android.content.ContentValues;
|
|
|
|
import android.content.Context;
|
|
|
|
import android.content.Intent;
|
|
|
|
import android.database.Cursor;
|
2011-03-08 02:01:58 +00:00
|
|
|
import android.net.ConnectivityManager;
|
2010-12-09 01:11:04 +00:00
|
|
|
import android.net.Uri;
|
2014-02-18 21:51:43 +00:00
|
|
|
import android.os.AsyncTask;
|
2010-08-10 00:48:53 +00:00
|
|
|
import android.os.IBinder;
|
|
|
|
import android.os.RemoteException;
|
Add a retry backoff/limit policy to attachment download
b/11081672
Prior to this, any time the AttachmentDownloadService
got a CONNECTION_ERROR, it would just instantly retry,
without any limit on the number of tries. This is bad
if the server is in a funny state, we'll just keep spamming
it with multiple connection attempts per second. Also,
this kills the client device's battery and responsiveness.
Now, it will retry instantly five times, and then retry on a
10 second delay 5 more times. After that it will give up.
Even if it gives up, if the user visits an email with an
attachment, or taps on an attachment to expand it, we'll
start the process over. So we shouldn't have permanent
apparently data loss, even if we fail on the first 10 tries.
I'm not certain that this is the best backoff/limit policy,
maybe we should add a delay after even the first connection
error. But I'm hesitant to change this at this point, it's
possible that something is relying on this behavior and
we don't have a lot of soak time left.
Change-Id: I53d75d5d214ccca887a89cf65b799fe640cc9bc5
2013-10-09 18:03:40 +00:00
|
|
|
import android.os.SystemClock;
|
2010-08-10 00:48:53 +00:00
|
|
|
import android.text.format.DateUtils;
|
|
|
|
|
2011-07-15 16:46:28 +00:00
|
|
|
import com.android.email.AttachmentInfo;
|
|
|
|
import com.android.email.EmailConnectivityManager;
|
|
|
|
import com.android.email.NotificationController;
|
|
|
|
import com.android.emailcommon.provider.Account;
|
|
|
|
import com.android.emailcommon.provider.EmailContent;
|
|
|
|
import com.android.emailcommon.provider.EmailContent.Attachment;
|
2012-06-28 17:40:46 +00:00
|
|
|
import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
|
2011-07-15 16:46:28 +00:00
|
|
|
import com.android.emailcommon.provider.EmailContent.Message;
|
|
|
|
import com.android.emailcommon.service.EmailServiceProxy;
|
|
|
|
import com.android.emailcommon.service.EmailServiceStatus;
|
|
|
|
import com.android.emailcommon.service.IEmailServiceCallback;
|
|
|
|
import com.android.emailcommon.utility.AttachmentUtilities;
|
|
|
|
import com.android.emailcommon.utility.Utility;
|
2013-06-19 23:11:32 +00:00
|
|
|
import com.android.mail.providers.UIProvider.AttachmentState;
|
2013-05-26 04:32:32 +00:00
|
|
|
import com.android.mail.utils.LogUtils;
|
2011-07-15 16:46:28 +00:00
|
|
|
|
2010-08-10 00:48:53 +00:00
|
|
|
import java.io.File;
|
2010-10-22 23:35:46 +00:00
|
|
|
import java.io.FileDescriptor;
|
|
|
|
import java.io.PrintWriter;
|
2010-08-20 16:53:46 +00:00
|
|
|
import java.util.Comparator;
|
2010-08-10 00:48:53 +00:00
|
|
|
import java.util.HashMap;
|
2010-08-20 16:53:46 +00:00
|
|
|
import java.util.Iterator;
|
2014-02-18 21:51:43 +00:00
|
|
|
import java.util.Queue;
|
2010-08-20 16:53:46 +00:00
|
|
|
import java.util.TreeSet;
|
2010-11-29 21:21:11 +00:00
|
|
|
import java.util.concurrent.ConcurrentHashMap;
|
2014-02-18 21:51:43 +00:00
|
|
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
2010-08-10 00:48:53 +00:00
|
|
|
|
|
|
|
public class AttachmentDownloadService extends Service implements Runnable {
|
2013-10-31 20:08:40 +00:00
|
|
|
public static final String TAG = LogUtils.TAG;
|
2010-08-10 00:48:53 +00:00
|
|
|
|
Add a retry backoff/limit policy to attachment download
b/11081672
Prior to this, any time the AttachmentDownloadService
got a CONNECTION_ERROR, it would just instantly retry,
without any limit on the number of tries. This is bad
if the server is in a funny state, we'll just keep spamming
it with multiple connection attempts per second. Also,
this kills the client device's battery and responsiveness.
Now, it will retry instantly five times, and then retry on a
10 second delay 5 more times. After that it will give up.
Even if it gives up, if the user visits an email with an
attachment, or taps on an attachment to expand it, we'll
start the process over. So we shouldn't have permanent
apparently data loss, even if we fail on the first 10 tries.
I'm not certain that this is the best backoff/limit policy,
maybe we should add a delay after even the first connection
error. But I'm hesitant to change this at this point, it's
possible that something is relying on this behavior and
we don't have a lot of soak time left.
Change-Id: I53d75d5d214ccca887a89cf65b799fe640cc9bc5
2013-10-09 18:03:40 +00:00
|
|
|
// Minimum wait time before retrying a download that failed due to connection error
|
|
|
|
private static final long CONNECTION_ERROR_RETRY_MILLIS = 10 * DateUtils.SECOND_IN_MILLIS;
|
|
|
|
// Number of retries before we start delaying between
|
|
|
|
private static final long CONNECTION_ERROR_DELAY_THRESHOLD = 5;
|
|
|
|
// Maximum time to retry for connection errors.
|
|
|
|
private static final long CONNECTION_ERROR_MAX_RETRIES = 10;
|
|
|
|
|
2010-08-10 00:48:53 +00:00
|
|
|
// Our idle time, waiting for notifications; this is something of a failsafe
|
|
|
|
private static final int PROCESS_QUEUE_WAIT_TIME = 30 * ((int)DateUtils.MINUTE_IN_MILLIS);
|
2010-11-29 21:21:11 +00:00
|
|
|
// How often our watchdog checks for callback timeouts
|
2012-06-28 17:40:46 +00:00
|
|
|
private static final int WATCHDOG_CHECK_INTERVAL = 20 * ((int)DateUtils.SECOND_IN_MILLIS);
|
2010-11-29 21:21:11 +00:00
|
|
|
// How long we'll wait for a callback before canceling a download and retrying
|
|
|
|
private static final int CALLBACK_TIMEOUT = 30 * ((int)DateUtils.SECOND_IN_MILLIS);
|
2011-01-21 19:58:39 +00:00
|
|
|
// Try to download an attachment in the background this many times before giving up
|
|
|
|
private static final int MAX_DOWNLOAD_RETRIES = 5;
|
2010-08-20 16:53:46 +00:00
|
|
|
private static final int PRIORITY_NONE = -1;
|
2010-08-10 00:48:53 +00:00
|
|
|
@SuppressWarnings("unused")
|
2010-08-20 16:53:46 +00:00
|
|
|
// Low priority will be used for opportunistic downloads
|
2011-01-21 19:58:39 +00:00
|
|
|
private static final int PRIORITY_BACKGROUND = 0;
|
2010-08-20 16:53:46 +00:00
|
|
|
// Normal priority is for forwarded downloads in outgoing mail
|
2011-01-21 19:58:39 +00:00
|
|
|
private static final int PRIORITY_SEND_MAIL = 1;
|
2010-08-20 16:53:46 +00:00
|
|
|
// High priority is for user requests
|
2011-01-21 19:58:39 +00:00
|
|
|
private static final int PRIORITY_FOREGROUND = 2;
|
2010-08-10 00:48:53 +00:00
|
|
|
|
2010-12-09 01:11:04 +00:00
|
|
|
// Minimum free storage in order to perform prefetch (25% of total memory)
|
|
|
|
private static final float PREFETCH_MINIMUM_STORAGE_AVAILABLE = 0.25F;
|
|
|
|
// Maximum prefetch storage (also 25% of total memory)
|
|
|
|
private static final float PREFETCH_MAXIMUM_ATTACHMENT_STORAGE = 0.25F;
|
|
|
|
|
2010-08-10 00:48:53 +00:00
|
|
|
// We can try various values here; I think 2 is completely reasonable as a first pass
|
|
|
|
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
|
|
|
|
// Limit on the number of simultaneous downloads per account
|
|
|
|
// Note that a limit of 1 is currently enforced by both Services (MailService and Controller)
|
|
|
|
private static final int MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT = 1;
|
2011-01-20 01:44:55 +00:00
|
|
|
// Limit on the number of attachments we'll check for background download
|
|
|
|
private static final int MAX_ATTACHMENTS_TO_CHECK = 25;
|
2010-12-09 01:11:04 +00:00
|
|
|
|
2011-07-15 16:46:28 +00:00
|
|
|
private static final String EXTRA_ATTACHMENT =
|
|
|
|
"com.android.email.AttachmentDownloadService.attachment";
|
|
|
|
|
2011-01-26 17:03:11 +00:00
|
|
|
// sRunningService is only set in the UI thread; it's visibility elsewhere is guaranteed
|
|
|
|
// by the use of "volatile"
|
|
|
|
/*package*/ static volatile AttachmentDownloadService sRunningService = null;
|
2010-08-10 00:48:53 +00:00
|
|
|
|
2010-08-20 16:53:46 +00:00
|
|
|
/*package*/ Context mContext;
|
2011-01-26 17:03:11 +00:00
|
|
|
/*package*/ EmailConnectivityManager mConnectivityManager;
|
2011-01-24 23:51:26 +00:00
|
|
|
|
2010-08-20 16:53:46 +00:00
|
|
|
/*package*/ final DownloadSet mDownloadSet = new DownloadSet(new DownloadComparator());
|
2010-11-29 21:21:11 +00:00
|
|
|
|
2011-02-13 02:56:09 +00:00
|
|
|
private final HashMap<Long, Intent> mAccountServiceMap = new HashMap<Long, Intent>();
|
2010-12-09 01:11:04 +00:00
|
|
|
// A map of attachment storage used per account
|
|
|
|
// NOTE: This map is not kept current in terms of deletions (i.e. it stores the last calculated
|
|
|
|
// amount plus the size of any new attachments laoded). If and when we reach the per-account
|
|
|
|
// limit, we recalculate the actual usage
|
|
|
|
/*package*/ final HashMap<Long, Long> mAttachmentStorageMap = new HashMap<Long, Long>();
|
2011-01-21 19:58:39 +00:00
|
|
|
// A map of attachment ids to the number of failed attempts to download the attachment
|
|
|
|
// NOTE: We do not want to persist this. This allows us to retry background downloading
|
|
|
|
// if any transient network errors are fixed & and the app is restarted
|
|
|
|
/* package */ final HashMap<Long, Integer> mAttachmentFailureMap = new HashMap<Long, Integer>();
|
2010-08-10 00:48:53 +00:00
|
|
|
private final ServiceCallback mServiceCallback = new ServiceCallback();
|
2010-12-09 01:11:04 +00:00
|
|
|
|
2010-08-10 00:48:53 +00:00
|
|
|
private final Object mLock = new Object();
|
|
|
|
private volatile boolean mStop = false;
|
|
|
|
|
2010-12-09 01:11:04 +00:00
|
|
|
/*package*/ AccountManagerStub mAccountManagerStub;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* We only use the getAccounts() call from AccountManager, so this class wraps that call and
|
|
|
|
* allows us to build a mock account manager stub in the unit tests
|
|
|
|
*/
|
|
|
|
/*package*/ static class AccountManagerStub {
|
|
|
|
private int mNumberOfAccounts;
|
|
|
|
private final AccountManager mAccountManager;
|
|
|
|
|
|
|
|
AccountManagerStub(Context context) {
|
|
|
|
if (context != null) {
|
|
|
|
mAccountManager = AccountManager.get(context);
|
|
|
|
} else {
|
|
|
|
mAccountManager = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*package*/ int getNumberOfAccounts() {
|
|
|
|
if (mAccountManager != null) {
|
|
|
|
return mAccountManager.getAccounts().length;
|
|
|
|
} else {
|
|
|
|
return mNumberOfAccounts;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*package*/ void setNumberOfAccounts(int numberOfAccounts) {
|
|
|
|
mNumberOfAccounts = numberOfAccounts;
|
|
|
|
}
|
|
|
|
}
|
2010-11-29 21:21:11 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Watchdog alarm receiver; responsible for making sure that downloads in progress are not
|
|
|
|
* stalled, as determined by the timing of the most recent service callback
|
|
|
|
*/
|
|
|
|
public static class Watchdog extends BroadcastReceiver {
|
|
|
|
@Override
|
|
|
|
public void onReceive(final Context context, Intent intent) {
|
|
|
|
new Thread(new Runnable() {
|
2012-06-28 17:40:46 +00:00
|
|
|
@Override
|
2010-11-29 21:21:11 +00:00
|
|
|
public void run() {
|
|
|
|
watchdogAlarm();
|
|
|
|
}
|
|
|
|
}, "AttachmentDownloadService Watchdog").start();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-08-20 16:53:46 +00:00
|
|
|
public static class DownloadRequest {
|
|
|
|
final int priority;
|
|
|
|
final long time;
|
|
|
|
final long attachmentId;
|
|
|
|
final long messageId;
|
|
|
|
final long accountId;
|
2010-08-10 00:48:53 +00:00
|
|
|
boolean inProgress = false;
|
2010-10-22 23:35:46 +00:00
|
|
|
int lastStatusCode;
|
|
|
|
int lastProgress;
|
|
|
|
long lastCallbackTime;
|
|
|
|
long startTime;
|
Add a retry backoff/limit policy to attachment download
b/11081672
Prior to this, any time the AttachmentDownloadService
got a CONNECTION_ERROR, it would just instantly retry,
without any limit on the number of tries. This is bad
if the server is in a funny state, we'll just keep spamming
it with multiple connection attempts per second. Also,
this kills the client device's battery and responsiveness.
Now, it will retry instantly five times, and then retry on a
10 second delay 5 more times. After that it will give up.
Even if it gives up, if the user visits an email with an
attachment, or taps on an attachment to expand it, we'll
start the process over. So we shouldn't have permanent
apparently data loss, even if we fail on the first 10 tries.
I'm not certain that this is the best backoff/limit policy,
maybe we should add a delay after even the first connection
error. But I'm hesitant to change this at this point, it's
possible that something is relying on this behavior and
we don't have a lot of soak time left.
Change-Id: I53d75d5d214ccca887a89cf65b799fe640cc9bc5
2013-10-09 18:03:40 +00:00
|
|
|
long retryCount;
|
|
|
|
long retryStartTime;
|
2010-08-10 00:48:53 +00:00
|
|
|
|
|
|
|
private DownloadRequest(Context context, Attachment attachment) {
|
|
|
|
attachmentId = attachment.mId;
|
|
|
|
Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey);
|
|
|
|
if (msg != null) {
|
|
|
|
accountId = msg.mAccountKey;
|
|
|
|
messageId = msg.mId;
|
2010-08-20 16:53:46 +00:00
|
|
|
} else {
|
|
|
|
accountId = messageId = -1;
|
|
|
|
}
|
|
|
|
priority = getPriority(attachment);
|
Fix an infinitely retrying download problem
b/11436795
If an attachment download fails due to a timeout, or
an exception being thrown from startDownload(), we'd call
cancelDownload() on it. But this didn't actually cancel,
it would remove it from the inProgres list, but leave it
in the list of all downloads, so we'd immediately retry it.
This is bad for two reasons:
1. It can starve out other attachment downloads that could
have been successful.
2. It will keep attempting to do network work, even if it's
hopeless, forever, draining battery.
Now, if an attachment download fails in this way, for the first
few times, we'll move it to the tail end of the list of
downloads we'd like to perform. If it fails more than 10 times,
we'll give up completely. Giving up is not permanent, if we
have a reason to attempt a download again (such as the user
tapping on it), then it will get added back to the download
service and retried.
Change-Id: I5364a7d8b4b25ce299b8dcf061db6e9ce12daf75
2013-11-08 21:57:51 +00:00
|
|
|
time = SystemClock.elapsedRealtime();
|
2010-08-20 16:53:46 +00:00
|
|
|
}
|
|
|
|
|
Fix an infinitely retrying download problem
b/11436795
If an attachment download fails due to a timeout, or
an exception being thrown from startDownload(), we'd call
cancelDownload() on it. But this didn't actually cancel,
it would remove it from the inProgres list, but leave it
in the list of all downloads, so we'd immediately retry it.
This is bad for two reasons:
1. It can starve out other attachment downloads that could
have been successful.
2. It will keep attempting to do network work, even if it's
hopeless, forever, draining battery.
Now, if an attachment download fails in this way, for the first
few times, we'll move it to the tail end of the list of
downloads we'd like to perform. If it fails more than 10 times,
we'll give up completely. Giving up is not permanent, if we
have a reason to attempt a download again (such as the user
tapping on it), then it will get added back to the download
service and retried.
Change-Id: I5364a7d8b4b25ce299b8dcf061db6e9ce12daf75
2013-11-08 21:57:51 +00:00
|
|
|
private DownloadRequest(DownloadRequest orig, long newTime) {
|
|
|
|
priority = orig.priority;
|
|
|
|
attachmentId = orig.attachmentId;
|
|
|
|
messageId = orig.messageId;
|
|
|
|
accountId = orig.accountId;
|
|
|
|
time = newTime;
|
|
|
|
inProgress = orig.inProgress;
|
|
|
|
lastStatusCode = orig.lastStatusCode;
|
|
|
|
lastProgress = orig.lastProgress;
|
|
|
|
lastCallbackTime = orig.lastCallbackTime;
|
|
|
|
startTime = orig.startTime;
|
|
|
|
retryCount = orig.retryCount;
|
|
|
|
retryStartTime = orig.retryStartTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2010-08-20 16:53:46 +00:00
|
|
|
@Override
|
|
|
|
public int hashCode() {
|
|
|
|
return (int)attachmentId;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Two download requests are equals if their attachment id's are equals
|
|
|
|
*/
|
|
|
|
@Override
|
|
|
|
public boolean equals(Object object) {
|
|
|
|
if (!(object instanceof DownloadRequest)) return false;
|
|
|
|
DownloadRequest req = (DownloadRequest)object;
|
|
|
|
return req.attachmentId == attachmentId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Comparator class for the download set; we first compare by priority. Requests with equal
|
|
|
|
* priority are compared by the time the request was created (older requests come first)
|
|
|
|
*/
|
|
|
|
/*protected*/ static class DownloadComparator implements Comparator<DownloadRequest> {
|
|
|
|
@Override
|
|
|
|
public int compare(DownloadRequest req1, DownloadRequest req2) {
|
|
|
|
int res;
|
|
|
|
if (req1.priority != req2.priority) {
|
|
|
|
res = (req1.priority < req2.priority) ? -1 : 1;
|
|
|
|
} else {
|
|
|
|
if (req1.time == req2.time) {
|
|
|
|
res = 0;
|
|
|
|
} else {
|
|
|
|
res = (req1.time > req2.time) ? -1 : 1;
|
|
|
|
}
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
2010-08-20 16:53:46 +00:00
|
|
|
return res;
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2010-08-20 16:53:46 +00:00
|
|
|
* The DownloadSet is a TreeSet sorted by priority class (e.g. low, high, etc.) and the
|
|
|
|
* time of the request. Higher priority requests
|
2010-08-10 00:48:53 +00:00
|
|
|
* are always processed first; among equals, the oldest request is processed first. The
|
|
|
|
* priority key represents this ordering. Note: All methods that change the attachment map are
|
|
|
|
* synchronized on the map itself
|
|
|
|
*/
|
2010-08-20 16:53:46 +00:00
|
|
|
/*package*/ class DownloadSet extends TreeSet<DownloadRequest> {
|
2010-08-10 00:48:53 +00:00
|
|
|
private static final long serialVersionUID = 1L;
|
2010-11-29 21:21:11 +00:00
|
|
|
private PendingIntent mWatchdogPendingIntent;
|
2010-08-10 00:48:53 +00:00
|
|
|
|
2010-08-20 16:53:46 +00:00
|
|
|
/*package*/ DownloadSet(Comparator<? super DownloadRequest> comparator) {
|
|
|
|
super(comparator);
|
|
|
|
}
|
2010-08-10 00:48:53 +00:00
|
|
|
|
|
|
|
/**
|
2010-08-20 16:53:46 +00:00
|
|
|
* Maps attachment id to DownloadRequest
|
2010-08-10 00:48:53 +00:00
|
|
|
*/
|
2010-11-29 21:21:11 +00:00
|
|
|
/*package*/ final ConcurrentHashMap<Long, DownloadRequest> mDownloadsInProgress =
|
|
|
|
new ConcurrentHashMap<Long, DownloadRequest>();
|
2010-08-10 00:48:53 +00:00
|
|
|
|
2014-03-27 07:31:06 +00:00
|
|
|
private void markAttachmentAsFailed(final Attachment att) {
|
|
|
|
final ContentValues cv = new ContentValues();
|
|
|
|
final int flags = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
|
2014-04-11 21:42:28 +00:00
|
|
|
cv.put(AttachmentColumns.FLAGS, att.mFlags &= ~flags);
|
|
|
|
cv.put(AttachmentColumns.UI_STATE, AttachmentState.FAILED);
|
2014-03-27 07:31:06 +00:00
|
|
|
att.update(mContext, cv);
|
|
|
|
}
|
|
|
|
|
2010-08-10 00:48:53 +00:00
|
|
|
/**
|
|
|
|
* onChange is called by the AttachmentReceiver upon receipt of a valid notification from
|
|
|
|
* EmailProvider that an attachment has been inserted or modified. It's not strictly
|
|
|
|
* necessary that we detect a deleted attachment, as the code always checks for the
|
|
|
|
* existence of an attachment before acting on it.
|
|
|
|
*/
|
2011-01-26 17:03:11 +00:00
|
|
|
public synchronized void onChange(Context context, Attachment att) {
|
2010-08-20 16:53:46 +00:00
|
|
|
DownloadRequest req = findDownloadRequest(att.mId);
|
|
|
|
long priority = getPriority(att);
|
|
|
|
if (priority == PRIORITY_NONE) {
|
2013-10-10 19:32:50 +00:00
|
|
|
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, "== Attachment changed: " + att.mId);
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
// In this case, there is no download priority for this attachment
|
2010-08-20 16:53:46 +00:00
|
|
|
if (req != null) {
|
|
|
|
// If it exists in the map, remove it
|
2010-08-10 00:48:53 +00:00
|
|
|
// NOTE: We don't yet support deleting downloads in progress
|
2013-10-10 19:32:50 +00:00
|
|
|
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, "== Attachment " + att.mId + " was in queue, removing");
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
2010-08-20 16:53:46 +00:00
|
|
|
remove(req);
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Ignore changes that occur during download
|
|
|
|
if (mDownloadsInProgress.containsKey(att.mId)) return;
|
2010-08-20 16:53:46 +00:00
|
|
|
// If this is new, add the request to the queue
|
2010-08-10 00:48:53 +00:00
|
|
|
if (req == null) {
|
2011-01-26 17:03:11 +00:00
|
|
|
req = new DownloadRequest(context, att);
|
2014-03-27 07:31:06 +00:00
|
|
|
final AttachmentInfo attachInfo = new AttachmentInfo(context, att);
|
|
|
|
if (!attachInfo.isEligibleForDownload()) {
|
|
|
|
// We can't download this file due to policy, depending on what type
|
|
|
|
// of request we received, we handle the response differently.
|
|
|
|
if (((att.mFlags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) ||
|
|
|
|
((att.mFlags & Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD) != 0)) {
|
|
|
|
// There are a couple of situations where we will not even allow this
|
|
|
|
// request to go in the queue because we can already process it as a
|
|
|
|
// failure.
|
|
|
|
// 1. The user explictly wants to download this attachment from the
|
|
|
|
// email view but they should not be able to...either because there is
|
|
|
|
// no app to view it or because its been marked as a policy violation.
|
|
|
|
// 2. The user is forwarding an email and the attachment has been
|
|
|
|
// marked as a policy violation. If the attachment is non viewable
|
|
|
|
// that is OK for forwarding a message so we'll let it pass through
|
|
|
|
markAttachmentAsFailed(att);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// If we get this far it a forward of an attachment that is only
|
|
|
|
// ineligible because we can't view it or process it. Not because we
|
|
|
|
// can't download it for policy reasons. Let's let this go through because
|
|
|
|
// the final recipient of this forward email might be able to process it.
|
|
|
|
}
|
2010-08-20 16:53:46 +00:00
|
|
|
add(req);
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
// If the request already existed, we'll update the priority (so that the time is
|
|
|
|
// up-to-date); otherwise, we create a new request
|
2013-10-10 19:32:50 +00:00
|
|
|
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, "== Download queued for attachment " + att.mId + ", class " +
|
2010-08-20 16:53:46 +00:00
|
|
|
req.priority + ", priority time " + req.time);
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Process the queue if we're in a wait
|
|
|
|
kick();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2010-08-20 16:53:46 +00:00
|
|
|
* Find a queued DownloadRequest, given the attachment's id
|
2010-08-10 00:48:53 +00:00
|
|
|
* @param id the id of the attachment
|
2010-08-20 16:53:46 +00:00
|
|
|
* @return the DownloadRequest for that attachment (or null, if none)
|
2010-08-10 00:48:53 +00:00
|
|
|
*/
|
2010-08-20 16:53:46 +00:00
|
|
|
/*package*/ synchronized DownloadRequest findDownloadRequest(long id) {
|
|
|
|
Iterator<DownloadRequest> iterator = iterator();
|
|
|
|
while(iterator.hasNext()) {
|
|
|
|
DownloadRequest req = iterator.next();
|
|
|
|
if (req.attachmentId == id) {
|
|
|
|
return req;
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
}
|
2010-08-20 16:53:46 +00:00
|
|
|
return null;
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
|
2011-08-24 01:02:11 +00:00
|
|
|
@Override
|
2011-07-15 16:46:28 +00:00
|
|
|
public synchronized boolean isEmpty() {
|
|
|
|
return super.isEmpty() && mDownloadsInProgress.isEmpty();
|
|
|
|
}
|
|
|
|
|
2010-08-10 00:48:53 +00:00
|
|
|
/**
|
|
|
|
* Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing
|
|
|
|
* the limit on maximum downloads
|
|
|
|
*/
|
2010-08-20 16:53:46 +00:00
|
|
|
/*package*/ synchronized void processQueue() {
|
2013-10-10 19:32:50 +00:00
|
|
|
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, "== Checking attachment queue, " + mDownloadSet.size()
|
|
|
|
+ " entries");
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
2010-08-20 16:53:46 +00:00
|
|
|
Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator();
|
2010-08-10 00:48:53 +00:00
|
|
|
// First, start up any required downloads, in priority order
|
2010-08-20 16:53:46 +00:00
|
|
|
while (iterator.hasNext() &&
|
|
|
|
(mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS)) {
|
|
|
|
DownloadRequest req = iterator.next();
|
2010-12-09 01:11:04 +00:00
|
|
|
// Enforce per-account limit here
|
|
|
|
if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) {
|
2013-10-10 19:32:50 +00:00
|
|
|
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, "== Skip #" + req.attachmentId + "; maxed for acct #" +
|
2010-12-09 01:11:04 +00:00
|
|
|
req.accountId);
|
|
|
|
}
|
|
|
|
continue;
|
2012-06-28 17:40:46 +00:00
|
|
|
} else if (Attachment.restoreAttachmentWithId(mContext, req.attachmentId) == null) {
|
|
|
|
continue;
|
2010-12-09 01:11:04 +00:00
|
|
|
}
|
2010-08-10 00:48:53 +00:00
|
|
|
if (!req.inProgress) {
|
Add a retry backoff/limit policy to attachment download
b/11081672
Prior to this, any time the AttachmentDownloadService
got a CONNECTION_ERROR, it would just instantly retry,
without any limit on the number of tries. This is bad
if the server is in a funny state, we'll just keep spamming
it with multiple connection attempts per second. Also,
this kills the client device's battery and responsiveness.
Now, it will retry instantly five times, and then retry on a
10 second delay 5 more times. After that it will give up.
Even if it gives up, if the user visits an email with an
attachment, or taps on an attachment to expand it, we'll
start the process over. So we shouldn't have permanent
apparently data loss, even if we fail on the first 10 tries.
I'm not certain that this is the best backoff/limit policy,
maybe we should add a delay after even the first connection
error. But I'm hesitant to change this at this point, it's
possible that something is relying on this behavior and
we don't have a lot of soak time left.
Change-Id: I53d75d5d214ccca887a89cf65b799fe640cc9bc5
2013-10-09 18:03:40 +00:00
|
|
|
final long currentTime = SystemClock.elapsedRealtime();
|
|
|
|
if (req.retryCount > 0 && req.retryStartTime > currentTime) {
|
|
|
|
LogUtils.d(TAG, "== waiting to retry attachment %d", req.attachmentId);
|
|
|
|
setWatchdogAlarm(CONNECTION_ERROR_RETRY_MILLIS);
|
|
|
|
continue;
|
|
|
|
}
|
2014-03-27 07:31:06 +00:00
|
|
|
// TODO: We try to gate ineligible downloads from entering the queue but its
|
|
|
|
// always possible that they made it in here regardless in the future. In a
|
|
|
|
// perfect world, we would make it bullet proof with a check for eligibility
|
|
|
|
// here instead/also.
|
2010-08-20 16:53:46 +00:00
|
|
|
mDownloadSet.tryStartDownload(req);
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
}
|
2010-12-28 20:21:40 +00:00
|
|
|
|
2011-01-24 23:51:26 +00:00
|
|
|
// Don't prefetch if background downloading is disallowed
|
2011-08-03 20:28:01 +00:00
|
|
|
EmailConnectivityManager ecm = mConnectivityManager;
|
|
|
|
if (ecm == null) return;
|
2011-10-03 20:03:35 +00:00
|
|
|
if (!ecm.isAutoSyncAllowed()) return;
|
2011-03-08 02:01:58 +00:00
|
|
|
// Don't prefetch unless we're on a WiFi network
|
2011-08-03 20:28:01 +00:00
|
|
|
if (ecm.getActiveNetworkType() != ConnectivityManager.TYPE_WIFI) {
|
2011-03-08 02:01:58 +00:00
|
|
|
return;
|
|
|
|
}
|
2010-08-10 00:48:53 +00:00
|
|
|
// Then, try opportunistic download of appropriate attachments
|
|
|
|
int backgroundDownloads = MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size();
|
2010-12-09 01:11:04 +00:00
|
|
|
// Always leave one slot for user requested download
|
|
|
|
if (backgroundDownloads > (MAX_SIMULTANEOUS_DOWNLOADS - 1)) {
|
2011-01-20 01:44:55 +00:00
|
|
|
// We'll load up the newest 25 attachments that aren't loaded or queued
|
|
|
|
Uri lookupUri = EmailContent.uriWithLimit(Attachment.CONTENT_URI,
|
|
|
|
MAX_ATTACHMENTS_TO_CHECK);
|
2012-06-28 17:40:46 +00:00
|
|
|
Cursor c = mContext.getContentResolver().query(lookupUri,
|
|
|
|
Attachment.CONTENT_PROJECTION,
|
2011-05-12 00:15:59 +00:00
|
|
|
EmailContent.Attachment.PRECACHE_INBOX_SELECTION,
|
2014-04-11 21:42:28 +00:00
|
|
|
null, AttachmentColumns._ID + " DESC");
|
2011-01-20 01:44:55 +00:00
|
|
|
File cacheDir = mContext.getCacheDir();
|
|
|
|
try {
|
|
|
|
while (c.moveToNext()) {
|
2012-06-28 17:40:46 +00:00
|
|
|
Attachment att = new Attachment();
|
|
|
|
att.restore(c);
|
|
|
|
Account account = Account.restoreAccountWithId(mContext, att.mAccountKey);
|
2011-02-13 02:56:09 +00:00
|
|
|
if (account == null) {
|
2011-01-20 01:44:55 +00:00
|
|
|
// Clean up this orphaned attachment; there's no point in keeping it
|
|
|
|
// around; then try to find another one
|
2012-06-28 17:40:46 +00:00
|
|
|
EmailContent.delete(mContext, Attachment.CONTENT_URI, att.mId);
|
|
|
|
} else {
|
2011-01-20 01:44:55 +00:00
|
|
|
// Check that the attachment meets system requirements for download
|
2012-06-28 17:40:46 +00:00
|
|
|
AttachmentInfo info = new AttachmentInfo(mContext, att);
|
2011-01-20 01:44:55 +00:00
|
|
|
if (info.isEligibleForDownload()) {
|
2012-06-28 17:40:46 +00:00
|
|
|
// Either the account must be able to prefetch or this must be
|
|
|
|
// an inline attachment
|
|
|
|
if (att.mContentId != null ||
|
|
|
|
(canPrefetchForAccount(account, cacheDir))) {
|
2011-01-21 19:58:39 +00:00
|
|
|
Integer tryCount;
|
|
|
|
tryCount = mAttachmentFailureMap.get(att.mId);
|
|
|
|
if (tryCount != null && tryCount > MAX_DOWNLOAD_RETRIES) {
|
|
|
|
// move onto the next attachment
|
|
|
|
continue;
|
|
|
|
}
|
2011-01-20 01:44:55 +00:00
|
|
|
// Start this download and we're done
|
|
|
|
DownloadRequest req = new DownloadRequest(mContext, att);
|
|
|
|
mDownloadSet.tryStartDownload(req);
|
|
|
|
break;
|
|
|
|
}
|
2014-03-27 07:31:06 +00:00
|
|
|
} else {
|
|
|
|
// If this attachment was ineligible for download
|
|
|
|
// because of policy related issues, its flags would be set to
|
|
|
|
// FLAG_POLICY_DISALLOWS_DOWNLOAD and would not show up in the
|
|
|
|
// query results. We are most likely here for other reasons such
|
|
|
|
// as the inability to view the attachment. In that case, let's just
|
|
|
|
// skip it for now.
|
|
|
|
LogUtils.e(TAG, "== skip attachment %d, it is ineligible", att.mId);
|
2011-01-20 01:44:55 +00:00
|
|
|
}
|
|
|
|
}
|
2010-12-09 01:11:04 +00:00
|
|
|
}
|
2011-01-20 01:44:55 +00:00
|
|
|
} finally {
|
|
|
|
c.close();
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Count the number of running downloads in progress for this account
|
|
|
|
* @param accountId the id of the account
|
|
|
|
* @return the count of running downloads
|
|
|
|
*/
|
2010-08-20 16:53:46 +00:00
|
|
|
/*package*/ synchronized int downloadsForAccount(long accountId) {
|
2010-08-10 00:48:53 +00:00
|
|
|
int count = 0;
|
|
|
|
for (DownloadRequest req: mDownloadsInProgress.values()) {
|
|
|
|
if (req.accountId == accountId) {
|
|
|
|
count++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
|
2011-07-26 16:57:24 +00:00
|
|
|
/**
|
|
|
|
* Watchdog for downloads; we use this in case we are hanging on a download, which might
|
|
|
|
* have failed silently (the connection dropped, for example)
|
|
|
|
*/
|
2010-11-29 21:21:11 +00:00
|
|
|
private void onWatchdogAlarm() {
|
2012-06-28 17:40:46 +00:00
|
|
|
// If our service instance is gone, just leave
|
2012-01-25 22:59:54 +00:00
|
|
|
if (mStop) {
|
|
|
|
return;
|
|
|
|
}
|
2010-11-29 21:21:11 +00:00
|
|
|
long now = System.currentTimeMillis();
|
|
|
|
for (DownloadRequest req: mDownloadsInProgress.values()) {
|
|
|
|
// Check how long it's been since receiving a callback
|
|
|
|
long timeSinceCallback = now - req.lastCallbackTime;
|
|
|
|
if (timeSinceCallback > CALLBACK_TIMEOUT) {
|
2013-10-10 19:32:50 +00:00
|
|
|
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, "== Download of " + req.attachmentId + " timed out");
|
2010-11-29 21:21:11 +00:00
|
|
|
}
|
Add a retry backoff/limit policy to attachment download
b/11081672
Prior to this, any time the AttachmentDownloadService
got a CONNECTION_ERROR, it would just instantly retry,
without any limit on the number of tries. This is bad
if the server is in a funny state, we'll just keep spamming
it with multiple connection attempts per second. Also,
this kills the client device's battery and responsiveness.
Now, it will retry instantly five times, and then retry on a
10 second delay 5 more times. After that it will give up.
Even if it gives up, if the user visits an email with an
attachment, or taps on an attachment to expand it, we'll
start the process over. So we shouldn't have permanent
apparently data loss, even if we fail on the first 10 tries.
I'm not certain that this is the best backoff/limit policy,
maybe we should add a delay after even the first connection
error. But I'm hesitant to change this at this point, it's
possible that something is relying on this behavior and
we don't have a lot of soak time left.
Change-Id: I53d75d5d214ccca887a89cf65b799fe640cc9bc5
2013-10-09 18:03:40 +00:00
|
|
|
cancelDownload(req);
|
2010-11-29 21:21:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Check whether we can start new downloads...
|
2011-07-27 22:53:51 +00:00
|
|
|
if (mConnectivityManager != null && mConnectivityManager.hasConnectivity()) {
|
2011-03-01 21:42:30 +00:00
|
|
|
processQueue();
|
|
|
|
}
|
2012-06-28 17:40:46 +00:00
|
|
|
// If there are downloads in progress, reset alarm
|
|
|
|
if (!mDownloadsInProgress.isEmpty()) {
|
2013-10-10 19:32:50 +00:00
|
|
|
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, "Reschedule watchdog...");
|
2012-06-28 17:40:46 +00:00
|
|
|
}
|
|
|
|
setWatchdogAlarm();
|
|
|
|
}
|
2010-11-29 21:21:11 +00:00
|
|
|
}
|
|
|
|
|
2011-02-13 02:56:09 +00:00
|
|
|
/**
|
|
|
|
* Attempt to execute the DownloadRequest, enforcing the maximum downloads per account
|
|
|
|
* parameter
|
|
|
|
* @param req the DownloadRequest
|
|
|
|
* @return whether or not the download was started
|
|
|
|
*/
|
|
|
|
/*package*/ synchronized boolean tryStartDownload(DownloadRequest req) {
|
2012-06-28 17:40:46 +00:00
|
|
|
EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
|
2013-07-30 02:11:41 +00:00
|
|
|
AttachmentDownloadService.this, req.accountId);
|
2011-02-13 02:56:09 +00:00
|
|
|
|
|
|
|
// Do not download the same attachment multiple times
|
|
|
|
boolean alreadyInProgress = mDownloadsInProgress.get(req.attachmentId) != null;
|
|
|
|
if (alreadyInProgress) return false;
|
|
|
|
|
|
|
|
try {
|
2013-10-10 19:32:50 +00:00
|
|
|
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, ">> Starting download for attachment #" + req.attachmentId);
|
2011-02-13 02:56:09 +00:00
|
|
|
}
|
2012-06-28 17:40:46 +00:00
|
|
|
startDownload(service, req);
|
2011-02-13 02:56:09 +00:00
|
|
|
} catch (RemoteException e) {
|
|
|
|
// TODO: Consider whether we need to do more in this case...
|
|
|
|
// For now, fix up our data to reflect the failure
|
|
|
|
cancelDownload(req);
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private synchronized DownloadRequest getDownloadInProgress(long attachmentId) {
|
|
|
|
return mDownloadsInProgress.get(attachmentId);
|
|
|
|
}
|
|
|
|
|
Add a retry backoff/limit policy to attachment download
b/11081672
Prior to this, any time the AttachmentDownloadService
got a CONNECTION_ERROR, it would just instantly retry,
without any limit on the number of tries. This is bad
if the server is in a funny state, we'll just keep spamming
it with multiple connection attempts per second. Also,
this kills the client device's battery and responsiveness.
Now, it will retry instantly five times, and then retry on a
10 second delay 5 more times. After that it will give up.
Even if it gives up, if the user visits an email with an
attachment, or taps on an attachment to expand it, we'll
start the process over. So we shouldn't have permanent
apparently data loss, even if we fail on the first 10 tries.
I'm not certain that this is the best backoff/limit policy,
maybe we should add a delay after even the first connection
error. But I'm hesitant to change this at this point, it's
possible that something is relying on this behavior and
we don't have a lot of soak time left.
Change-Id: I53d75d5d214ccca887a89cf65b799fe640cc9bc5
2013-10-09 18:03:40 +00:00
|
|
|
private void setWatchdogAlarm(final long delay) {
|
2012-06-28 17:40:46 +00:00
|
|
|
// Lazily initialize the pending intent
|
|
|
|
if (mWatchdogPendingIntent == null) {
|
|
|
|
Intent intent = new Intent(mContext, Watchdog.class);
|
|
|
|
mWatchdogPendingIntent =
|
|
|
|
PendingIntent.getBroadcast(mContext, 0, intent, 0);
|
|
|
|
}
|
|
|
|
// Set the alarm
|
|
|
|
AlarmManager am = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
|
Add a retry backoff/limit policy to attachment download
b/11081672
Prior to this, any time the AttachmentDownloadService
got a CONNECTION_ERROR, it would just instantly retry,
without any limit on the number of tries. This is bad
if the server is in a funny state, we'll just keep spamming
it with multiple connection attempts per second. Also,
this kills the client device's battery and responsiveness.
Now, it will retry instantly five times, and then retry on a
10 second delay 5 more times. After that it will give up.
Even if it gives up, if the user visits an email with an
attachment, or taps on an attachment to expand it, we'll
start the process over. So we shouldn't have permanent
apparently data loss, even if we fail on the first 10 tries.
I'm not certain that this is the best backoff/limit policy,
maybe we should add a delay after even the first connection
error. But I'm hesitant to change this at this point, it's
possible that something is relying on this behavior and
we don't have a lot of soak time left.
Change-Id: I53d75d5d214ccca887a89cf65b799fe640cc9bc5
2013-10-09 18:03:40 +00:00
|
|
|
am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + delay,
|
2012-06-28 17:40:46 +00:00
|
|
|
mWatchdogPendingIntent);
|
|
|
|
}
|
|
|
|
|
Add a retry backoff/limit policy to attachment download
b/11081672
Prior to this, any time the AttachmentDownloadService
got a CONNECTION_ERROR, it would just instantly retry,
without any limit on the number of tries. This is bad
if the server is in a funny state, we'll just keep spamming
it with multiple connection attempts per second. Also,
this kills the client device's battery and responsiveness.
Now, it will retry instantly five times, and then retry on a
10 second delay 5 more times. After that it will give up.
Even if it gives up, if the user visits an email with an
attachment, or taps on an attachment to expand it, we'll
start the process over. So we shouldn't have permanent
apparently data loss, even if we fail on the first 10 tries.
I'm not certain that this is the best backoff/limit policy,
maybe we should add a delay after even the first connection
error. But I'm hesitant to change this at this point, it's
possible that something is relying on this behavior and
we don't have a lot of soak time left.
Change-Id: I53d75d5d214ccca887a89cf65b799fe640cc9bc5
2013-10-09 18:03:40 +00:00
|
|
|
private void setWatchdogAlarm() {
|
|
|
|
setWatchdogAlarm(WATCHDOG_CHECK_INTERVAL);
|
|
|
|
}
|
|
|
|
|
2010-11-29 21:21:11 +00:00
|
|
|
/**
|
|
|
|
* Do the work of starting an attachment download using the EmailService interface, and
|
|
|
|
* set our watchdog alarm
|
|
|
|
*
|
2013-07-15 21:44:54 +00:00
|
|
|
* @param service the service handling the download
|
2010-11-29 21:21:11 +00:00
|
|
|
* @param req the DownloadRequest
|
|
|
|
* @throws RemoteException
|
|
|
|
*/
|
2012-06-28 17:40:46 +00:00
|
|
|
private void startDownload(EmailServiceProxy service, DownloadRequest req)
|
2010-11-29 21:21:11 +00:00
|
|
|
throws RemoteException {
|
|
|
|
req.startTime = System.currentTimeMillis();
|
|
|
|
req.inProgress = true;
|
|
|
|
mDownloadsInProgress.put(req.attachmentId, req);
|
2014-02-26 16:57:53 +00:00
|
|
|
service.loadAttachment(mServiceCallback, req.accountId, req.attachmentId,
|
2013-07-15 21:44:54 +00:00
|
|
|
req.priority != PRIORITY_FOREGROUND);
|
2012-06-28 17:40:46 +00:00
|
|
|
setWatchdogAlarm();
|
2011-02-23 22:51:45 +00:00
|
|
|
}
|
2011-05-12 00:15:59 +00:00
|
|
|
|
2010-12-08 20:03:47 +00:00
|
|
|
private void cancelDownload(DownloadRequest req) {
|
Fix an infinitely retrying download problem
b/11436795
If an attachment download fails due to a timeout, or
an exception being thrown from startDownload(), we'd call
cancelDownload() on it. But this didn't actually cancel,
it would remove it from the inProgres list, but leave it
in the list of all downloads, so we'd immediately retry it.
This is bad for two reasons:
1. It can starve out other attachment downloads that could
have been successful.
2. It will keep attempting to do network work, even if it's
hopeless, forever, draining battery.
Now, if an attachment download fails in this way, for the first
few times, we'll move it to the tail end of the list of
downloads we'd like to perform. If it fails more than 10 times,
we'll give up completely. Giving up is not permanent, if we
have a reason to attempt a download again (such as the user
tapping on it), then it will get added back to the download
service and retried.
Change-Id: I5364a7d8b4b25ce299b8dcf061db6e9ce12daf75
2013-11-08 21:57:51 +00:00
|
|
|
LogUtils.d(TAG, "cancelDownload #%d", req.attachmentId);
|
2010-12-08 20:03:47 +00:00
|
|
|
req.inProgress = false;
|
Fix an infinitely retrying download problem
b/11436795
If an attachment download fails due to a timeout, or
an exception being thrown from startDownload(), we'd call
cancelDownload() on it. But this didn't actually cancel,
it would remove it from the inProgres list, but leave it
in the list of all downloads, so we'd immediately retry it.
This is bad for two reasons:
1. It can starve out other attachment downloads that could
have been successful.
2. It will keep attempting to do network work, even if it's
hopeless, forever, draining battery.
Now, if an attachment download fails in this way, for the first
few times, we'll move it to the tail end of the list of
downloads we'd like to perform. If it fails more than 10 times,
we'll give up completely. Giving up is not permanent, if we
have a reason to attempt a download again (such as the user
tapping on it), then it will get added back to the download
service and retried.
Change-Id: I5364a7d8b4b25ce299b8dcf061db6e9ce12daf75
2013-11-08 21:57:51 +00:00
|
|
|
mDownloadsInProgress.remove(req.attachmentId);
|
|
|
|
// Remove the download from our queue, and then decide whether or not to add it back.
|
|
|
|
remove(req);
|
|
|
|
req.retryCount++;
|
|
|
|
if (req.retryCount > CONNECTION_ERROR_MAX_RETRIES) {
|
|
|
|
LogUtils.d(TAG, "too many failures, giving up");
|
|
|
|
} else {
|
|
|
|
LogUtils.d(TAG, "moving to end of queue, will retry");
|
|
|
|
// The time field of DownloadRequest is final, because it's unsafe to change it
|
|
|
|
// as long as the DownloadRequest is in the DownloadSet. It's needed for the
|
|
|
|
// comparator, so changing time would make the request unfindable.
|
|
|
|
// Instead, we'll create a new DownloadRequest with an updated time.
|
|
|
|
// This will sort at the end of the set.
|
|
|
|
req = new DownloadRequest(req, SystemClock.elapsedRealtime());
|
|
|
|
add(req);
|
|
|
|
}
|
2010-12-08 20:03:47 +00:00
|
|
|
}
|
|
|
|
|
2010-08-10 00:48:53 +00:00
|
|
|
/**
|
|
|
|
* Called when a download is finished; we get notified of this via our EmailServiceCallback
|
|
|
|
* @param attachmentId the id of the attachment whose download is finished
|
|
|
|
* @param statusCode the EmailServiceStatus code returned by the Service
|
|
|
|
*/
|
2010-08-20 16:53:46 +00:00
|
|
|
/*package*/ synchronized void endDownload(long attachmentId, int statusCode) {
|
2010-08-10 00:48:53 +00:00
|
|
|
// Say we're no longer downloading this
|
|
|
|
mDownloadsInProgress.remove(attachmentId);
|
2011-01-21 19:58:39 +00:00
|
|
|
|
|
|
|
// TODO: This code is conservative and treats connection issues as failures.
|
|
|
|
// Since we have no mechanism to throttle reconnection attempts, it makes
|
|
|
|
// sense to be cautious here. Once logic is in place to prevent connecting
|
|
|
|
// in a tight loop, we can exclude counting connection issues as "failures".
|
|
|
|
|
|
|
|
// Update the attachment failure list if needed
|
|
|
|
Integer downloadCount;
|
|
|
|
downloadCount = mAttachmentFailureMap.remove(attachmentId);
|
|
|
|
if (statusCode != EmailServiceStatus.SUCCESS) {
|
|
|
|
if (downloadCount == null) {
|
|
|
|
downloadCount = 0;
|
|
|
|
}
|
|
|
|
downloadCount += 1;
|
|
|
|
mAttachmentFailureMap.put(attachmentId, downloadCount);
|
|
|
|
}
|
|
|
|
|
2010-08-20 16:53:46 +00:00
|
|
|
DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
|
2010-08-10 00:48:53 +00:00
|
|
|
if (statusCode == EmailServiceStatus.CONNECTION_ERROR) {
|
|
|
|
// If this needs to be retried, just process the queue again
|
|
|
|
if (req != null) {
|
Add a retry backoff/limit policy to attachment download
b/11081672
Prior to this, any time the AttachmentDownloadService
got a CONNECTION_ERROR, it would just instantly retry,
without any limit on the number of tries. This is bad
if the server is in a funny state, we'll just keep spamming
it with multiple connection attempts per second. Also,
this kills the client device's battery and responsiveness.
Now, it will retry instantly five times, and then retry on a
10 second delay 5 more times. After that it will give up.
Even if it gives up, if the user visits an email with an
attachment, or taps on an attachment to expand it, we'll
start the process over. So we shouldn't have permanent
apparently data loss, even if we fail on the first 10 tries.
I'm not certain that this is the best backoff/limit policy,
maybe we should add a delay after even the first connection
error. But I'm hesitant to change this at this point, it's
possible that something is relying on this behavior and
we don't have a lot of soak time left.
Change-Id: I53d75d5d214ccca887a89cf65b799fe640cc9bc5
2013-10-09 18:03:40 +00:00
|
|
|
req.retryCount++;
|
|
|
|
if (req.retryCount > CONNECTION_ERROR_MAX_RETRIES) {
|
|
|
|
LogUtils.d(TAG, "Connection Error #%d, giving up", attachmentId);
|
|
|
|
remove(req);
|
|
|
|
} else if (req.retryCount > CONNECTION_ERROR_DELAY_THRESHOLD) {
|
|
|
|
// TODO: I'm not sure this is a great retry/backoff policy, but we're
|
|
|
|
// afraid of changing behavior too much in case something relies upon it.
|
|
|
|
// So now, for the first five errors, we'll retry immediately. For the next
|
|
|
|
// five tries, we'll add a ten second delay between each. After that, we'll
|
|
|
|
// give up.
|
|
|
|
LogUtils.d(TAG, "ConnectionError #%d, retried %d times, adding delay",
|
|
|
|
attachmentId, req.retryCount);
|
|
|
|
req.inProgress = false;
|
|
|
|
req.retryStartTime = SystemClock.elapsedRealtime() +
|
|
|
|
CONNECTION_ERROR_RETRY_MILLIS;
|
|
|
|
setWatchdogAlarm(CONNECTION_ERROR_RETRY_MILLIS);
|
|
|
|
} else {
|
|
|
|
LogUtils.d(TAG, "ConnectionError #%d, retried %d times, adding delay",
|
|
|
|
attachmentId, req.retryCount);
|
|
|
|
req.inProgress = false;
|
|
|
|
req.retryStartTime = 0;
|
|
|
|
kick();
|
|
|
|
}
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
2010-10-29 00:20:46 +00:00
|
|
|
|
2011-01-11 19:45:36 +00:00
|
|
|
// If the request is still in the queue, remove it
|
|
|
|
if (req != null) {
|
|
|
|
remove(req);
|
|
|
|
}
|
2013-10-10 19:32:50 +00:00
|
|
|
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
2010-10-29 00:20:46 +00:00
|
|
|
long secs = 0;
|
|
|
|
if (req != null) {
|
|
|
|
secs = (System.currentTimeMillis() - req.time) / 1000;
|
|
|
|
}
|
2010-08-10 00:48:53 +00:00
|
|
|
String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" :
|
|
|
|
"Error " + statusCode;
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs
|
|
|
|
+ " seconds from request, status: " + status);
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
2010-10-29 00:20:46 +00:00
|
|
|
|
2010-08-10 00:48:53 +00:00
|
|
|
Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId);
|
|
|
|
if (attachment != null) {
|
2010-12-09 01:11:04 +00:00
|
|
|
long accountId = attachment.mAccountKey;
|
|
|
|
// Update our attachment storage for this account
|
|
|
|
Long currentStorage = mAttachmentStorageMap.get(accountId);
|
|
|
|
if (currentStorage == null) {
|
|
|
|
currentStorage = 0L;
|
|
|
|
}
|
|
|
|
mAttachmentStorageMap.put(accountId, currentStorage + attachment.mSize);
|
2010-08-10 00:48:53 +00:00
|
|
|
boolean deleted = false;
|
|
|
|
if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
|
|
|
|
if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) {
|
2010-08-20 16:53:46 +00:00
|
|
|
// If this is a forwarding download, and the attachment doesn't exist (or
|
2010-08-10 00:48:53 +00:00
|
|
|
// can't be downloaded) delete it from the outgoing message, lest that
|
|
|
|
// message never get sent
|
|
|
|
EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId);
|
|
|
|
// TODO: Talk to UX about whether this is even worth doing
|
2010-10-18 20:14:20 +00:00
|
|
|
NotificationController nc = NotificationController.getInstance(mContext);
|
|
|
|
nc.showDownloadForwardFailedNotification(attachment);
|
2010-08-10 00:48:53 +00:00
|
|
|
deleted = true;
|
|
|
|
}
|
|
|
|
// If we're an attachment on forwarded mail, and if we're not still blocked,
|
|
|
|
// try to send pending mail now (as mediated by MailService)
|
2011-01-14 19:47:26 +00:00
|
|
|
if ((req != null) &&
|
2010-10-29 00:20:46 +00:00
|
|
|
!Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) {
|
2013-10-10 19:32:50 +00:00
|
|
|
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, "== Downloads finished for outgoing msg #"
|
|
|
|
+ req.messageId);
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
2012-06-28 17:40:46 +00:00
|
|
|
EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
|
2013-07-30 02:11:41 +00:00
|
|
|
mContext, accountId);
|
2012-06-28 17:40:46 +00:00
|
|
|
try {
|
|
|
|
service.sendMail(accountId);
|
|
|
|
} catch (RemoteException e) {
|
|
|
|
// We tried
|
|
|
|
}
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
}
|
2011-01-20 17:47:57 +00:00
|
|
|
if (statusCode == EmailServiceStatus.MESSAGE_NOT_FOUND) {
|
2011-05-12 00:15:59 +00:00
|
|
|
Message msg = Message.restoreMessageWithId(mContext, attachment.mMessageKey);
|
|
|
|
if (msg == null) {
|
|
|
|
// If there's no associated message, delete the attachment
|
|
|
|
EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId);
|
|
|
|
} else {
|
|
|
|
// If there really is a message, retry
|
Add a retry backoff/limit policy to attachment download
b/11081672
Prior to this, any time the AttachmentDownloadService
got a CONNECTION_ERROR, it would just instantly retry,
without any limit on the number of tries. This is bad
if the server is in a funny state, we'll just keep spamming
it with multiple connection attempts per second. Also,
this kills the client device's battery and responsiveness.
Now, it will retry instantly five times, and then retry on a
10 second delay 5 more times. After that it will give up.
Even if it gives up, if the user visits an email with an
attachment, or taps on an attachment to expand it, we'll
start the process over. So we shouldn't have permanent
apparently data loss, even if we fail on the first 10 tries.
I'm not certain that this is the best backoff/limit policy,
maybe we should add a delay after even the first connection
error. But I'm hesitant to change this at this point, it's
possible that something is relying on this behavior and
we don't have a lot of soak time left.
Change-Id: I53d75d5d214ccca887a89cf65b799fe640cc9bc5
2013-10-09 18:03:40 +00:00
|
|
|
// TODO: How will this get retried? It's still marked as inProgress?
|
2011-05-12 00:15:59 +00:00
|
|
|
kick();
|
|
|
|
return;
|
|
|
|
}
|
2011-01-20 17:47:57 +00:00
|
|
|
} else if (!deleted) {
|
2010-08-10 00:48:53 +00:00
|
|
|
// Clear the download flags, since we're done for now. Note that this happens
|
|
|
|
// only for non-recoverable errors. When these occur for forwarded mail, we can
|
|
|
|
// ignore it and continue; otherwise, it was either 1) a user request, in which
|
|
|
|
// case the user can retry manually or 2) an opportunistic download, in which
|
|
|
|
// case the download wasn't critical
|
|
|
|
ContentValues cv = new ContentValues();
|
|
|
|
int flags =
|
|
|
|
Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
|
2014-04-11 21:42:28 +00:00
|
|
|
cv.put(AttachmentColumns.FLAGS, attachment.mFlags &= ~flags);
|
|
|
|
cv.put(AttachmentColumns.UI_STATE, AttachmentState.SAVED);
|
2010-08-10 00:48:53 +00:00
|
|
|
attachment.update(mContext, cv);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Process the queue
|
|
|
|
kick();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-08-20 16:53:46 +00:00
|
|
|
/**
|
|
|
|
* Calculate the download priority of an Attachment. A priority of zero means that the
|
|
|
|
* attachment is not marked for download.
|
|
|
|
* @param att the Attachment
|
|
|
|
* @return the priority key of the Attachment
|
|
|
|
*/
|
|
|
|
private static int getPriority(Attachment att) {
|
|
|
|
int priorityClass = PRIORITY_NONE;
|
|
|
|
int flags = att.mFlags;
|
|
|
|
if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
|
2011-01-21 19:58:39 +00:00
|
|
|
priorityClass = PRIORITY_SEND_MAIL;
|
2010-08-20 16:53:46 +00:00
|
|
|
} else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) {
|
2011-01-21 19:58:39 +00:00
|
|
|
priorityClass = PRIORITY_FOREGROUND;
|
2010-08-20 16:53:46 +00:00
|
|
|
}
|
|
|
|
return priorityClass;
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private void kick() {
|
|
|
|
synchronized(mLock) {
|
|
|
|
mLock.notify();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* We use an EmailServiceCallback to keep track of the progress of downloads. These callbacks
|
2010-08-27 18:31:37 +00:00
|
|
|
* come from either Controller (IMAP) or ExchangeService (EAS). Note that we only implement the
|
2010-08-10 00:48:53 +00:00
|
|
|
* single callback that's defined by the EmailServiceCallback interface.
|
|
|
|
*/
|
|
|
|
private class ServiceCallback extends IEmailServiceCallback.Stub {
|
2012-06-28 17:40:46 +00:00
|
|
|
@Override
|
2010-08-10 00:48:53 +00:00
|
|
|
public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
|
|
|
|
int progress) {
|
2010-10-22 23:35:46 +00:00
|
|
|
// Record status and progress
|
2010-12-09 01:11:04 +00:00
|
|
|
DownloadRequest req = mDownloadSet.getDownloadInProgress(attachmentId);
|
2010-10-22 23:35:46 +00:00
|
|
|
if (req != null) {
|
2013-10-10 19:32:50 +00:00
|
|
|
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
2010-12-09 01:11:04 +00:00
|
|
|
String code;
|
|
|
|
switch(statusCode) {
|
|
|
|
case EmailServiceStatus.SUCCESS: code = "Success"; break;
|
|
|
|
case EmailServiceStatus.IN_PROGRESS: code = "In progress"; break;
|
|
|
|
default: code = Integer.toString(statusCode); break;
|
|
|
|
}
|
|
|
|
if (statusCode != EmailServiceStatus.IN_PROGRESS) {
|
2013-11-06 23:39:04 +00:00
|
|
|
LogUtils.d(TAG, ">> Attachment status " + attachmentId + ": " + code);
|
2013-10-31 20:08:40 +00:00
|
|
|
} else if (progress >= (req.lastProgress + 10)) {
|
2013-11-06 23:39:04 +00:00
|
|
|
LogUtils.d(TAG, ">> Attachment progress %d: %d%%", attachmentId, progress);
|
2010-12-09 01:11:04 +00:00
|
|
|
}
|
|
|
|
}
|
2010-10-22 23:35:46 +00:00
|
|
|
req.lastStatusCode = statusCode;
|
|
|
|
req.lastProgress = progress;
|
|
|
|
req.lastCallbackTime = System.currentTimeMillis();
|
2012-06-28 17:40:46 +00:00
|
|
|
Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId);
|
|
|
|
if (attachment != null && statusCode == EmailServiceStatus.IN_PROGRESS) {
|
|
|
|
ContentValues values = new ContentValues();
|
|
|
|
values.put(AttachmentColumns.UI_DOWNLOADED_SIZE,
|
|
|
|
attachment.mSize * progress / 100);
|
|
|
|
// Update UIProvider with updated download size
|
|
|
|
// Individual services will set contentUri and state when finished
|
|
|
|
attachment.update(mContext, values);
|
|
|
|
}
|
2010-10-22 23:35:46 +00:00
|
|
|
}
|
2010-08-10 00:48:53 +00:00
|
|
|
switch (statusCode) {
|
|
|
|
case EmailServiceStatus.IN_PROGRESS:
|
|
|
|
break;
|
|
|
|
default:
|
2010-08-20 16:53:46 +00:00
|
|
|
mDownloadSet.endDownload(attachmentId, statusCode);
|
2010-08-10 00:48:53 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-02-13 02:56:09 +00:00
|
|
|
/*package*/ void addServiceIntentForTest(long accountId, Intent intent) {
|
|
|
|
mAccountServiceMap.put(accountId, intent);
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
|
2010-08-20 16:53:46 +00:00
|
|
|
/*package*/ void onChange(Attachment att) {
|
2011-01-26 17:03:11 +00:00
|
|
|
mDownloadSet.onChange(this, att);
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
|
2010-08-20 16:53:46 +00:00
|
|
|
/*package*/ boolean isQueued(long attachmentId) {
|
|
|
|
return mDownloadSet.findDownloadRequest(attachmentId) != null;
|
|
|
|
}
|
|
|
|
|
2010-10-23 19:14:00 +00:00
|
|
|
/*package*/ int getSize() {
|
|
|
|
return mDownloadSet.size();
|
|
|
|
}
|
|
|
|
|
2010-08-20 16:53:46 +00:00
|
|
|
/*package*/ boolean dequeue(long attachmentId) {
|
|
|
|
DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
|
|
|
|
if (req != null) {
|
2013-10-10 19:32:50 +00:00
|
|
|
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, "Dequeued attachmentId: " + attachmentId);
|
2010-10-29 00:20:46 +00:00
|
|
|
}
|
2010-08-20 16:53:46 +00:00
|
|
|
mDownloadSet.remove(req);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
|
2010-10-23 19:14:00 +00:00
|
|
|
/**
|
|
|
|
* Ask the service for the number of items in the download queue
|
|
|
|
* @return the number of items queued for download
|
|
|
|
*/
|
|
|
|
public static int getQueueSize() {
|
2011-01-26 17:03:11 +00:00
|
|
|
AttachmentDownloadService service = sRunningService;
|
|
|
|
if (service != null) {
|
|
|
|
return service.getSize();
|
2010-10-23 19:14:00 +00:00
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2010-08-10 00:48:53 +00:00
|
|
|
/**
|
|
|
|
* Ask the service whether a particular attachment is queued for download
|
|
|
|
* @param attachmentId the id of the Attachment (as stored by EmailProvider)
|
|
|
|
* @return whether or not the attachment is queued for download
|
|
|
|
*/
|
|
|
|
public static boolean isAttachmentQueued(long attachmentId) {
|
2011-01-26 17:03:11 +00:00
|
|
|
AttachmentDownloadService service = sRunningService;
|
|
|
|
if (service != null) {
|
|
|
|
return service.isQueued(attachmentId);
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ask the service to remove an attachment from the download queue
|
|
|
|
* @param attachmentId the id of the Attachment (as stored by EmailProvider)
|
|
|
|
* @return whether or not the attachment was removed from the queue
|
|
|
|
*/
|
|
|
|
public static boolean cancelQueuedAttachment(long attachmentId) {
|
2011-01-26 17:03:11 +00:00
|
|
|
AttachmentDownloadService service = sRunningService;
|
|
|
|
if (service != null) {
|
|
|
|
return service.dequeue(attachmentId);
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2010-11-29 21:21:11 +00:00
|
|
|
public static void watchdogAlarm() {
|
2011-01-26 17:03:11 +00:00
|
|
|
AttachmentDownloadService service = sRunningService;
|
|
|
|
if (service != null) {
|
|
|
|
service.mDownloadSet.onWatchdogAlarm();
|
2010-11-29 21:21:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-02-18 21:51:43 +00:00
|
|
|
// The queue entries here are entries of the form {id, flags}, with the values passed in to
|
|
|
|
// attachmentChanged()
|
|
|
|
private static final Queue<long[]> sAttachmentChangedQueue =
|
|
|
|
new ConcurrentLinkedQueue<long[]>();
|
|
|
|
private static AsyncTask<Void, Void, Void> sAttachmentChangedTask;
|
|
|
|
|
2010-08-10 00:48:53 +00:00
|
|
|
/**
|
|
|
|
* Called directly by EmailProvider whenever an attachment is inserted or changed
|
2011-07-15 16:46:28 +00:00
|
|
|
* @param context the caller's context
|
2010-08-10 00:48:53 +00:00
|
|
|
* @param id the attachment's id
|
|
|
|
* @param flags the new flags for the attachment
|
|
|
|
*/
|
2011-07-15 16:46:28 +00:00
|
|
|
public static void attachmentChanged(final Context context, final long id, final int flags) {
|
2014-02-18 21:51:43 +00:00
|
|
|
synchronized (sAttachmentChangedQueue) {
|
|
|
|
sAttachmentChangedQueue.add(new long[]{id, flags});
|
|
|
|
|
|
|
|
if (sAttachmentChangedTask == null) {
|
|
|
|
sAttachmentChangedTask = new AsyncTask<Void, Void, Void>() {
|
|
|
|
@Override
|
|
|
|
protected Void doInBackground(Void... params) {
|
|
|
|
while (true) {
|
|
|
|
final long[] change;
|
|
|
|
synchronized (sAttachmentChangedQueue) {
|
|
|
|
change = sAttachmentChangedQueue.poll();
|
|
|
|
if (change == null) {
|
|
|
|
sAttachmentChangedTask = null;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
final long id = change[0];
|
|
|
|
final long flags = change[1];
|
|
|
|
final Attachment attachment =
|
|
|
|
Attachment.restoreAttachmentWithId(context, id);
|
|
|
|
if (attachment == null) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
attachment.mFlags = (int) flags;
|
|
|
|
final Intent intent =
|
|
|
|
new Intent(context, AttachmentDownloadService.class);
|
|
|
|
intent.putExtra(EXTRA_ATTACHMENT, attachment);
|
|
|
|
context.startService(intent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
|
|
}
|
|
|
|
}
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
|
2010-12-09 01:11:04 +00:00
|
|
|
/**
|
|
|
|
* Determine whether an attachment can be prefetched for the given account
|
|
|
|
* @return true if download is allowed, false otherwise
|
|
|
|
*/
|
2011-02-13 02:56:09 +00:00
|
|
|
public boolean canPrefetchForAccount(Account account, File dir) {
|
2011-01-20 01:44:55 +00:00
|
|
|
// Check account, just in case
|
|
|
|
if (account == null) return false;
|
|
|
|
// First, check preference and quickly return if prefetch isn't allowed
|
|
|
|
if ((account.mFlags & Account.FLAGS_BACKGROUND_ATTACHMENTS) == 0) return false;
|
|
|
|
|
2010-12-09 01:11:04 +00:00
|
|
|
long totalStorage = dir.getTotalSpace();
|
|
|
|
long usableStorage = dir.getUsableSpace();
|
|
|
|
long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE);
|
|
|
|
|
|
|
|
// If there's not enough overall storage available, stop now
|
|
|
|
if (usableStorage < minAvailable) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts();
|
|
|
|
long perAccountMaxStorage =
|
|
|
|
(long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts);
|
|
|
|
|
|
|
|
// Retrieve our idea of currently used attachment storage; since we don't track deletions,
|
|
|
|
// this number is the "worst case". If the number is greater than what's allowed per
|
|
|
|
// account, we walk the directory to determine the actual number
|
2011-02-13 02:56:09 +00:00
|
|
|
Long accountStorage = mAttachmentStorageMap.get(account.mId);
|
2010-12-09 01:11:04 +00:00
|
|
|
if (accountStorage == null || (accountStorage > perAccountMaxStorage)) {
|
|
|
|
// Calculate the exact figure for attachment storage for this account
|
|
|
|
accountStorage = 0L;
|
|
|
|
File[] files = dir.listFiles();
|
|
|
|
if (files != null) {
|
|
|
|
for (File file : files) {
|
|
|
|
accountStorage += file.length();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Cache the value
|
2011-02-13 02:56:09 +00:00
|
|
|
mAttachmentStorageMap.put(account.mId, accountStorage);
|
2010-12-09 01:11:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Return true if we're using less than the maximum per account
|
|
|
|
if (accountStorage < perAccountMaxStorage) {
|
|
|
|
return true;
|
|
|
|
} else {
|
2013-10-10 19:32:50 +00:00
|
|
|
if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, ">> Prefetch not allowed for account " + account.mId + "; used " +
|
2010-12-09 01:11:04 +00:00
|
|
|
accountStorage + ", limit " + perAccountMaxStorage);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-06-28 17:40:46 +00:00
|
|
|
@Override
|
2010-08-10 00:48:53 +00:00
|
|
|
public void run() {
|
2011-01-26 17:03:11 +00:00
|
|
|
// These fields are only used within the service thread
|
2010-08-10 00:48:53 +00:00
|
|
|
mContext = this;
|
2011-01-26 17:03:11 +00:00
|
|
|
mConnectivityManager = new EmailConnectivityManager(this, TAG);
|
2010-12-09 01:11:04 +00:00
|
|
|
mAccountManagerStub = new AccountManagerStub(this);
|
2010-12-28 20:21:40 +00:00
|
|
|
|
2010-08-10 00:48:53 +00:00
|
|
|
// Run through all attachments in the database that require download and add them to
|
|
|
|
// the queue
|
|
|
|
int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
|
|
|
|
Cursor c = getContentResolver().query(Attachment.CONTENT_URI,
|
2014-04-11 21:42:28 +00:00
|
|
|
EmailContent.ID_PROJECTION, "(" + AttachmentColumns.FLAGS + " & ?) != 0",
|
2010-08-10 00:48:53 +00:00
|
|
|
new String[] {Integer.toString(mask)}, null);
|
|
|
|
try {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, "Count: " + c.getCount());
|
2010-08-10 00:48:53 +00:00
|
|
|
while (c.moveToNext()) {
|
|
|
|
Attachment attachment = Attachment.restoreAttachmentWithId(
|
|
|
|
this, c.getLong(EmailContent.ID_PROJECTION_COLUMN));
|
|
|
|
if (attachment != null) {
|
2011-01-26 17:03:11 +00:00
|
|
|
mDownloadSet.onChange(this, attachment);
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
|
|
e.printStackTrace();
|
|
|
|
}
|
|
|
|
finally {
|
|
|
|
c.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Loop until stopped, with a 30 minute wait loop
|
|
|
|
while (!mStop) {
|
|
|
|
// Here's where we run our attachment loading logic...
|
2013-10-14 19:25:04 +00:00
|
|
|
// Make a local copy of the variable so we don't null-crash on service shutdown
|
|
|
|
final EmailConnectivityManager ecm = mConnectivityManager;
|
|
|
|
if (ecm != null) {
|
|
|
|
ecm.waitForConnectivity();
|
|
|
|
}
|
|
|
|
if (mStop) {
|
|
|
|
// We might be bailing out here due to the service shutting down
|
|
|
|
break;
|
|
|
|
}
|
2010-08-20 16:53:46 +00:00
|
|
|
mDownloadSet.processQueue();
|
2011-07-15 16:46:28 +00:00
|
|
|
if (mDownloadSet.isEmpty()) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(TAG, "*** All done; shutting down service");
|
2011-07-15 16:46:28 +00:00
|
|
|
stopSelf();
|
|
|
|
break;
|
|
|
|
}
|
2010-08-10 00:48:53 +00:00
|
|
|
synchronized(mLock) {
|
|
|
|
try {
|
|
|
|
mLock.wait(PROCESS_QUEUE_WAIT_TIME);
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
// That's ok; we'll just keep looping
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2011-01-26 17:03:11 +00:00
|
|
|
|
|
|
|
// Unregister now that we're done
|
2013-10-14 19:25:04 +00:00
|
|
|
// Make a local copy of the variable so we don't null-crash on service shutdown
|
|
|
|
final EmailConnectivityManager ecm = mConnectivityManager;
|
|
|
|
if (ecm != null) {
|
|
|
|
ecm.unregister();
|
2011-07-15 16:46:28 +00:00
|
|
|
}
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
2011-07-15 16:46:28 +00:00
|
|
|
if (sRunningService == null) {
|
|
|
|
sRunningService = this;
|
|
|
|
}
|
2011-07-18 16:49:52 +00:00
|
|
|
if (intent != null && intent.hasExtra(EXTRA_ATTACHMENT)) {
|
2014-04-11 21:42:28 +00:00
|
|
|
Attachment att = intent.getParcelableExtra(EXTRA_ATTACHMENT);
|
2011-07-15 16:46:28 +00:00
|
|
|
onChange(att);
|
|
|
|
}
|
2010-08-10 00:48:53 +00:00
|
|
|
return Service.START_STICKY;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onCreate() {
|
|
|
|
// Start up our service thread
|
|
|
|
new Thread(this, "AttachmentDownloadService").start();
|
|
|
|
}
|
|
|
|
@Override
|
|
|
|
public IBinder onBind(Intent intent) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onDestroy() {
|
2011-07-26 16:57:24 +00:00
|
|
|
// Mark this instance of the service as stopped
|
|
|
|
mStop = true;
|
2010-08-10 00:48:53 +00:00
|
|
|
if (sRunningService != null) {
|
|
|
|
kick();
|
2011-07-15 16:46:28 +00:00
|
|
|
sRunningService = null;
|
|
|
|
}
|
|
|
|
if (mConnectivityManager != null) {
|
|
|
|
mConnectivityManager.unregister();
|
2013-10-14 19:25:04 +00:00
|
|
|
mConnectivityManager.stopWait();
|
2011-07-15 16:46:28 +00:00
|
|
|
mConnectivityManager = null;
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|
|
|
|
}
|
2010-10-22 23:35:46 +00:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
|
|
|
|
pw.println("AttachmentDownloadService");
|
|
|
|
long time = System.currentTimeMillis();
|
|
|
|
synchronized(mDownloadSet) {
|
|
|
|
pw.println(" Queue, " + mDownloadSet.size() + " entries");
|
|
|
|
Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator();
|
|
|
|
// First, start up any required downloads, in priority order
|
|
|
|
while (iterator.hasNext()) {
|
|
|
|
DownloadRequest req = iterator.next();
|
|
|
|
pw.println(" Account: " + req.accountId + ", Attachment: " + req.attachmentId);
|
|
|
|
pw.println(" Priority: " + req.priority + ", Time: " + req.time +
|
|
|
|
(req.inProgress ? " [In progress]" : ""));
|
2011-01-26 17:03:11 +00:00
|
|
|
Attachment att = Attachment.restoreAttachmentWithId(this, req.attachmentId);
|
2010-10-22 23:35:46 +00:00
|
|
|
if (att == null) {
|
|
|
|
pw.println(" Attachment not in database?");
|
2010-10-26 21:53:18 +00:00
|
|
|
} else if (att.mFileName != null) {
|
2010-10-22 23:35:46 +00:00
|
|
|
String fileName = att.mFileName;
|
|
|
|
String suffix = "[none]";
|
|
|
|
int lastDot = fileName.lastIndexOf('.');
|
|
|
|
if (lastDot >= 0) {
|
|
|
|
suffix = fileName.substring(lastDot);
|
|
|
|
}
|
|
|
|
pw.print(" Suffix: " + suffix);
|
2012-09-08 17:36:32 +00:00
|
|
|
if (att.getContentUri() != null) {
|
|
|
|
pw.print(" ContentUri: " + att.getContentUri());
|
2010-10-22 23:35:46 +00:00
|
|
|
}
|
|
|
|
pw.print(" Mime: ");
|
|
|
|
if (att.mMimeType != null) {
|
|
|
|
pw.print(att.mMimeType);
|
|
|
|
} else {
|
2011-02-09 01:50:30 +00:00
|
|
|
pw.print(AttachmentUtilities.inferMimeType(fileName, null));
|
2010-10-26 21:53:18 +00:00
|
|
|
pw.print(" [inferred]");
|
2010-10-22 23:35:46 +00:00
|
|
|
}
|
|
|
|
pw.println(" Size: " + att.mSize);
|
|
|
|
}
|
|
|
|
if (req.inProgress) {
|
|
|
|
pw.println(" Status: " + req.lastStatusCode + ", Progress: " +
|
|
|
|
req.lastProgress);
|
|
|
|
pw.println(" Started: " + req.startTime + ", Callback: " +
|
|
|
|
req.lastCallbackTime);
|
|
|
|
pw.println(" Elapsed: " + ((time - req.startTime) / 1000L) + "s");
|
|
|
|
if (req.lastCallbackTime > 0) {
|
|
|
|
pw.println(" CB: " + ((time - req.lastCallbackTime) / 1000L) + "s");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2010-08-10 00:48:53 +00:00
|
|
|
}
|