Download service refactoring: multiple downloads and cancellation support

This commit is contained in:
David A. Velasco 2012-10-09 14:53:25 +02:00
parent 7edbe05bb5
commit 68ce2e7a38
16 changed files with 712 additions and 173 deletions

View file

@ -1,14 +1,18 @@
package com.owncloud.android.files.services;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.AbstractList;
import java.util.Iterator;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
import eu.alefzero.webdav.OnDatatransferProgressListener;
import com.owncloud.android.network.OwnCloudClientUtils;
import com.owncloud.android.operations.DownloadFileOperation;
import com.owncloud.android.operations.RemoteOperationResult;
import android.accounts.Account;
import android.app.Notification;
@ -18,6 +22,7 @@ import android.app.Service;
import android.content.ContentValues;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
@ -41,28 +46,40 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
private static final String TAG = "FileDownloader";
private NotificationManager mNotificationMngr;
private Looper mServiceLooper;
private ServiceHandler mServiceHandler;
private IBinder mBinder;
private WebdavClient mDownloadClient = null;
private Account mLastAccount = null;
//private AbstractList<Account> mAccounts = new Vector<Account>();
private ConcurrentMap<String, DownloadFileOperation> mPendingDownloads = new ConcurrentHashMap<String, DownloadFileOperation>();
private DownloadFileOperation mCurrentDownload = null;
/*
private Account mAccount;
private String mFilePath;
private String mRemotePath;
private int mLastPercent;
private long mTotalDownloadSize;
private long mCurrentDownloadSize;
*/
private NotificationManager mNotificationMngr;
private Notification mNotification;
private int mLastPercent;
/**
* Static map with the files being download and the path to the temporal file were are download
*/
private static Map<String, String> mDownloadsInProgress = Collections.synchronizedMap(new HashMap<String, String>());
//private static Set<String> mDownloadsInProgress = Collections.synchronizedSet(new HashSet<String>());
/**
* Returns True when the file referred by 'remotePath' in the ownCloud account 'account' is downloading
*/
public static boolean isDownloading(Account account, String remotePath) {
return (mDownloadsInProgress.get(buildRemoteName(account.name, remotePath)) != null);
}
/*public static boolean isDownloading(Account account, String remotePath) {
return (mDownloadsInProgress.contains(buildRemoteName(account.name, remotePath)));
}*/
/**
* Builds a key for mDownloadsInProgress from the accountName and remotePath
@ -71,19 +88,6 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
return accountName + remotePath;
}
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
downloadFile();
stopSelf(msg.arg1);
}
}
public static final String getSavePath(String accountName) {
File sdCard = Environment.getExternalStorageDirectory();
return sdCard.getAbsolutePath() + "/owncloud/" + Uri.encode(accountName, "@");
@ -96,6 +100,10 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
// URL encoding is an 'easy fix' to overcome that NTFS and FAT32 don't allow ":" in file names, that can be in the accountName since 0.1.190B
}
/**
* Service initialization
*/
@Override
public void onCreate() {
super.onCreate();
@ -105,13 +113,16 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
mBinder = new FileDownloaderBinder();
}
@Override
public IBinder onBind(Intent arg0) {
return null;
}
/**
* Entry point to add one or several files to the queue of downloads.
*
* New downloads are added calling to startService(), resulting in a call to this method. This ensures the service will keep on working
* although the caller activity goes away.
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if ( !intent.hasExtra(EXTRA_ACCOUNT) ||
@ -121,116 +132,241 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
Log.e(TAG, "Not enough information provided in intent");
return START_NOT_STICKY;
}
mAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
mFilePath = intent.getStringExtra(EXTRA_FILE_PATH);
mRemotePath = intent.getStringExtra(EXTRA_REMOTE_PATH);
mTotalDownloadSize = intent.getLongExtra(EXTRA_FILE_SIZE, -1);
mCurrentDownloadSize = mLastPercent = 0;
Account account = intent.getParcelableExtra(EXTRA_ACCOUNT);
String filePath = intent.getStringExtra(EXTRA_FILE_PATH);
String remotePath = intent.getStringExtra(EXTRA_REMOTE_PATH);
long totalDownloadSize = intent.getLongExtra(EXTRA_FILE_SIZE, -1);
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
mServiceHandler.sendMessage(msg);
AbstractList<String> requestedDownloads = new Vector<String>(); // dvelasco: now this will always contain just one element, but that can change in a near future
String downloadKey = buildRemoteName(account.name, remotePath);
try {
DownloadFileOperation newDownload = new DownloadFileOperation(account, filePath, remotePath, (String)null, totalDownloadSize, false);
mPendingDownloads.putIfAbsent(downloadKey, newDownload);
newDownload.addDatatransferProgressListener(this);
requestedDownloads.add(downloadKey);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Not enough information provided in intent: " + e.getMessage());
return START_NOT_STICKY;
}
if (requestedDownloads.size() > 0) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = requestedDownloads;
mServiceHandler.sendMessage(msg);
}
return START_NOT_STICKY;
}
/**
* Core download method: requests the file to download and stores it.
* Provides a binder object that clients can use to perform operations on the queue of downloads, excepting the addition of new files.
*
* Implemented to perform cancellation, pause and resume of existing downloads.
*/
private void downloadFile() {
boolean downloadResult = false;
/// prepare client object to send the request to the ownCloud server
WebdavClient wdc = OwnCloudClientUtils.createOwnCloudClient(mAccount, getApplicationContext());
wdc.setDataTransferProgressListener(this);
/// download will be in a temporal file
File tmpFile = new File(getTemporalPath(mAccount.name) + mFilePath);
/// create status notification to show the download progress
mNotification = new Notification(R.drawable.icon, getString(R.string.downloader_download_in_progress_ticker), System.currentTimeMillis());
mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
mNotification.contentView = new RemoteViews(getApplicationContext().getPackageName(), R.layout.progressbar_layout);
mNotification.contentView.setProgressBar(R.id.status_progress, 100, 0, mTotalDownloadSize == -1);
mNotification.contentView.setTextViewText(R.id.status_text, String.format(getString(R.string.downloader_download_in_progress_content), 0, tmpFile.getName()));
mNotification.contentView.setImageViewResource(R.id.status_icon, R.drawable.icon);
// TODO put something smart in the contentIntent below
mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
mNotificationMngr.notify(R.string.downloader_download_in_progress_ticker, mNotification);
@Override
public IBinder onBind(Intent arg0) {
return mBinder;
}
/// perform the download
tmpFile.getParentFile().mkdirs();
mDownloadsInProgress.put(buildRemoteName(mAccount.name, mRemotePath), tmpFile.getAbsolutePath());
File newFile = null;
try {
if (wdc.downloadFile(mRemotePath, tmpFile)) {
newFile = new File(getSavePath(mAccount.name) + mFilePath);
newFile.getParentFile().mkdirs();
boolean moved = tmpFile.renameTo(newFile);
/**
* Binder to let client components to perform operations on the queue of downloads.
*
* It provides by itself the available operations.
*/
public class FileDownloaderBinder extends Binder {
if (moved) {
/**
* Cancels a pending or current download of a remote file.
*
* @param account Owncloud account where the remote file is stored.
* @param remotePath URL to the remote file in the queue of downloads.
*/
public void cancel(Account account, String remotePath) {
synchronized (mPendingDownloads) {
DownloadFileOperation download = mPendingDownloads.remove(buildRemoteName(account.name, remotePath));
if (download != null) {
download.cancel();
}
}
}
/**
* Returns True when the file referred by 'remotePath' in the ownCloud account 'account' is downloading
*
* @param account Owncloud account where the remote file is stored.
* @param remotePath URL to the remote file in the queue of downloads.
*/
public boolean isDownloading(Account account, String remotePath) {
synchronized (mPendingDownloads) {
return (mPendingDownloads.containsKey(buildRemoteName(account.name, remotePath)));
}
}
}
/**
* Download worker. Performs the pending downloads in the order they were requested.
*
* Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.
*/
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
@SuppressWarnings("unchecked")
AbstractList<String> requestedDownloads = (AbstractList<String>) msg.obj;
if (msg.obj != null) {
Iterator<String> it = requestedDownloads.iterator();
while (it.hasNext()) {
downloadFile(it.next());
}
}
stopSelf(msg.arg1);
}
}
/**
* Core download method: requests a file to download and stores it.
*
* @param downloadKey Key to access the download to perform, contained in mPendingDownloads
*/
private void downloadFile(String downloadKey) {
synchronized(mPendingDownloads) {
mCurrentDownload = mPendingDownloads.get(downloadKey);
}
if (mCurrentDownload != null) {
notifyDownloadStart(mCurrentDownload);
/// prepare client object to send the request to the ownCloud server
if (mDownloadClient == null || mLastAccount != mCurrentDownload.getAccount()) {
mLastAccount = mCurrentDownload.getAccount();
mDownloadClient = OwnCloudClientUtils.createOwnCloudClient(mLastAccount, getApplicationContext());
}
/// perform the download
//mDownloadsInProgress.add(buildRemoteName(mLastAccount.name, mCurrentDownload.getRemotePath()));
RemoteOperationResult downloadResult = null;
File newLocalFile = null;
//try {
downloadResult = mCurrentDownload.execute(mDownloadClient);
if (downloadResult.isSuccess()) {
ContentValues cv = new ContentValues();
cv.put(ProviderTableMeta.FILE_STORAGE_PATH, newFile.getAbsolutePath());
newLocalFile = new File(getSavePath(mCurrentDownload.getAccount().name) + mCurrentDownload.getLocalPath());
cv.put(ProviderTableMeta.FILE_STORAGE_PATH, newLocalFile.getAbsolutePath());
getContentResolver().update(
ProviderTableMeta.CONTENT_URI,
cv,
ProviderTableMeta.FILE_NAME + "=? AND "
+ ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?",
new String[] {
mFilePath.substring(mFilePath.lastIndexOf('/') + 1),
mAccount.name });
downloadResult = true;
new String[] {
mCurrentDownload.getLocalPath().substring(mCurrentDownload.getLocalPath().lastIndexOf('/') + 1),
mLastAccount.name });
}
}
} finally {
mDownloadsInProgress.remove(buildRemoteName(mAccount.name, mRemotePath));
/*} finally {
mDownloadsInProgress.remove(buildRemoteName(mLastAccount.name, mCurrentDownload.getRemotePath()));
}*/
mPendingDownloads.remove(downloadKey);
/// notify result
notifyDownloadResult(mCurrentDownload, downloadResult);
sendFinalBroadcast(mCurrentDownload, downloadResult, (downloadResult.isSuccess())? newLocalFile.getAbsolutePath():null);
}
/// notify result
mNotificationMngr.cancel(R.string.downloader_download_in_progress_ticker);
int tickerId = (downloadResult) ? R.string.downloader_download_succeeded_ticker : R.string.downloader_download_failed_ticker;
int contentId = (downloadResult) ? R.string.downloader_download_succeeded_content : R.string.downloader_download_failed_content;
Notification finalNotification = new Notification(R.drawable.icon, getString(tickerId), System.currentTimeMillis());
finalNotification.flags |= Notification.FLAG_AUTO_CANCEL;
// TODO put something smart in the contentIntent below
finalNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
finalNotification.setLatestEventInfo(getApplicationContext(), getString(tickerId), String.format(getString(contentId), tmpFile.getName()), finalNotification.contentIntent);
mNotificationMngr.notify(tickerId, finalNotification);
sendFinalBroadcast(downloadResult, (downloadResult)?newFile.getAbsolutePath():null);
}
/**
* Callback method to update the progress bar in the status notification.
*/
@Override
public void transferProgress(long progressRate) {
mCurrentDownloadSize += progressRate;
int percent = (int)(100.0*((double)mCurrentDownloadSize)/((double)mTotalDownloadSize));
public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileName) {
int percent = (int)(100.0*((double)totalTransferredSoFar)/((double)totalToTransfer));
if (percent != mLastPercent) {
mNotification.contentView.setProgressBar(R.id.status_progress, 100, (int)(100*mCurrentDownloadSize/mTotalDownloadSize), mTotalDownloadSize == -1);
mNotification.contentView.setTextViewText(R.id.status_text, String.format(getString(R.string.downloader_download_in_progress_content), percent, new File(mFilePath).getName()));
mNotification.contentView.setProgressBar(R.id.status_progress, 100, percent, totalToTransfer == -1);
mNotification.contentView.setTextViewText(R.id.status_text, String.format(getString(R.string.downloader_download_in_progress_content), percent, fileName));
mNotificationMngr.notify(R.string.downloader_download_in_progress_ticker, mNotification);
}
mLastPercent = percent;
}
/**
* Callback method to update the progress bar in the status notification (old version)
*/
@Override
public void onTransferProgress(long progressRate) {
// NOTHING TO DO HERE ANYMORE
}
/**
* Creates a status notification to show the download progress
*
* @param download Download operation starting.
*/
private void notifyDownloadStart(DownloadFileOperation download) {
/// create status notification to show the download progress
mLastPercent = 0;
mNotification = new Notification(R.drawable.icon, getString(R.string.downloader_download_in_progress_ticker), System.currentTimeMillis());
mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
mNotification.contentView = new RemoteViews(getApplicationContext().getPackageName(), R.layout.progressbar_layout);
mNotification.contentView.setProgressBar(R.id.status_progress, 100, 0, download.getSize() == -1);
mNotification.contentView.setTextViewText(R.id.status_text, String.format(getString(R.string.downloader_download_in_progress_content), 0, new File(download.getLocalPath()).getName()));
mNotification.contentView.setImageViewResource(R.id.status_icon, R.drawable.icon);
// TODO put something smart in the contentIntent below
mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
mNotificationMngr.notify(R.string.downloader_download_in_progress_ticker, mNotification);
}
/**
* Updates the status notification with the result of a download operation.
*
* @param downloadResult Result of the download operation.
* @param download Finished download operation
*/
private void notifyDownloadResult(DownloadFileOperation download, RemoteOperationResult downloadResult) {
mNotificationMngr.cancel(R.string.downloader_download_in_progress_ticker);
if (!downloadResult.isCancelled()) {
int tickerId = (downloadResult.isSuccess()) ? R.string.downloader_download_succeeded_ticker : R.string.downloader_download_failed_ticker;
int contentId = (downloadResult.isSuccess()) ? R.string.downloader_download_succeeded_content : R.string.downloader_download_failed_content;
Notification finalNotification = new Notification(R.drawable.icon, getString(tickerId), System.currentTimeMillis());
finalNotification.flags |= Notification.FLAG_AUTO_CANCEL;
// TODO put something smart in the contentIntent below
finalNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
finalNotification.setLatestEventInfo(getApplicationContext(), getString(tickerId), String.format(getString(contentId), new File(download.getLocalPath()).getName()), finalNotification.contentIntent);
mNotificationMngr.notify(tickerId, finalNotification);
}
}
/**
* Sends a broadcast in order to the interested activities can update their view
*
* @param downloadResult 'True' if the download was successful
* @param newFilePath Absolute path to the download file
* @param download Finished download operation
* @param downloadResult Result of the download operation
* @param newFilePath Absolute path to the downloaded file
*/
private void sendFinalBroadcast(boolean downloadResult, String newFilePath) {
private void sendFinalBroadcast(DownloadFileOperation download, RemoteOperationResult downloadResult, String newFilePath) {
Intent end = new Intent(DOWNLOAD_FINISH_MESSAGE);
end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult);
end.putExtra(ACCOUNT_NAME, mAccount.name);
end.putExtra(EXTRA_REMOTE_PATH, mRemotePath);
if (downloadResult) {
end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess());
end.putExtra(ACCOUNT_NAME, download.getAccount().name);
end.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());
if (downloadResult.isSuccess()) {
end.putExtra(EXTRA_FILE_PATH, newFilePath);
}
sendBroadcast(end);

