2b70606c4f7464db5ec3d717cb1cc42796c01378
[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 }
235 }
236
237
238 public void clearListeners() {
239 mBoundListeners.clear();
240 }
241
242
243 /**
244 * Returns True when the file described by 'file' in the ownCloud account 'account' is downloading or
245 * waiting to download.
246 *
247 * If 'file' is a directory, returns 'true' if any of its descendant files is downloading or
248 * waiting to download.
249 *
250 * @param account ownCloud account where the remote file is stored.
251 * @param file A file that could be in the queue of downloads.
252 */
253 public boolean isDownloading(Account account, OCFile file) {
254 if (account == null || file == null) return false;
255 return (mPendingDownloads.contains(account, file.getRemotePath()));
256 }
257
258
259 /**
260 * Adds a listener interested in the progress of the download for a concrete file.
261 *
262 * @param listener Object to notify about progress of transfer.
263 * @param account ownCloud account holding the file of interest.
264 * @param file {@link OCFile} of interest for listener.
265 */
266 public void addDatatransferProgressListener (
267 OnDatatransferProgressListener listener, Account account, OCFile file
268 ) {
269 if (account == null || file == null || listener == null) return;
270 //String targetKey = buildKey(account, file.getRemotePath());
271 mBoundListeners.put(file.getFileId(), listener);
272 }
273
274
275 /**
276 * Removes a listener interested in the progress of the download for a concrete file.
277 *
278 * @param listener Object to notify about progress of transfer.
279 * @param account ownCloud account holding the file of interest.
280 * @param file {@link OCFile} of interest for listener.
281 */
282 public void removeDatatransferProgressListener (
283 OnDatatransferProgressListener listener, Account account, OCFile file
284 ) {
285 if (account == null || file == null || listener == null) return;
286 //String targetKey = buildKey(account, file.getRemotePath());
287 Long fileId = file.getFileId();
288 if (mBoundListeners.get(fileId) == listener) {
289 mBoundListeners.remove(fileId);
290 }
291 }
292
293 @Override
294 public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer,
295 String fileName) {
296 //String key = buildKey(mCurrentDownload.getAccount(), mCurrentDownload.getFile().getRemotePath());
297 OnDatatransferProgressListener boundListener = mBoundListeners.get(mCurrentDownload.getFile().getFileId());
298 if (boundListener != null) {
299 boundListener.onTransferProgress(progressRate, totalTransferredSoFar, totalToTransfer, fileName);
300 }
301 }
302
303 }
304
305
306 /**
307 * Download worker. Performs the pending downloads in the order they were requested.
308 *
309 * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.
310 */
311 private static class ServiceHandler extends Handler {
312 // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak
313 FileDownloader mService;
314 public ServiceHandler(Looper looper, FileDownloader service) {
315 super(looper);
316 if (service == null)
317 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'");
318 mService = service;
319 }
320
321 @Override
322 public void handleMessage(Message msg) {
323 @SuppressWarnings("unchecked")
324 AbstractList<String> requestedDownloads = (AbstractList<String>) msg.obj;
325 if (msg.obj != null) {
326 Iterator<String> it = requestedDownloads.iterator();
327 while (it.hasNext()) {
328 mService.downloadFile(it.next());
329 }
330 }
331 mService.stopSelf(msg.arg1);
332 }
333 }
334
335
336 /**
337 * Core download method: requests a file to download and stores it.
338 *
339 * @param downloadKey Key to access the download to perform, contained in mPendingDownloads
340 */
341 private void downloadFile(String downloadKey) {
342
343 mCurrentDownload = mPendingDownloads.get(downloadKey);
344
345 if (mCurrentDownload != null) {
346
347 notifyDownloadStart(mCurrentDownload);
348
349 RemoteOperationResult downloadResult = null;
350 try {
351 /// prepare client object to send the request to the ownCloud server
352 if (mDownloadClient == null || !mLastAccount.equals(mCurrentDownload.getAccount())) {
353 mLastAccount = mCurrentDownload.getAccount();
354 mStorageManager =
355 new FileDataStorageManager(mLastAccount, getContentResolver());
356 OwnCloudAccount ocAccount = new OwnCloudAccount(mLastAccount, this);
357 mDownloadClient = OwnCloudClientManagerFactory.getDefaultSingleton().
358 getClientFor(ocAccount, this);
359 }
360
361 /// perform the download
362 downloadResult = mCurrentDownload.execute(mDownloadClient);
363 if (downloadResult.isSuccess()) {
364 saveDownloadedFile();
365 /*} else {
366 updateUnsuccessfulDownloadedFile();
367 */
368 }
369
370 } catch (AccountsException e) {
371 Log_OC.e(TAG, "Error while trying to get authorization for " + mLastAccount.name, e);
372 downloadResult = new RemoteOperationResult(e);
373 } catch (IOException e) {
374 Log_OC.e(TAG, "Error while trying to get authorization for " + mLastAccount.name, e);
375 downloadResult = new RemoteOperationResult(e);
376
377 } finally {
378 mPendingDownloads.remove(mLastAccount, mCurrentDownload.getRemotePath());
379 }
380
381
382 /// notify result
383 notifyDownloadResult(mCurrentDownload, downloadResult);
384
385 sendBroadcastDownloadFinished(mCurrentDownload, downloadResult);
386 }
387 }
388
389
390 /**
391 * Updates the OC File after a successful download.
392 */
393 private void saveDownloadedFile() {
394 OCFile file = mStorageManager.getFileById(mCurrentDownload.getFile().getFileId());
395 long syncDate = System.currentTimeMillis();
396 file.setLastSyncDateForProperties(syncDate);
397 file.setLastSyncDateForData(syncDate);
398 file.setNeedsUpdateThumbnail(true);
399 file.setModificationTimestamp(mCurrentDownload.getModificationTimestamp());
400 file.setModificationTimestampAtLastSyncForData(mCurrentDownload.getModificationTimestamp());
401 // file.setEtag(mCurrentDownload.getEtag()); // TODO Etag, where available
402 file.setMimetype(mCurrentDownload.getMimeType());
403 file.setStoragePath(mCurrentDownload.getSavePath());
404 file.setFileLength((new File(mCurrentDownload.getSavePath()).length()));
405 file.setRemoteId(mCurrentDownload.getFile().getRemoteId());
406 //file.setDownloading(false);
407 mStorageManager.saveFile(file);
408 mStorageManager.triggerMediaScan(file.getStoragePath());
409 }
410
411 /**
412 * Update the OC File after a unsuccessful download
413 */
414 private void updateUnsuccessfulDownloadedFile() {
415 OCFile file = mStorageManager.getFileById(mCurrentDownload.getFile().getFileId());
416 file.setDownloading(false);
417 mStorageManager.saveFile(file);
418 }
419
420
421 /**
422 * Creates a status notification to show the download progress
423 *
424 * @param download Download operation starting.
425 */
426 private void notifyDownloadStart(DownloadFileOperation download) {
427 /// create status notification with a progress bar
428 mLastPercent = 0;
429 mNotificationBuilder =
430 NotificationBuilderWithProgressBar.newNotificationBuilderWithProgressBar(this);
431 mNotificationBuilder
432 .setSmallIcon(R.drawable.notification_icon)
433 .setTicker(getString(R.string.downloader_download_in_progress_ticker))
434 .setContentTitle(getString(R.string.downloader_download_in_progress_ticker))
435 .setOngoing(true)
436 .setProgress(100, 0, download.getSize() < 0)
437 .setContentText(
438 String.format(getString(R.string.downloader_download_in_progress_content), 0,
439 new File(download.getSavePath()).getName())
440 );
441
442 /// includes a pending intent in the notification showing the details view of the file
443 Intent showDetailsIntent = null;
444 if (PreviewImageFragment.canBePreviewed(download.getFile())) {
445 showDetailsIntent = new Intent(this, PreviewImageActivity.class);
446 } else {
447 showDetailsIntent = new Intent(this, FileDisplayActivity.class);
448 }
449 showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, download.getFile());
450 showDetailsIntent.putExtra(FileActivity.EXTRA_ACCOUNT, download.getAccount());
451 showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
452
453 mNotificationBuilder.setContentIntent(PendingIntent.getActivity(
454 this, (int) System.currentTimeMillis(), showDetailsIntent, 0
455 ));
456
457 mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotificationBuilder.build());
458 }
459
460
461 /**
462 * Callback method to update the progress bar in the status notification.
463 */
464 @Override
465 public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String filePath)
466 {
467 int percent = (int)(100.0*((double)totalTransferredSoFar)/((double)totalToTransfer));
468 if (percent != mLastPercent) {
469 mNotificationBuilder.setProgress(100, percent, totalToTransfer < 0);
470 String fileName = filePath.substring(filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1);
471 String text = String.format(getString(R.string.downloader_download_in_progress_content), percent, fileName);
472 mNotificationBuilder.setContentText(text);
473 mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotificationBuilder.build());
474 }
475 mLastPercent = percent;
476 }
477
478
479 /**
480 * Updates the status notification with the result of a download operation.
481 *
482 * @param downloadResult Result of the download operation.
483 * @param download Finished download operation
484 */
485 private void notifyDownloadResult(DownloadFileOperation download, RemoteOperationResult downloadResult) {
486 mNotificationManager.cancel(R.string.downloader_download_in_progress_ticker);
487 if (!downloadResult.isCancelled()) {
488 int tickerId = (downloadResult.isSuccess()) ? R.string.downloader_download_succeeded_ticker :
489 R.string.downloader_download_failed_ticker;
490
491 boolean needsToUpdateCredentials = (
492 downloadResult.getCode() == ResultCode.UNAUTHORIZED ||
493 downloadResult.isIdPRedirection()
494 );
495 tickerId = (needsToUpdateCredentials) ?
496 R.string.downloader_download_failed_credentials_error : tickerId;
497
498 mNotificationBuilder
499 .setTicker(getString(tickerId))
500 .setContentTitle(getString(tickerId))
501 .setAutoCancel(true)
502 .setOngoing(false)
503 .setProgress(0, 0, false);
504
505 if (needsToUpdateCredentials) {
506
507 // let the user update credentials with one click
508 Intent updateAccountCredentials = new Intent(this, AuthenticatorActivity.class);
509 updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, download.getAccount());
510 updateAccountCredentials.putExtra(
511 AuthenticatorActivity.EXTRA_ACTION, AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN
512 );
513 updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
514 updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
515 updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND);
516 mNotificationBuilder
517 .setContentIntent(PendingIntent.getActivity(
518 this, (int) System.currentTimeMillis(), updateAccountCredentials, PendingIntent.FLAG_ONE_SHOT));
519
520 mDownloadClient = null; // grant that future retries on the same account will get the fresh credentials
521
522 } else {
523 // TODO put something smart in showDetailsIntent
524 Intent showDetailsIntent = new Intent();
525 mNotificationBuilder
526 .setContentIntent(PendingIntent.getActivity(
527 this, (int) System.currentTimeMillis(), showDetailsIntent, 0));
528 }
529
530 mNotificationBuilder.setContentText(
531 ErrorMessageAdapter.getErrorCauseMessage(downloadResult, download, getResources())
532 );
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 DownloadFileOperation download = null;
584 //String targetKey = buildKey(account, file.getRemotePath());
585 ArrayList<String> keyItems = new ArrayList<String>();
586 if (file.isFolder()) {
587 Log_OC.d(TAG, "Folder download. Canceling pending downloads (from folder)");
588
589 // TODO
590 /*
591 Iterator<String> it = mPendingDownloads.keySet().iterator();
592 boolean found = false;
593 while (it.hasNext()) {
594 String keyDownloadOperation = it.next();
595 found = keyDownloadOperation.startsWith(targetKey);
596 if (found) {
597 keyItems.add(keyDownloadOperation);
598 }
599 }
600
601 for (String item: keyItems) {
602 download = mPendingDownloads.remove(item);
603 Log_OC.d(TAG, "Key removed: " + item);
604
605 if (download != null) {
606 download.cancel();
607 }
608 }
609
610 */
611
612 } else {
613 // this is not really expected...
614 Log_OC.d(TAG, "Canceling file download");
615 download = mPendingDownloads.remove(account, file.getRemotePath());
616 if (download != null) {
617 download.cancel();
618 }
619 }
620 }
621
622 }