Run folder download cancelation in new thread
[pub/Android/ownCloud.git] / src / com / owncloud / android / files / services / FileDownloader.java
1 /* ownCloud Android client application
2 * Copyright (C) 2012 Bartek Przybylski
3 * Copyright (C) 2012-2013 ownCloud Inc.
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 2,
7 * as published by the Free Software Foundation.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 */
18
19 package com.owncloud.android.files.services;
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.util.AbstractList;
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.Iterator;
27 import java.util.Map;
28 import java.util.Vector;
29 import java.util.concurrent.ConcurrentHashMap;
30 import java.util.concurrent.ConcurrentMap;
31
32 import com.owncloud.android.R;
33 import com.owncloud.android.authentication.AuthenticatorActivity;
34 import com.owncloud.android.datamodel.FileDataStorageManager;
35 import com.owncloud.android.datamodel.OCFile;
36
37 import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
38 import com.owncloud.android.lib.common.OwnCloudAccount;
39 import com.owncloud.android.lib.common.OwnCloudClient;
40 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
41 import com.owncloud.android.notifications.NotificationBuilderWithProgressBar;
42 import com.owncloud.android.notifications.NotificationDelayer;
43 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
44 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
45 import com.owncloud.android.lib.common.utils.Log_OC;
46 import com.owncloud.android.lib.resources.files.FileUtils;
47 import com.owncloud.android.operations.DownloadFileOperation;
48 import com.owncloud.android.ui.activity.FileActivity;
49 import com.owncloud.android.ui.activity.FileDisplayActivity;
50 import com.owncloud.android.ui.preview.PreviewImageActivity;
51 import com.owncloud.android.ui.preview.PreviewImageFragment;
52 import com.owncloud.android.utils.ErrorMessageAdapter;
53
54 import android.accounts.Account;
55 import android.accounts.AccountsException;
56 import android.app.NotificationManager;
57 import android.app.PendingIntent;
58 import android.app.Service;
59 import android.content.Intent;
60 import android.os.Binder;
61 import android.os.Handler;
62 import android.os.HandlerThread;
63 import android.os.IBinder;
64 import android.os.Looper;
65 import android.os.Message;
66 import android.os.Process;
67 import android.support.v4.app.NotificationCompat;
68
69 public class FileDownloader extends Service implements OnDatatransferProgressListener {
70
71 public static final String EXTRA_ACCOUNT = "ACCOUNT";
72 public static final String EXTRA_FILE = "FILE";
73
74 public static final String ACTION_CANCEL_FILE_DOWNLOAD = "CANCEL_FILE_DOWNLOAD";
75
76 private static final String DOWNLOAD_ADDED_MESSAGE = "DOWNLOAD_ADDED";
77 private static final String DOWNLOAD_FINISH_MESSAGE = "DOWNLOAD_FINISH";
78 public static final String EXTRA_DOWNLOAD_RESULT = "RESULT";
79 public static final String EXTRA_FILE_PATH = "FILE_PATH";
80 public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH";
81 public static final String ACCOUNT_NAME = "ACCOUNT_NAME";
82
83 private static final String TAG = "FileDownloader";
84
85 private Looper mServiceLooper;
86 private ServiceHandler mServiceHandler;
87 private IBinder mBinder;
88 private OwnCloudClient mDownloadClient = null;
89 private Account mLastAccount = null;
90 private FileDataStorageManager mStorageManager;
91
92 private ConcurrentMap<String, DownloadFileOperation> mPendingDownloads = new ConcurrentHashMap<String, DownloadFileOperation>();
93 private DownloadFileOperation mCurrentDownload = null;
94
95 private NotificationManager mNotificationManager;
96 private NotificationCompat.Builder mNotificationBuilder;
97 private int mLastPercent;
98
99 private Account mAccount;
100 private OCFile mFile;
101
102
103 public static String getDownloadAddedMessage() {
104 return FileDownloader.class.getName().toString() + DOWNLOAD_ADDED_MESSAGE;
105 }
106
107 public static String getDownloadFinishMessage() {
108 return FileDownloader.class.getName().toString() + DOWNLOAD_FINISH_MESSAGE;
109 }
110
111 /**
112 * Builds a key for mPendingDownloads from the account and file to download
113 *
114 * @param account Account where the file to download is stored
115 * @param file File to download
116 */
117 private String buildRemoteName(Account account, OCFile file) {
118 return account.name + file.getRemotePath();
119 }
120
121
122 /**
123 * Service initialization
124 */
125 @Override
126 public void onCreate() {
127 super.onCreate();
128 mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
129 HandlerThread thread = new HandlerThread("FileDownloaderThread",
130 Process.THREAD_PRIORITY_BACKGROUND);
131 thread.start();
132 mServiceLooper = thread.getLooper();
133 mServiceHandler = new ServiceHandler(mServiceLooper, this);
134 mBinder = new FileDownloaderBinder();
135 }
136
137 /**
138 * Entry point to add one or several files to the queue of downloads.
139 *
140 * New downloads are added calling to startService(), resulting in a call to this method. This ensures the service will keep on working
141 * although the caller activity goes away.
142 */
143 @Override
144 public int onStartCommand(Intent intent, int flags, int startId) {
145 if ( !intent.hasExtra(EXTRA_ACCOUNT) ||
146 !intent.hasExtra(EXTRA_FILE)
147 /*!intent.hasExtra(EXTRA_FILE_PATH) ||
148 !intent.hasExtra(EXTRA_REMOTE_PATH)*/
149 ) {
150 Log_OC.e(TAG, "Not enough information provided in intent");
151 return START_NOT_STICKY;
152 } else {
153 mAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
154 mFile = intent.getParcelableExtra(EXTRA_FILE);
155
156 if (ACTION_CANCEL_FILE_DOWNLOAD.equals(intent.getAction())) {
157
158 new Thread(new Runnable() {
159 public void run() {
160 // Cancel the download
161 cancel(mAccount,mFile);
162 }
163 }).start();
164
165 } else {
166
167 AbstractList<String> requestedDownloads = new Vector<String>(); // dvelasco: now this always contains just one element, but that can change in a near future (download of multiple selection)
168 String downloadKey = buildRemoteName(mAccount, mFile);
169 try {
170 DownloadFileOperation newDownload = new DownloadFileOperation(mAccount, mFile);
171 mPendingDownloads.putIfAbsent(downloadKey, newDownload);
172 newDownload.addDatatransferProgressListener(this);
173 newDownload.addDatatransferProgressListener((FileDownloaderBinder) mBinder);
174 requestedDownloads.add(downloadKey);
175 sendBroadcastNewDownload(newDownload);
176
177 } catch (IllegalArgumentException e) {
178 Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage());
179 return START_NOT_STICKY;
180 }
181
182 if (requestedDownloads.size() > 0) {
183 Message msg = mServiceHandler.obtainMessage();
184 msg.arg1 = startId;
185 msg.obj = requestedDownloads;
186 mServiceHandler.sendMessage(msg);
187 }
188 }
189 }
190
191 return START_NOT_STICKY;
192 }
193
194
195 /**
196 * Provides a binder object that clients can use to perform operations on the queue of downloads, excepting the addition of new files.
197 *
198 * Implemented to perform cancellation, pause and resume of existing downloads.
199 */
200 @Override
201 public IBinder onBind(Intent arg0) {
202 return mBinder;
203 }
204
205
206 /**
207 * Called when ALL the bound clients were onbound.
208 */
209 @Override
210 public boolean onUnbind(Intent intent) {
211 ((FileDownloaderBinder)mBinder).clearListeners();
212 return false; // not accepting rebinding (default behaviour)
213 }
214
215
216 /**
217 * Binder to let client components to perform operations on the queue of downloads.
218 *
219 * It provides by itself the available operations.
220 */
221 public class FileDownloaderBinder extends Binder implements OnDatatransferProgressListener {
222
223 /**
224 * Map of listeners that will be reported about progress of downloads from a {@link FileDownloaderBinder} instance
225 */
226 private Map<String, OnDatatransferProgressListener> mBoundListeners = new HashMap<String, OnDatatransferProgressListener>();
227
228
229 /**
230 * Cancels a pending or current download of a remote file.
231 *
232 * @param account Owncloud account where the remote file is stored.
233 * @param file A file in the queue of pending downloads
234 */
235 public void cancel(Account account, OCFile file) {
236 DownloadFileOperation download = null;
237 synchronized (mPendingDownloads) {
238 download = mPendingDownloads.remove(buildRemoteName(account, file));
239 }
240 if (download != null) {
241 download.cancel();
242 }
243 }
244
245
246 public void clearListeners() {
247 mBoundListeners.clear();
248 }
249
250
251 /**
252 * Returns True when the file described by 'file' in the ownCloud account 'account' is downloading or waiting to download.
253 *
254 * If 'file' is a directory, returns 'true' if some of its descendant files is downloading or waiting to download.
255 *
256 * @param account Owncloud account where the remote file is stored.
257 * @param file A file that could be in the queue of downloads.
258 */
259 public boolean isDownloading(Account account, OCFile file) {
260 if (account == null || file == null) return false;
261 String targetKey = buildRemoteName(account, file);
262 synchronized (mPendingDownloads) {
263 if (file.isFolder()) {
264 // this can be slow if there are many downloads :(
265 Iterator<String> it = mPendingDownloads.keySet().iterator();
266 boolean found = false;
267 while (it.hasNext() && !found) {
268 found = it.next().startsWith(targetKey);
269 }
270 return found;
271 } else {
272 return (mPendingDownloads.containsKey(targetKey));
273 }
274 }
275 }
276
277
278 /**
279 * Adds a listener interested in the progress of the download for a concrete file.
280 *
281 * @param listener Object to notify about progress of transfer.
282 * @param account ownCloud account holding the file of interest.
283 * @param file {@link OCfile} of interest for listener.
284 */
285 public void addDatatransferProgressListener (OnDatatransferProgressListener listener, Account account, OCFile file) {
286 if (account == null || file == null || listener == null) return;
287 String targetKey = buildRemoteName(account, file);
288 mBoundListeners.put(targetKey, listener);
289 }
290
291
292 /**
293 * Removes a listener interested in the progress of the download for a concrete file.
294 *
295 * @param listener Object to notify about progress of transfer.
296 * @param account ownCloud account holding the file of interest.
297 * @param file {@link OCfile} of interest for listener.
298 */
299 public void removeDatatransferProgressListener (OnDatatransferProgressListener listener, Account account, OCFile file) {
300 if (account == null || file == null || listener == null) return;
301 String targetKey = buildRemoteName(account, file);
302 if (mBoundListeners.get(targetKey) == listener) {
303 mBoundListeners.remove(targetKey);
304 }
305 }
306
307 @Override
308 public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer,
309 String fileName) {
310 String key = buildRemoteName(mCurrentDownload.getAccount(), mCurrentDownload.getFile());
311 OnDatatransferProgressListener boundListener = mBoundListeners.get(key);
312 if (boundListener != null) {
313 boundListener.onTransferProgress(progressRate, totalTransferredSoFar, totalToTransfer, fileName);
314 }
315 }
316
317 }
318
319
320 /**
321 * Download worker. Performs the pending downloads in the order they were requested.
322 *
323 * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.
324 */
325 private static class ServiceHandler extends Handler {
326 // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak
327 FileDownloader mService;
328 public ServiceHandler(Looper looper, FileDownloader service) {
329 super(looper);
330 if (service == null)
331 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'");
332 mService = service;
333 }
334
335 @Override
336 public void handleMessage(Message msg) {
337 @SuppressWarnings("unchecked")
338 AbstractList<String> requestedDownloads = (AbstractList<String>) msg.obj;
339 if (msg.obj != null) {
340 Iterator<String> it = requestedDownloads.iterator();
341 while (it.hasNext()) {
342 mService.downloadFile(it.next());
343 }
344 }
345 mService.stopSelf(msg.arg1);
346 }
347 }
348
349
350 /**
351 * Core download method: requests a file to download and stores it.
352 *
353 * @param downloadKey Key to access the download to perform, contained in mPendingDownloads
354 */
355 private void downloadFile(String downloadKey) {
356
357 synchronized(mPendingDownloads) {
358 mCurrentDownload = mPendingDownloads.get(downloadKey);
359 }
360
361 if (mCurrentDownload != null) {
362
363 notifyDownloadStart(mCurrentDownload);
364
365 RemoteOperationResult downloadResult = null;
366 try {
367 /// prepare client object to send the request to the ownCloud server
368 if (mDownloadClient == null || !mLastAccount.equals(mCurrentDownload.getAccount())) {
369 mLastAccount = mCurrentDownload.getAccount();
370 mStorageManager =
371 new FileDataStorageManager(mLastAccount, getContentResolver());
372 OwnCloudAccount ocAccount = new OwnCloudAccount(mLastAccount, this);
373 mDownloadClient = OwnCloudClientManagerFactory.getDefaultSingleton().
374 getClientFor(ocAccount, this);
375 }
376
377 /// perform the download
378 downloadResult = mCurrentDownload.execute(mDownloadClient);
379 if (downloadResult.isSuccess()) {
380 saveDownloadedFile();
381 }
382
383 } catch (AccountsException e) {
384 Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e);
385 downloadResult = new RemoteOperationResult(e);
386 } catch (IOException e) {
387 Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e);
388 downloadResult = new RemoteOperationResult(e);
389
390 } finally {
391 synchronized(mPendingDownloads) {
392 mPendingDownloads.remove(downloadKey);
393 }
394 }
395
396
397 /// notify result
398 notifyDownloadResult(mCurrentDownload, downloadResult);
399
400 sendBroadcastDownloadFinished(mCurrentDownload, downloadResult);
401 }
402 }
403
404
405 /**
406 * Updates the OC File after a successful download.
407 */
408 private void saveDownloadedFile() {
409 OCFile file = mStorageManager.getFileById(mCurrentDownload.getFile().getFileId());
410 long syncDate = System.currentTimeMillis();
411 file.setLastSyncDateForProperties(syncDate);
412 file.setLastSyncDateForData(syncDate);
413 file.setNeedsUpdateThumbnail(true);
414 file.setModificationTimestamp(mCurrentDownload.getModificationTimestamp());
415 file.setModificationTimestampAtLastSyncForData(mCurrentDownload.getModificationTimestamp());
416 // file.setEtag(mCurrentDownload.getEtag()); // TODO Etag, where available
417 file.setMimetype(mCurrentDownload.getMimeType());
418 file.setStoragePath(mCurrentDownload.getSavePath());
419 file.setFileLength((new File(mCurrentDownload.getSavePath()).length()));
420 file.setRemoteId(mCurrentDownload.getFile().getRemoteId());
421 mStorageManager.saveFile(file);
422 mStorageManager.triggerMediaScan(file.getStoragePath());
423 }
424
425
426 /**
427 * Creates a status notification to show the download progress
428 *
429 * @param download Download operation starting.
430 */
431 private void notifyDownloadStart(DownloadFileOperation download) {
432 /// create status notification with a progress bar
433 mLastPercent = 0;
434 mNotificationBuilder =
435 NotificationBuilderWithProgressBar.newNotificationBuilderWithProgressBar(this);
436 mNotificationBuilder
437 .setSmallIcon(R.drawable.notification_icon)
438 .setTicker(getString(R.string.downloader_download_in_progress_ticker))
439 .setContentTitle(getString(R.string.downloader_download_in_progress_ticker))
440 .setOngoing(true)
441 .setProgress(100, 0, download.getSize() < 0)
442 .setContentText(
443 String.format(getString(R.string.downloader_download_in_progress_content), 0,
444 new File(download.getSavePath()).getName())
445 );
446
447 /// includes a pending intent in the notification showing the details view of the file
448 Intent showDetailsIntent = null;
449 if (PreviewImageFragment.canBePreviewed(download.getFile())) {
450 showDetailsIntent = new Intent(this, PreviewImageActivity.class);
451 } else {
452 showDetailsIntent = new Intent(this, FileDisplayActivity.class);
453 }
454 showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, download.getFile());
455 showDetailsIntent.putExtra(FileActivity.EXTRA_ACCOUNT, download.getAccount());
456 showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
457
458 mNotificationBuilder.setContentIntent(PendingIntent.getActivity(
459 this, (int) System.currentTimeMillis(), showDetailsIntent, 0
460 ));
461
462 mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotificationBuilder.build());
463 }
464
465
466 /**
467 * Callback method to update the progress bar in the status notification.
468 */
469 @Override
470 public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String filePath) {
471 int percent = (int)(100.0*((double)totalTransferredSoFar)/((double)totalToTransfer));
472 if (percent != mLastPercent) {
473 mNotificationBuilder.setProgress(100, percent, totalToTransfer < 0);
474 String fileName = filePath.substring(filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1);
475 String text = String.format(getString(R.string.downloader_download_in_progress_content), percent, fileName);
476 mNotificationBuilder.setContentText(text);
477 mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotificationBuilder.build());
478 }
479 mLastPercent = percent;
480 }
481
482
483 /**
484 * Updates the status notification with the result of a download operation.
485 *
486 * @param downloadResult Result of the download operation.
487 * @param download Finished download operation
488 */
489 private void notifyDownloadResult(DownloadFileOperation download, RemoteOperationResult downloadResult) {
490 mNotificationManager.cancel(R.string.downloader_download_in_progress_ticker);
491 if (!downloadResult.isCancelled()) {
492 int tickerId = (downloadResult.isSuccess()) ? R.string.downloader_download_succeeded_ticker :
493 R.string.downloader_download_failed_ticker;
494
495 boolean needsToUpdateCredentials = (
496 downloadResult.getCode() == ResultCode.UNAUTHORIZED ||
497 downloadResult.isIdPRedirection()
498 );
499 tickerId = (needsToUpdateCredentials) ?
500 R.string.downloader_download_failed_credentials_error : tickerId;
501
502 mNotificationBuilder
503 .setTicker(getString(tickerId))
504 .setContentTitle(getString(tickerId))
505 .setAutoCancel(true)
506 .setOngoing(false)
507 .setProgress(0, 0, false);
508
509 if (needsToUpdateCredentials) {
510
511 // let the user update credentials with one click
512 Intent updateAccountCredentials = new Intent(this, AuthenticatorActivity.class);
513 updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, download.getAccount());
514 updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACTION, AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN);
515 updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
516 updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
517 updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND);
518 mNotificationBuilder
519 .setContentIntent(PendingIntent.getActivity(
520 this, (int) System.currentTimeMillis(), updateAccountCredentials, PendingIntent.FLAG_ONE_SHOT));
521
522 mDownloadClient = null; // grant that future retries on the same account will get the fresh credentials
523
524 } else {
525 // TODO put something smart in showDetailsIntent
526 Intent showDetailsIntent = new Intent();
527 mNotificationBuilder
528 .setContentIntent(PendingIntent.getActivity(
529 this, (int) System.currentTimeMillis(), showDetailsIntent, 0));
530 }
531
532 mNotificationBuilder.setContentText(ErrorMessageAdapter.getErrorCauseMessage(downloadResult, download, getResources()));
533 mNotificationManager.notify(tickerId, mNotificationBuilder.build());
534
535 // Remove success notification
536 if (downloadResult.isSuccess()) {
537 // Sleep 2 seconds, so show the notification before remove it
538 NotificationDelayer.cancelWithDelay(
539 mNotificationManager,
540 R.string.downloader_download_succeeded_ticker,
541 2000);
542 }
543
544 }
545 }
546
547
548 /**
549 * Sends a broadcast when a download finishes in order to the interested activities can update their view
550 *
551 * @param download Finished download operation
552 * @param downloadResult Result of the download operation
553 */
554 private void sendBroadcastDownloadFinished(DownloadFileOperation download, RemoteOperationResult downloadResult) {
555 Intent end = new Intent(getDownloadFinishMessage());
556 end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess());
557 end.putExtra(ACCOUNT_NAME, download.getAccount().name);
558 end.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());
559 end.putExtra(EXTRA_FILE_PATH, download.getSavePath());
560 sendStickyBroadcast(end);
561 }
562
563
564 /**
565 * Sends a broadcast when a new download is added to the queue.
566 *
567 * @param download Added download operation
568 */
569 private void sendBroadcastNewDownload(DownloadFileOperation download) {
570 Intent added = new Intent(getDownloadAddedMessage());
571 added.putExtra(ACCOUNT_NAME, download.getAccount().name);
572 added.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());
573 added.putExtra(EXTRA_FILE_PATH, download.getSavePath());
574 sendStickyBroadcast(added);
575 }
576
577 /**
578 * Cancel operation
579 * @param account Owncloud account where the remote file is stored.
580 * @param file File OCFile
581 */
582 public void cancel(Account account, OCFile file){
583 if(Looper.myLooper() == Looper.getMainLooper()) {
584 Log_OC.d(TAG, "Current Thread is Main Thread.");
585 } else {
586 Log_OC.d(TAG, "Current Thread is NOT Main Thread.");
587 }
588
589 DownloadFileOperation download = null;
590 String targetKey = buildRemoteName(account, file);
591 ArrayList<String> keyItems = new ArrayList<String>();
592 synchronized (mPendingDownloads) {
593 if (file.isFolder()) {
594 Log_OC.d(TAG, "Folder download. Canceling pending downloads (from folder)");
595 Iterator<String> it = mPendingDownloads.keySet().iterator();
596 boolean found = false;
597 while (it.hasNext()) {
598 String keyDownloadOperation = it.next();
599 found = keyDownloadOperation.startsWith(targetKey);
600 if (found) {
601 keyItems.add(keyDownloadOperation);
602 }
603 }
604 } else {
605 Log_OC.d(TAG, "Canceling file download");
606 keyItems.add(buildRemoteName(account, file));
607 }
608 }
609 for (String item: keyItems) {
610 download = mPendingDownloads.remove(item);
611 Log_OC.d(TAG, "Key removed: " + item);
612
613 if (download != null) {
614 download.cancel();
615 }
616 }
617 }
618 }