View file

@ -62,17 +62,20 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
private static final String TAG = FileUploader.class.getSimpleName();
private NotificationManager mNotificationManager;
private Looper mServiceLooper;
private ServiceHandler mServiceHandler;
private AbstractList<Account> mAccounts = new Vector<Account>();
private AbstractList<UploadFileOperation> mUploads = new Vector<UploadFileOperation>();
private int mCurrentIndexUpload;
private NotificationManager mNotificationManager;
private Notification mNotification;
private long mTotalDataToSend, mSendData;
private int mTotalFilesToSend;
private int mCurrentIndexUpload, mPreviousPercent;
private int mSuccessCounter;
private RemoteViews mDefaultNotificationContentView;
private long mTotalDataToSend, mSendData;
private int mTotalFilesToSend, mPreviousPercent;
private int mSuccessCounter;
/**
* Static map with the files being download and the path to the temporal file were are download
@ -107,23 +110,9 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
@Override
public IBinder onBind(Intent arg0) {
return null;
}
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
uploadFile();
stopSelf(msg.arg1);
}
}
/**
* Service initialization
*/
@Override
public void onCreate() {
super.onCreate();
@ -135,6 +124,13 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
mServiceHandler = new ServiceHandler(mServiceLooper);
}
/**
* Entry point to add one or several files to the queue of uploads.
*
* New uploads are added calling to startService(), resulting in a call to this method. This ensures the service will keep on working
* although the caller activity goes away.
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (!intent.hasExtra(KEY_ACCOUNT) && !intent.hasExtra(KEY_UPLOAD_TYPE)) {
@ -191,6 +187,37 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
}
/**
* Provides a binder object that clients can use to perform operations on the queue of uploads, excepting the addition of new files.
*
* Implemented to perform cancellation, pause and resume of existing uploads.
*/
@Override
public IBinder onBind(Intent arg0) {
return null;
}
/**
* Upload worker. Performs the pending uploads in the order they were requested.
*
* Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.
*/
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
uploadFile();
stopSelf(msg.arg1);
}
}
/**
* Core upload method: sends the file(s) to upload
*/
@ -381,7 +408,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
* Callback method to update the progress bar in the status notification.
*/
@Override
public void transferProgress(long progressRate) {
public void onTransferProgress(long progressRate) {
mSendData += progressRate;
int percent = (int)(100*((double)mSendData)/((double)mTotalDataToSend));
if (percent != mPreviousPercent) {
@ -392,4 +419,11 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
}
mPreviousPercent = percent;
}
@Override
public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileName) {
// TODO Maybe replace the other transferProgress with this
}
}

