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