View file

@ -0,0 +1,213 @@
/* ownCloud Android client application
* Copyright (C) 2012 Bartek Przybylski
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.owncloud.android.operations;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.http.HttpStatus;
import com.owncloud.android.files.services.FileDownloader;
import com.owncloud.android.operations.RemoteOperation;
import com.owncloud.android.operations.RemoteOperationResult;
import eu.alefzero.webdav.OnDatatransferProgressListener;
import eu.alefzero.webdav.WebdavClient;
import eu.alefzero.webdav.WebdavUtils;
import android.accounts.Account;
import android.util.Log;
import android.webkit.MimeTypeMap;
/**
* Remote operation performing the download of a file to an ownCloud server
*
* @author David A. Velasco
*/
public class DownloadFileOperation extends RemoteOperation {
private static final String TAG = DownloadFileOperation.class.getCanonicalName();
private Account mAccount = null;
private String mLocalPath = null;
private String mRemotePath = null;
private String mMimeType = null;
private long mSize = -1;
private Boolean mCancellationRequested = false;
private Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<OnDatatransferProgressListener>();
public Account getAccount() {
return mAccount;
}
public String getLocalPath() {
return mLocalPath;
}
public String getRemotePath() {
return mRemotePath;
}
public String getMimeType() {
return mMimeType;
}
public long getSize() {
return mSize;
}
public DownloadFileOperation( Account account,
String localPath,
String remotePath,
String mimeType,
long size,
boolean forceOverwrite) {
if (account == null)
throw new IllegalArgumentException("Illegal null account in DownloadFileOperation creation");
if (localPath == null)
throw new IllegalArgumentException("Illegal null local path in DownloadFileOperation creation");
if (remotePath == null)
throw new IllegalArgumentException("Illegal null remote path in DownloadFileOperation creation");
mAccount = account;
mLocalPath = localPath;
mRemotePath = remotePath;
mMimeType = mimeType;
if (mMimeType == null) {
try {
mMimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(
localPath.substring(localPath.lastIndexOf('.') + 1));
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Trying to find out MIME type of a file without extension: " + localPath);
}
}
if (mMimeType == null) {
mMimeType = "application/octet-stream";
}
mSize = size;
}
public void addDatatransferProgressListener (OnDatatransferProgressListener listener) {
mDataTransferListeners.add(listener);
}
@Override
protected RemoteOperationResult run(WebdavClient client) {
RemoteOperationResult result = null;
File newFile = null;
boolean moved = false;
/// download will be in a temporal file
File tmpFile = new File(FileDownloader.getTemporalPath(mAccount.name) + mLocalPath);
/// perform the download
try {
tmpFile.getParentFile().mkdirs();
int status = downloadFile(client, tmpFile);
if (isSuccess(status)) {
newFile = new File(FileDownloader.getSavePath(mAccount.name) + mLocalPath);
newFile.getParentFile().mkdirs();
moved = tmpFile.renameTo(newFile);
}
if (!moved)
result = new RemoteOperationResult(RemoteOperationResult.ResultCode.STORAGE_ERROR_MOVING_FROM_TMP);
else
result = new RemoteOperationResult(isSuccess(status), status);
Log.i(TAG, "Download of " + mLocalPath + " to " + mRemotePath + ": " + result.getLogMessage());
} catch (Exception e) {
result = new RemoteOperationResult(e);
Log.e(TAG, "Download of " + mRemotePath + " to " + mLocalPath + ": " + result.getLogMessage(), e);
}
return result;
}
public boolean isSuccess(int status) {
return (status == HttpStatus.SC_OK);
}
protected int downloadFile(WebdavClient client, File targetFile) throws HttpException, IOException, OperationCancelledException {
int status = -1;
boolean savedFile = false;
GetMethod get = new GetMethod(client.getBaseUri() + WebdavUtils.encodePath(mRemotePath));
Iterator<OnDatatransferProgressListener> it = null;
try {
status = client.executeMethod(get);
if (isSuccess(status)) {
targetFile.createNewFile();
BufferedInputStream bis = new BufferedInputStream(get.getResponseBodyAsStream());
FileOutputStream fos = new FileOutputStream(targetFile);
long transferred = 0;
byte[] bytes = new byte[4096];
int readResult = 0;
while ((readResult = bis.read(bytes)) != -1) {
synchronized(mCancellationRequested) {
if (mCancellationRequested) {
throw new OperationCancelledException();
}
}
fos.write(bytes, 0, readResult);
transferred += readResult;
it = mDataTransferListeners.iterator();
while (it.hasNext()) {
it.next().onTransferProgress(readResult, transferred, mSize, targetFile.getName());
}
}
fos.close();
savedFile = true;
} else {
client.exhaustResponse(get.getResponseBodyAsStream());
}
} finally {
if (!savedFile && targetFile.exists()) {
targetFile.delete();
}
get.releaseConnection(); // let the connection available for other methods
}
return status;
}
public void cancel() {
synchronized(mCancellationRequested) {
mCancellationRequested = true;
}
}
}

View file

@ -0,0 +1,5 @@
package com.owncloud.android.operations;
public class OperationCancelledException extends Exception {
}

View file

@ -57,7 +57,9 @@ public class RemoteOperationResult {
NO_NETWORK_CONNECTION,
SSL_ERROR,
SSL_RECOVERABLE_PEER_UNVERIFIED,
BAD_OC_VERSION
BAD_OC_VERSION,
STORAGE_ERROR_MOVING_FROM_TMP,
CANCELLED
}
private boolean mSuccess = false;
@ -94,7 +96,10 @@ public class RemoteOperationResult {
public RemoteOperationResult(Exception e) {
mException = e;
if (e instanceof SocketException) {
if (e instanceof OperationCancelledException) {
mCode = ResultCode.CANCELLED;
} else if (e instanceof SocketException) {
mCode = ResultCode.WRONG_CONNECTION;
} else if (e instanceof SocketTimeoutException) {
@ -132,6 +137,10 @@ public class RemoteOperationResult {
return mSuccess;
}
public boolean isCancelled() {
return mCode == ResultCode.CANCELLED;
}
public int getHttpCode() {
return mHttpCode;
}
@ -169,7 +178,10 @@ public class RemoteOperationResult {
public String getLogMessage() {
if (mException != null) {
if (mException instanceof SocketException) {
if (mException instanceof OperationCancelledException) {
return "Operation cancelled by the caller";
} else if (mException instanceof SocketException) {
return "Socket exception";
} else if (mException instanceof SocketTimeoutException) {
@ -209,6 +221,9 @@ public class RemoteOperationResult {
} else if (mCode == ResultCode.BAD_OC_VERSION) {
return "No valid ownCloud version was found at the server";
} else if (mCode == ResultCode.STORAGE_ERROR_MOVING_FROM_TMP) {
return "Error while moving file from temporal to final directory";
}
return "Operation finished with HTTP status code " + mHttpCode + " (" + (isSuccess()?"success":"fail") + ")";

View file

@ -144,7 +144,7 @@ public class UploadFileOperation extends RemoteOperation {
try {
File f = new File(mLocalPath);
FileRequestEntity entity = new FileRequestEntity(f, mMimeType);
entity.setOnDatatransferProgressListener(mDataTransferListener);
entity.addOnDatatransferProgressListener(mDataTransferListener);
put.setRequestEntity(entity);
status = client.executeMethod(put);
client.exhaustResponse(put.getResponseBodyAsStream());

View file

@ -20,9 +20,13 @@ package com.owncloud.android.ui.activity;
import android.accounts.Account;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.app.FragmentTransaction;
import com.actionbarsherlock.app.ActionBar;
@ -30,6 +34,7 @@ import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.MenuItem;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.files.services.FileDownloader;
import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
import com.owncloud.android.ui.fragment.FileDetailFragment;
import com.owncloud.android.R;
@ -46,6 +51,7 @@ public class FileDetailActivity extends SherlockFragmentActivity implements File
public static final int DIALOG_SHORT_WAIT = 0;
private boolean mConfigurationChangedToLandscape = false;
private FileDownloaderBinder mDownloaderBinder = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -58,6 +64,8 @@ public class FileDetailActivity extends SherlockFragmentActivity implements File
);
if (!mConfigurationChangedToLandscape) {
bindService(new Intent(this, FileDownloader.class), mConnection, Context.BIND_AUTO_CREATE);
setContentView(R.layout.file_activity_details);
ActionBar actionBar = getSupportActionBar();
@ -78,6 +86,32 @@ public class FileDetailActivity extends SherlockFragmentActivity implements File
}
/** Defines callbacks for service binding, passed to bindService() */
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
mDownloaderBinder = (FileDownloaderBinder) service;
FileDetailFragment fragment = (FileDetailFragment) getSupportFragmentManager().findFragmentByTag(FileDetailFragment.FTAG);
if (fragment != null)
fragment.updateFileDetails(); // a new chance to get the mDownloadBinder through getDownloadBinder()
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
mDownloaderBinder = null;
}
};
@Override
public void onDestroy() {
super.onDestroy();
unbindService(mConnection);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
boolean returnValue = false;
@ -141,4 +175,13 @@ public class FileDetailActivity extends SherlockFragmentActivity implements File
// nothing to do here!
}
/**
* {@inheritDoc}
*/
@Override
public FileDownloaderBinder getFileDownloaderBinder() {
return mDownloaderBinder;
}
}

View file

@ -26,12 +26,14 @@ import android.app.ProgressDialog;
import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
@ -40,6 +42,7 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.support.v4.app.FragmentTransaction;
@ -64,6 +67,7 @@ import com.owncloud.android.datamodel.DataStorageManager;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.files.services.FileDownloader;
import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.network.OwnCloudClientUtils;
import com.owncloud.android.syncadapter.FileSyncService;
@ -90,6 +94,7 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements
private SyncBroadcastReceiver mSyncBroadcastReceiver;
private UploadFinishReceiver mUploadFinishReceiver;
private DownloadFinishReceiver mDownloadFinishReceiver;
private FileDownloaderBinder mDownloaderBinder = null;
private OCFileListFragment mFileList;
@ -123,6 +128,7 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements
} else { /// at least an account is available
bindService(new Intent(this, FileDownloader.class), mConnection, Context.BIND_AUTO_CREATE);
initDataFromCurrentAccount();
}
@ -206,6 +212,13 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements
}
@Override
public void onDestroy() {
super.onDestroy();
unbindService(mConnection);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getSherlock().getMenuInflater();
@ -421,7 +434,7 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements
registerReceiver(mDownloadFinishReceiver, downloadIntentFilter);
// List current directory
//mFileList.listDirectory(mCurrentDir);
mFileList.listDirectory(mCurrentDir); // we should find the way to avoid the need of this
} else {
@ -873,6 +886,40 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements
}
}
/**
* {@inheritDoc}
*/
@Override
public FileDownloaderBinder getFileDownloaderBinder() {
return mDownloaderBinder;
}
/** Defines callbacks for service binding, passed to bindService() */
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
mDownloaderBinder = (FileDownloaderBinder) service;
// a new chance to get the mDownloadBinder through getDownloadBinder() - THIS IS A MESS
mFileList.listDirectory();
if (mDualPane) {
FileDetailFragment fragment = (FileDetailFragment) getSupportFragmentManager().findFragmentByTag(FileDetailFragment.FTAG);
if (fragment != null)
fragment.updateFileDetails();
}
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
mDownloaderBinder = null;
}
};
/**
* Launch an intent to request the PIN code to the user before letting him use the app
*/

View file

@ -0,0 +1,15 @@
package com.owncloud.android.ui.activity;
import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
public interface TransferServiceGetter {
/**
* Callback method invoked when the parent activity is fully created to get a reference to the FileDownloader service API.
*
* @return Directory to list firstly. Can be NULL.
*/
public FileDownloaderBinder getFileDownloaderBinder();
}

View file

@ -23,8 +23,9 @@ import com.owncloud.android.AccountUtils;
import com.owncloud.android.DisplayUtils;
import com.owncloud.android.datamodel.DataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.files.services.FileDownloader;
import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.ui.activity.TransferServiceGetter;
import com.owncloud.android.R;
@ -52,12 +53,14 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
private Vector<OCFile> mFiles = null;
private DataStorageManager mStorageManager;
private Account mAccount;
private TransferServiceGetter mTransferServiceGetter;
public FileListListAdapter(OCFile file, DataStorageManager storage_man,
Context context) {
Context context, TransferServiceGetter transferServiceGetter) {
mStorageManager = storage_man;
mContext = context;
mAccount = AccountUtils.getCurrentOwnCloudAccount(mContext);
mTransferServiceGetter = transferServiceGetter;
swapDirectory(file);
/*mFile = file;
mFiles = mStorageManager.getDirectoryContent(mFile);*/
@ -118,7 +121,9 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
fileIcon.setImageResource(R.drawable.ic_menu_archive);
}
ImageView localStateView = (ImageView) view.findViewById(R.id.imageView2);
if (FileDownloader.isDownloading(mAccount, file.getRemotePath())) {
//if (FileDownloader.isDownloading(mAccount, file.getRemotePath())) {
FileDownloaderBinder downloaderBinder = mTransferServiceGetter.getFileDownloaderBinder();
if (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, file.getRemotePath())) {
localStateView.setImageResource(R.drawable.downloading_file_indicator);
localStateView.setVisibility(View.VISIBLE);
} else if (FileUploader.isUploading(mAccount, file.getRemotePath())) {

View file

@ -79,9 +79,12 @@ import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.files.services.FileDownloader;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
import com.owncloud.android.network.OwnCloudClientUtils;
import com.owncloud.android.ui.activity.FileDetailActivity;
import com.owncloud.android.ui.activity.FileDisplayActivity;
import com.owncloud.android.ui.activity.TransferServiceGetter;
import com.owncloud.android.ui.fragment.OCFileListFragment.ContainerActivity;
import com.owncloud.android.utils.OwnCloudVersion;
import com.owncloud.android.R;
@ -136,7 +139,7 @@ public class FileDetailFragment extends SherlockFragment implements
* @param fileToDetail An {@link OCFile} to show in the fragment
* @param ocAccount An ownCloud account; needed to start downloads
*/
public FileDetailFragment(OCFile fileToDetail, Account ocAccount){
public FileDetailFragment(OCFile fileToDetail, Account ocAccount) {
mFile = fileToDetail;
mAccount = ocAccount;
mLayout = R.layout.file_details_empty;
@ -147,20 +150,6 @@ public class FileDetailFragment extends SherlockFragment implements
}
/**
* {@inheritDoc}
*/
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mContainerActivity = (ContainerActivity) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement FileListFragment.ContainerActivity");
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@ -185,11 +174,24 @@ public class FileDetailFragment extends SherlockFragment implements
mPreview = (ImageView)mView.findViewById(R.id.fdPreview);
}
updateFileDetails();
return view;
}
/**
* {@inheritDoc}
*/
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mContainerActivity = (ContainerActivity) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement " + FileDetailFragment.ContainerActivity.class.getCanonicalName());
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
Log.i(getClass().toString(), "onSaveInstanceState() start");
@ -242,10 +244,10 @@ public class FileDetailFragment extends SherlockFragment implements
public void onClick(View v) {
switch (v.getId()) {
case R.id.fdDownloadBtn: {
if (FileDownloader.isDownloading(mAccount, mFile.getRemotePath())) {
// TODO cancelar descarga
//if (FileDownloader.isDownloading(mAccount, mFile.getRemotePath())) {
FileDownloaderBinder downloaderBinder = mContainerActivity.getFileDownloaderBinder();
if (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, mFile.getRemotePath())) {
downloaderBinder.cancel(mAccount, mFile.getRemotePath());
if (mFile.isDown()) {
setButtonsForDown();
} else {
@ -440,7 +442,9 @@ public class FileDetailFragment extends SherlockFragment implements
cb.setChecked(mFile.keepInSync());
// configure UI for depending upon local state of the file
if (FileDownloader.isDownloading(mAccount, mFile.getRemotePath()) || FileUploader.isUploading(mAccount, mFile.getRemotePath())) {
//if (FileDownloader.isDownloading(mAccount, mFile.getRemotePath()) || FileUploader.isUploading(mAccount, mFile.getRemotePath())) {
FileDownloaderBinder downloaderBinder = mContainerActivity.getFileDownloaderBinder();
if ((downloaderBinder != null && downloaderBinder.isDownloading(mAccount, mFile.getRemotePath())) || FileUploader.isUploading(mAccount, mFile.getRemotePath())) {
setButtonsForTransferring();
} else if (mFile.isDown()) {
@ -585,7 +589,7 @@ public class FileDetailFragment extends SherlockFragment implements
*
* @author David A. Velasco
*/
public interface ContainerActivity {
public interface ContainerActivity extends TransferServiceGetter {
/**
* Callback method invoked when the detail fragment wants to notice its container

View file

@ -19,7 +19,9 @@ package com.owncloud.android.ui.fragment;
import com.owncloud.android.datamodel.DataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
import com.owncloud.android.ui.FragmentListView;
import com.owncloud.android.ui.activity.TransferServiceGetter;
import com.owncloud.android.ui.adapter.FileListListAdapter;
import android.app.Activity;
@ -76,12 +78,15 @@ public class OCFileListFragment extends FragmentListView {
}
/**
* {@inheritDoc}
*/
@Override
public void onActivityCreated(Bundle savedInstanceState) {
Log.i(TAG, "onActivityCreated() start");
super.onCreate(savedInstanceState);
mAdapter = new FileListListAdapter(mContainerActivity.getInitialDirectory(), mContainerActivity.getStorageManager(), getActivity());
super.onActivityCreated(savedInstanceState);
mAdapter = new FileListListAdapter(mContainerActivity.getInitialDirectory(), mContainerActivity.getStorageManager(), getActivity(), mContainerActivity);
setListAdapter(mAdapter);
if (savedInstanceState != null) {
@ -154,7 +159,9 @@ public class OCFileListFragment extends FragmentListView {
* Calls {@link OCFileListFragment#listDirectory(OCFile)} with a null parameter
*/
public void listDirectory(){
int position = mList.getFirstVisiblePosition();
listDirectory(null);
mList.setSelectionFromTop(position, 0);
}
/**
@ -187,6 +194,7 @@ public class OCFileListFragment extends FragmentListView {
mFile = directory;
mAdapter.swapDirectory(mFile);
mList.setSelectionFromTop(0, 0);
mList.invalidate();
}
@ -196,7 +204,7 @@ public class OCFileListFragment extends FragmentListView {
*
* @author David A. Velasco
*/
public interface ContainerActivity {
public interface ContainerActivity extends TransferServiceGetter {
/**
* Callback method invoked when a directory is clicked by the user on the files list

View file

@ -95,7 +95,7 @@ public class ChunkFromFileChannelRequestEntity implements RequestEntity {
out.write(mBuffer.array(), 0, readCount);
mBuffer.clear();
if (mListener != null)
mListener.transferProgress(readCount);
mListener.onTransferProgress(readCount);
}
} catch (IOException io) {

View file

@ -7,6 +7,10 @@ import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.apache.commons.httpclient.methods.RequestEntity;
@ -23,7 +27,7 @@ public class FileRequestEntity implements RequestEntity {
final File mFile;
final String mContentType;
OnDatatransferProgressListener mListener;
Set<OnDatatransferProgressListener> mListeners = new HashSet<OnDatatransferProgressListener>();
public FileRequestEntity(final File file, final String contentType) {
super();
@ -49,10 +53,19 @@ public class FileRequestEntity implements RequestEntity {
return true;
}
public void setOnDatatransferProgressListener(OnDatatransferProgressListener listener) {
mListener = listener;
public void addOnDatatransferProgressListener(OnDatatransferProgressListener listener) {
mListeners.add(listener);
}
public void addOnDatatransferProgressListeners(Collection<OnDatatransferProgressListener> listeners) {
mListeners.addAll(listeners);
}
public void removeOnDatatransferProgressListener(OnDatatransferProgressListener listener) {
mListeners.remove(listener);
}
@Override
public void writeRequest(final OutputStream out) throws IOException {
//byte[] tmp = new byte[4096];
@ -64,22 +77,22 @@ public class FileRequestEntity implements RequestEntity {
RandomAccessFile raf = new RandomAccessFile(mFile, "rw");
FileChannel channel = raf.getChannel();
FileLock lock = channel.tryLock();
//InputStream instream = new FileInputStream(this.file);
Iterator<OnDatatransferProgressListener> it = null;
try {
//while ((i = instream.read(tmp)) >= 0) {
while ((i = channel.read(tmp)) >= 0) {
out.write(tmp.array(), 0, i);
tmp.clear();
if (mListener != null)
mListener.transferProgress(i);
it = mListeners.iterator();
while (it.hasNext()) {
it.next().onTransferProgress(i);
}
}
} catch (IOException io) {
Log.e("FileRequestException", io.getMessage());
throw new RuntimeException("Ugly solution to workaround the default policy of retries when the server falls while uploading ; temporal fix; really", io);
} finally {
//instream.close();
lock.release();
channel.close();
raf.close();

View file

@ -1,5 +1,6 @@
package eu.alefzero.webdav;
public interface OnDatatransferProgressListener {
public void transferProgress(long progressRate);
public void onTransferProgress(long progressRate);
public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileName);
}

View file

@ -98,7 +98,7 @@ public class WebdavClient extends HttpClient {
int readResult;
while ((readResult = bis.read(bytes)) != -1) {
if (mDataTransferListener != null)
mDataTransferListener.transferProgress(readResult);
mDataTransferListener.onTransferProgress(readResult);
fos.write(bytes, 0, readResult);
}
fos.close();
@ -165,7 +165,7 @@ public class WebdavClient extends HttpClient {
try {
File f = new File(localFile);
FileRequestEntity entity = new FileRequestEntity(f, contentType);
entity.setOnDatatransferProgressListener(mDataTransferListener);
entity.addOnDatatransferProgressListener(mDataTransferListener);
put.setRequestEntity(entity);
status = executeMethod(put);