1 /* ownCloud Android client application 
   2  *   Copyright (C) 2012 Bartek Przybylski 
   3  *   Copyright (C) 2012-2015 ownCloud Inc. 
   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. 
   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. 
  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/>. 
  19 package com
.owncloud
.android
.files
.services
; 
  22 import java
.io
.IOException
; 
  23 import java
.util
.AbstractList
; 
  24 import java
.util
.HashMap
; 
  25 import java
.util
.Iterator
; 
  27 import java
.util
.Vector
; 
  28 import java
.util
.concurrent
.ConcurrentMap
; 
  30 import com
.owncloud
.android
.R
; 
  31 import com
.owncloud
.android
.authentication
.AccountUtils
; 
  32 import com
.owncloud
.android
.authentication
.AuthenticatorActivity
; 
  33 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  34 import com
.owncloud
.android
.datamodel
.OCFile
; 
  36 import com
.owncloud
.android
.lib
.common
.network
.OnDatatransferProgressListener
; 
  37 import com
.owncloud
.android
.lib
.common
.OwnCloudAccount
; 
  38 import com
.owncloud
.android
.lib
.common
.OwnCloudClient
; 
  39 import com
.owncloud
.android
.lib
.common
.OwnCloudClientManagerFactory
; 
  40 import com
.owncloud
.android
.notifications
.NotificationBuilderWithProgressBar
; 
  41 import com
.owncloud
.android
.notifications
.NotificationDelayer
; 
  42 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
; 
  43 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
.ResultCode
; 
  44 import com
.owncloud
.android
.lib
.common
.utils
.Log_OC
; 
  45 import com
.owncloud
.android
.lib
.resources
.files
.FileUtils
; 
  46 import com
.owncloud
.android
.operations
.DownloadFileOperation
; 
  47 import com
.owncloud
.android
.ui
.activity
.FileActivity
; 
  48 import com
.owncloud
.android
.ui
.activity
.FileDisplayActivity
; 
  49 import com
.owncloud
.android
.ui
.preview
.PreviewImageActivity
; 
  50 import com
.owncloud
.android
.ui
.preview
.PreviewImageFragment
; 
  51 import com
.owncloud
.android
.utils
.ErrorMessageAdapter
; 
  53 import android
.accounts
.Account
; 
  54 import android
.accounts
.AccountsException
; 
  55 import android
.app
.NotificationManager
; 
  56 import android
.app
.PendingIntent
; 
  57 import android
.app
.Service
; 
  58 import android
.content
.Intent
; 
  59 import android
.os
.Binder
; 
  60 import android
.os
.Handler
; 
  61 import android
.os
.HandlerThread
; 
  62 import android
.os
.IBinder
; 
  63 import android
.os
.Looper
; 
  64 import android
.os
.Message
; 
  65 import android
.os
.Process
; 
  66 import android
.support
.v4
.app
.NotificationCompat
; 
  67 import android
.util
.Pair
; 
  69 public class FileDownloader 
extends Service 
implements OnDatatransferProgressListener 
{ 
  71     public static final String EXTRA_ACCOUNT 
= "ACCOUNT"; 
  72     public static final String EXTRA_FILE 
= "FILE"; 
  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 EXTRA_LINKED_TO_PATH 
= "LINKED_TO"; 
  80     public static final String ACCOUNT_NAME 
= "ACCOUNT_NAME"; 
  82     private static final String TAG 
= "FileDownloader"; 
  84     private Looper mServiceLooper
; 
  85     private ServiceHandler mServiceHandler
; 
  86     private IBinder mBinder
; 
  87     private OwnCloudClient mDownloadClient 
= null
; 
  88     private Account mCurrentAccount 
= null
; 
  89     private FileDataStorageManager mStorageManager
; 
  91     private IndexedForest
<DownloadFileOperation
> mPendingDownloads 
= new IndexedForest
<DownloadFileOperation
>(); 
  93     private DownloadFileOperation mCurrentDownload 
= null
; 
  95     private NotificationManager mNotificationManager
; 
  96     private NotificationCompat
.Builder mNotificationBuilder
; 
  97     private int mLastPercent
; 
 100     public static String 
getDownloadAddedMessage() { 
 101         return FileDownloader
.class.getName() + DOWNLOAD_ADDED_MESSAGE
; 
 104     public static String 
getDownloadFinishMessage() { 
 105         return FileDownloader
.class.getName() + DOWNLOAD_FINISH_MESSAGE
; 
 109      * Service initialization 
 112     public void onCreate() { 
 114         Log_OC
.d(TAG
, "Creating service"); 
 115         mNotificationManager 
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
); 
 116         HandlerThread thread 
= new HandlerThread("FileDownloaderThread", Process
.THREAD_PRIORITY_BACKGROUND
); 
 118         mServiceLooper 
= thread
.getLooper(); 
 119         mServiceHandler 
= new ServiceHandler(mServiceLooper
, this); 
 120         mBinder 
= new FileDownloaderBinder(); 
 128     public void onDestroy() { 
 129         Log_OC
.v(TAG
, "Destroying service"); 
 131         mServiceHandler 
= null
; 
 132         mServiceLooper
.quit(); 
 133         mServiceLooper 
= null
; 
 134         mNotificationManager 
= null
; 
 140      * Entry point to add one or several files to the queue of downloads. 
 142      * New downloads are added calling to startService(), resulting in a call to this method. 
 143      * This ensures the service will keep on working although the caller activity goes away. 
 146     public int onStartCommand(Intent intent
, int flags
, int startId
) { 
 147         Log_OC
.d(TAG
, "Starting command with id " + startId
); 
 149         if (!intent
.hasExtra(EXTRA_ACCOUNT
) || 
 150                 !intent
.hasExtra(EXTRA_FILE
) 
 152             Log_OC
.e(TAG
, "Not enough information provided in intent"); 
 153             return START_NOT_STICKY
; 
 155             final Account account 
= intent
.getParcelableExtra(EXTRA_ACCOUNT
); 
 156             final OCFile file 
= intent
.getParcelableExtra(EXTRA_FILE
); 
 159                     "NOW " + TAG + ", thread " + Thread.currentThread().getName(), 
 160                     "Received request to download file" 
 163             AbstractList
<String
> requestedDownloads 
= new Vector
<String
>(); 
 165                 DownloadFileOperation newDownload 
= new DownloadFileOperation(account
, file
); 
 166                 newDownload
.addDatatransferProgressListener(this); 
 167                 newDownload
.addDatatransferProgressListener((FileDownloaderBinder
) mBinder
); 
 168                 Pair
<String
, String
> putResult 
= mPendingDownloads
.putIfAbsent( 
 169                         account
, file
.getRemotePath(), newDownload
 
 171                 String downloadKey 
= putResult
.first
; 
 172                 requestedDownloads
.add(downloadKey
); 
 174                         "NOW " + TAG + ", thread " + Thread.currentThread().getName(), 
 175                         "Download on " + file.getRemotePath() + " added to queue" 
 178                 // Store file on db with state 'downloading' 
 180                     TODO - check if helps with UI responsiveness, letting only folders use FileDownloaderBinder to check 
 181                     FileDataStorageManager storageManager = new FileDataStorageManager(account, getContentResolver()); 
 182                     file.setDownloading(true); 
 183                     storageManager.saveFile(file); 
 186                 sendBroadcastNewDownload(newDownload
, putResult
.second
); 
 188             } catch (IllegalArgumentException e
) { 
 189                 Log_OC
.e(TAG
, "Not enough information provided in intent: " + e
.getMessage()); 
 190                 return START_NOT_STICKY
; 
 193             if (requestedDownloads
.size() > 0) { 
 194                 Message msg 
= mServiceHandler
.obtainMessage(); 
 196                 msg
.obj 
= requestedDownloads
; 
 197                 mServiceHandler
.sendMessage(msg
); 
 202         return START_NOT_STICKY
; 
 207      * Provides a binder object that clients can use to perform operations on the queue of downloads, 
 208      * excepting the addition of new files. 
 210      * Implemented to perform cancellation, pause and resume of existing downloads. 
 213     public IBinder 
onBind(Intent arg0
) { 
 219      * Called when ALL the bound clients were onbound. 
 222     public boolean onUnbind(Intent intent
) { 
 223         ((FileDownloaderBinder
) mBinder
).clearListeners(); 
 224         return false
;   // not accepting rebinding (default behaviour) 
 229      * Binder to let client components to perform operations on the queue of downloads. 
 231      * It provides by itself the available operations. 
 233     public class FileDownloaderBinder 
extends Binder 
implements OnDatatransferProgressListener 
{ 
 236          * Map of listeners that will be reported about progress of downloads from a {@link FileDownloaderBinder} 
 239         private Map
<Long
, OnDatatransferProgressListener
> mBoundListeners 
= 
 240                 new HashMap
<Long
, OnDatatransferProgressListener
>(); 
 244          * Cancels a pending or current download of a remote file. 
 246          * @param account ownCloud account where the remote file is stored. 
 247          * @param file    A file in the queue of pending downloads 
 249         public void cancel(Account account
, OCFile file
) { 
 251                     "NOW " + TAG + ", thread " + Thread.currentThread().getName(), 
 252                     "Received request to cancel download of " + file.getRemotePath() 
 254             Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(), 
 255                     "Removing download of " + file.getRemotePath());*/ 
 256             Pair
<DownloadFileOperation
, String
> removeResult 
= 
 257                     mPendingDownloads
.remove(account
, file
.getRemotePath()); 
 258             DownloadFileOperation download 
= removeResult
.first
; 
 259             if (download 
!= null
) { 
 260                 /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(), 
 261                         "Canceling returned download of " + file.getRemotePath());*/ 
 264                 if (mCurrentDownload 
!= null 
&& mCurrentAccount 
!= null 
&& 
 265                         mCurrentDownload
.getRemotePath().startsWith(file
.getRemotePath()) && 
 266                         account
.name
.equals(mCurrentAccount
.name
)) { 
 267                     /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(), 
 268                             "Canceling current sync as descendant: " + mCurrentDownload.getRemotePath());*/ 
 269                     mCurrentDownload
.cancel(); 
 275          * Cancels a pending or current upload for an account 
 277          * @param account Owncloud accountName where the remote file will be stored. 
 279         public void cancel(Account account
) { 
 280             Log_OC
.d(TAG
, "Account= " + account
.name
); 
 282             if (mCurrentDownload 
!= null
) { 
 283                 Log_OC
.d(TAG
, "Current Download Account= " + mCurrentDownload
.getAccount().name
); 
 284                 if (mCurrentDownload
.getAccount().name
.equals(account
.name
)) { 
 285                     mCurrentDownload
.cancel(); 
 288             // Cancel pending downloads 
 289             cancelDownloadsForAccount(account
); 
 292         public void clearListeners() { 
 293             mBoundListeners
.clear(); 
 298          * Returns True when the file described by 'file' in the ownCloud account 'account' is downloading or 
 299          * waiting to download. 
 301          * If 'file' is a directory, returns 'true' if any of its descendant files is downloading or 
 302          * waiting to download. 
 304          * @param account ownCloud account where the remote file is stored. 
 305          * @param file    A file that could be in the queue of downloads. 
 307         public boolean isDownloading(Account account
, OCFile file
) { 
 308             if (account 
== null 
|| file 
== null
) return false
; 
 309             return (mPendingDownloads
.contains(account
, file
.getRemotePath())); 
 314          * Adds a listener interested in the progress of the download for a concrete file. 
 316          * @param listener Object to notify about progress of transfer. 
 317          * @param account  ownCloud account holding the file of interest. 
 318          * @param file     {@link OCFile} of interest for listener. 
 320         public void addDatatransferProgressListener( 
 321                 OnDatatransferProgressListener listener
, Account account
, OCFile file
 
 323             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 324             //String targetKey = buildKey(account, file.getRemotePath()); 
 325             mBoundListeners
.put(file
.getFileId(), listener
); 
 330          * Removes a listener interested in the progress of the download for a concrete file. 
 332          * @param listener Object to notify about progress of transfer. 
 333          * @param account  ownCloud account holding the file of interest. 
 334          * @param file     {@link OCFile} of interest for listener. 
 336         public void removeDatatransferProgressListener( 
 337                 OnDatatransferProgressListener listener
, Account account
, OCFile file
 
 339             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 340             //String targetKey = buildKey(account, file.getRemotePath()); 
 341             Long fileId 
= file
.getFileId(); 
 342             if (mBoundListeners
.get(fileId
) == listener
) { 
 343                 mBoundListeners
.remove(fileId
); 
 348         public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, 
 350             //String key = buildKey(mCurrentDownload.getAccount(), mCurrentDownload.getFile().getRemotePath()); 
 351             OnDatatransferProgressListener boundListener 
= mBoundListeners
.get(mCurrentDownload
.getFile().getFileId()); 
 352             if (boundListener 
!= null
) { 
 353                 boundListener
.onTransferProgress(progressRate
, totalTransferredSoFar
, totalToTransfer
, fileName
); 
 358          * Review downloads and cancel it if its account doesn't exist 
 360         public void checkAccountOfCurrentDownload() { 
 361             if (mCurrentDownload 
!= null 
&& 
 362                     !AccountUtils
.exists(mCurrentDownload
.getAccount(), getApplicationContext())) { 
 363                 mCurrentDownload
.cancel(); 
 365             // The rest of downloads are cancelled when they try to start 
 372      * Download worker. Performs the pending downloads in the order they were requested. 
 374      * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}. 
 376     private static class ServiceHandler 
extends Handler 
{ 
 377         // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak 
 378         FileDownloader mService
; 
 380         public ServiceHandler(Looper looper
, FileDownloader service
) { 
 383                 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); 
 388         public void handleMessage(Message msg
) { 
 389             @SuppressWarnings("unchecked") 
 390             AbstractList
<String
> requestedDownloads 
= (AbstractList
<String
>) msg
.obj
; 
 391             if (msg
.obj 
!= null
) { 
 392                 Iterator
<String
> it 
= requestedDownloads
.iterator(); 
 393                 while (it
.hasNext()) { 
 394                     String next 
= it
.next(); 
 395                     mService
.downloadFile(next
); 
 398             Log_OC
.d(TAG
, "Stopping after command with id " + msg
.arg1
); 
 399             mService
.stopSelf(msg
.arg1
); 
 405      * Core download method: requests a file to download and stores it. 
 407      * @param downloadKey Key to access the download to perform, contained in mPendingDownloads 
 409     private void downloadFile(String downloadKey
) { 
 411         /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(), 
 412                 "Getting download of " + downloadKey);*/ 
 413         mCurrentDownload 
= mPendingDownloads
.get(downloadKey
); 
 415         if (mCurrentDownload 
!= null
) { 
 416             // Detect if the account exists 
 417             if (AccountUtils
.exists(mCurrentDownload
.getAccount(), getApplicationContext())) { 
 418                 Log_OC
.d(TAG
, "Account " + mCurrentDownload
.getAccount().name 
+ " exists"); 
 419                 notifyDownloadStart(mCurrentDownload
); 
 421                 RemoteOperationResult downloadResult 
= null
; 
 423                     /// prepare client object to send the request to the ownCloud server 
 424                     if (mCurrentAccount 
== null 
|| !mCurrentAccount
.equals(mCurrentDownload
.getAccount())) { 
 425                         mCurrentAccount 
= mCurrentDownload
.getAccount(); 
 426                         mStorageManager 
= new FileDataStorageManager( 
 430                     }   // else, reuse storage manager from previous operation 
 432                     // always get client from client manager, to get fresh credentials in case of update 
 433                     OwnCloudAccount ocAccount 
= new OwnCloudAccount(mCurrentAccount
, this); 
 434                     mDownloadClient 
= OwnCloudClientManagerFactory
.getDefaultSingleton(). 
 435                             getClientFor(ocAccount
, this); 
 438                     /// perform the download 
 439                     /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(), 
 440                         "Executing download of " + mCurrentDownload.getRemotePath());*/ 
 441                     downloadResult 
= mCurrentDownload
.execute(mDownloadClient
); 
 442                     if (downloadResult
.isSuccess()) { 
 443                         saveDownloadedFile(); 
 446                 } catch (AccountsException e
) { 
 447                     Log_OC
.e(TAG
, "Error while trying to get authorization for " + mCurrentAccount
.name
, e
); 
 448                     downloadResult 
= new RemoteOperationResult(e
); 
 449                 } catch (IOException e
) { 
 450                     Log_OC
.e(TAG
, "Error while trying to get authorization for " + mCurrentAccount
.name
, e
); 
 451                     downloadResult 
= new RemoteOperationResult(e
); 
 454                 /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(), 
 455                         "Removing payload " + mCurrentDownload.getRemotePath());*/ 
 457                     Pair
<DownloadFileOperation
, String
> removeResult 
= 
 458                             mPendingDownloads
.removePayload(mCurrentAccount
, mCurrentDownload
.getRemotePath()); 
 461                     notifyDownloadResult(mCurrentDownload
, downloadResult
); 
 463                     sendBroadcastDownloadFinished(mCurrentDownload
, downloadResult
, removeResult
.second
); 
 466                 // Cancel the transfer 
 467                 Log_OC
.d(TAG
, "Account " + mCurrentDownload
.getAccount().toString() + " doesn't exist"); 
 468                 cancelDownloadsForAccount(mCurrentDownload
.getAccount()); 
 476      * Updates the OC File after a successful download. 
 478     private void saveDownloadedFile() { 
 479         OCFile file 
= mStorageManager
.getFileById(mCurrentDownload
.getFile().getFileId()); 
 480         long syncDate 
= System
.currentTimeMillis(); 
 481         file
.setLastSyncDateForProperties(syncDate
); 
 482         file
.setLastSyncDateForData(syncDate
); 
 483         file
.setNeedsUpdateThumbnail(true
); 
 484         file
.setModificationTimestamp(mCurrentDownload
.getModificationTimestamp()); 
 485         file
.setModificationTimestampAtLastSyncForData(mCurrentDownload
.getModificationTimestamp()); 
 486         // file.setEtag(mCurrentDownload.getEtag());    // TODO Etag, where available 
 487         file
.setMimetype(mCurrentDownload
.getMimeType()); 
 488         file
.setStoragePath(mCurrentDownload
.getSavePath()); 
 489         file
.setFileLength((new File(mCurrentDownload
.getSavePath()).length())); 
 490         file
.setRemoteId(mCurrentDownload
.getFile().getRemoteId()); 
 491         mStorageManager
.saveFile(file
); 
 492         mStorageManager
.triggerMediaScan(file
.getStoragePath()); 
 496      * Update the OC File after a unsuccessful download 
 498     private void updateUnsuccessfulDownloadedFile() { 
 499         OCFile file 
= mStorageManager
.getFileById(mCurrentDownload
.getFile().getFileId()); 
 500         file
.setDownloading(false
); 
 501         mStorageManager
.saveFile(file
); 
 506      * Creates a status notification to show the download progress 
 508      * @param download Download operation starting. 
 510     private void notifyDownloadStart(DownloadFileOperation download
) { 
 511         /// create status notification with a progress bar 
 513         mNotificationBuilder 
= 
 514                 NotificationBuilderWithProgressBar
.newNotificationBuilderWithProgressBar(this); 
 516                 .setSmallIcon(R
.drawable
.notification_icon
) 
 517                 .setTicker(getString(R
.string
.downloader_download_in_progress_ticker
)) 
 518                 .setContentTitle(getString(R
.string
.downloader_download_in_progress_ticker
)) 
 520                 .setProgress(100, 0, download
.getSize() < 0) 
 522                         String
.format(getString(R
.string
.downloader_download_in_progress_content
), 0, 
 523                                 new File(download
.getSavePath()).getName()) 
 526         /// includes a pending intent in the notification showing the details view of the file 
 527         Intent showDetailsIntent 
= null
; 
 528         if (PreviewImageFragment
.canBePreviewed(download
.getFile())) { 
 529             showDetailsIntent 
= new Intent(this, PreviewImageActivity
.class); 
 531             showDetailsIntent 
= new Intent(this, FileDisplayActivity
.class); 
 533         showDetailsIntent
.putExtra(FileActivity
.EXTRA_FILE
, download
.getFile()); 
 534         showDetailsIntent
.putExtra(FileActivity
.EXTRA_ACCOUNT
, download
.getAccount()); 
 535         showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 537         mNotificationBuilder
.setContentIntent(PendingIntent
.getActivity( 
 538                 this, (int) System
.currentTimeMillis(), showDetailsIntent
, 0 
 541         mNotificationManager
.notify(R
.string
.downloader_download_in_progress_ticker
, mNotificationBuilder
.build()); 
 546      * Callback method to update the progress bar in the status notification. 
 549     public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, String filePath
) { 
 550         int percent 
= (int) (100.0 * ((double) totalTransferredSoFar
) / ((double) totalToTransfer
)); 
 551         if (percent 
!= mLastPercent
) { 
 552             mNotificationBuilder
.setProgress(100, percent
, totalToTransfer 
< 0); 
 553             String fileName 
= filePath
.substring(filePath
.lastIndexOf(FileUtils
.PATH_SEPARATOR
) + 1); 
 554             String text 
= String
.format(getString(R
.string
.downloader_download_in_progress_content
), percent
, fileName
); 
 555             mNotificationBuilder
.setContentText(text
); 
 556             mNotificationManager
.notify(R
.string
.downloader_download_in_progress_ticker
, mNotificationBuilder
.build()); 
 558         mLastPercent 
= percent
; 
 563      * Updates the status notification with the result of a download operation. 
 565      * @param downloadResult Result of the download operation. 
 566      * @param download       Finished download operation 
 568     private void notifyDownloadResult(DownloadFileOperation download
, RemoteOperationResult downloadResult
) { 
 569         mNotificationManager
.cancel(R
.string
.downloader_download_in_progress_ticker
); 
 570         if (!downloadResult
.isCancelled()) { 
 571             int tickerId 
= (downloadResult
.isSuccess()) ? R
.string
.downloader_download_succeeded_ticker 
: 
 572                     R
.string
.downloader_download_failed_ticker
; 
 574             boolean needsToUpdateCredentials 
= ( 
 575                     downloadResult
.getCode() == ResultCode
.UNAUTHORIZED 
|| 
 576                             downloadResult
.isIdPRedirection() 
 578             tickerId 
= (needsToUpdateCredentials
) ?
 
 579                     R
.string
.downloader_download_failed_credentials_error 
: tickerId
; 
 582                     .setTicker(getString(tickerId
)) 
 583                     .setContentTitle(getString(tickerId
)) 
 586                     .setProgress(0, 0, false
); 
 588             if (needsToUpdateCredentials
) { 
 590                 // let the user update credentials with one click 
 591                 Intent updateAccountCredentials 
= new Intent(this, AuthenticatorActivity
.class); 
 592                 updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACCOUNT
, download
.getAccount()); 
 593                 updateAccountCredentials
.putExtra( 
 594                         AuthenticatorActivity
.EXTRA_ACTION
, AuthenticatorActivity
.ACTION_UPDATE_EXPIRED_TOKEN
 
 596                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
); 
 597                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
); 
 598                 updateAccountCredentials
.addFlags(Intent
.FLAG_FROM_BACKGROUND
); 
 600                         .setContentIntent(PendingIntent
.getActivity( 
 601                                 this, (int) System
.currentTimeMillis(), updateAccountCredentials
, PendingIntent
.FLAG_ONE_SHOT
)); 
 604                 // TODO put something smart in showDetailsIntent 
 605                 Intent showDetailsIntent 
= new Intent(); 
 607                         .setContentIntent(PendingIntent
.getActivity( 
 608                                 this, (int) System
.currentTimeMillis(), showDetailsIntent
, 0)); 
 611             mNotificationBuilder
.setContentText( 
 612                     ErrorMessageAdapter
.getErrorCauseMessage(downloadResult
, download
, getResources()) 
 614             mNotificationManager
.notify(tickerId
, mNotificationBuilder
.build()); 
 616             // Remove success notification 
 617             if (downloadResult
.isSuccess()) { 
 618                 // Sleep 2 seconds, so show the notification before remove it 
 619                 NotificationDelayer
.cancelWithDelay( 
 620                         mNotificationManager
, 
 621                         R
.string
.downloader_download_succeeded_ticker
, 
 630      * Sends a broadcast when a download finishes in order to the interested activities can update their view 
 632      * @param download               Finished download operation 
 633      * @param downloadResult         Result of the download operation 
 634      * @param unlinkedFromRemotePath Path in the downloads tree where the download was unlinked from 
 636     private void sendBroadcastDownloadFinished( 
 637             DownloadFileOperation download
, 
 638             RemoteOperationResult downloadResult
, 
 639             String unlinkedFromRemotePath
) { 
 640         Intent end 
= new Intent(getDownloadFinishMessage()); 
 641         end
.putExtra(EXTRA_DOWNLOAD_RESULT
, downloadResult
.isSuccess()); 
 642         end
.putExtra(ACCOUNT_NAME
, download
.getAccount().name
); 
 643         end
.putExtra(EXTRA_REMOTE_PATH
, download
.getRemotePath()); 
 644         end
.putExtra(EXTRA_FILE_PATH
, download
.getSavePath()); 
 645         if (unlinkedFromRemotePath 
!= null
) { 
 646             end
.putExtra(EXTRA_LINKED_TO_PATH
, unlinkedFromRemotePath
); 
 648         sendStickyBroadcast(end
); 
 653      * Sends a broadcast when a new download is added to the queue. 
 655      * @param download           Added download operation 
 656      * @param linkedToRemotePath Path in the downloads tree where the download was linked to 
 658     private void sendBroadcastNewDownload(DownloadFileOperation download
, String linkedToRemotePath
) { 
 659         Intent added 
= new Intent(getDownloadAddedMessage()); 
 660         added
.putExtra(ACCOUNT_NAME
, download
.getAccount().name
); 
 661         added
.putExtra(EXTRA_REMOTE_PATH
, download
.getRemotePath()); 
 662         added
.putExtra(EXTRA_FILE_PATH
, download
.getSavePath()); 
 663         added
.putExtra(EXTRA_LINKED_TO_PATH
, linkedToRemotePath
); 
 664         sendStickyBroadcast(added
); 
 668      * Remove downloads of an account 
 672     private void cancelDownloadsForAccount(Account account
) { 
 673         // Cancel pending downloads 
 674         ConcurrentMap downloadsAccount 
= mPendingDownloads
.get(account
); 
 675         Iterator
<String
> it 
= downloadsAccount
.keySet().iterator(); 
 676         Log_OC
.d(TAG
, "Number of pending downloads= " + downloadsAccount
.size()); 
 677         DownloadFileOperation download
; 
 678         while (it
.hasNext()) { 
 679             String key 
= it
.next(); 
 680             Log_OC
.d(TAG
, "download CANCELLED " + key
); 
 681             if (key
.startsWith(account
.name
)) { 
 682                 synchronized (mPendingDownloads
) { 
 683                     download 
= mPendingDownloads
.get(key
); 
 684                     if (download 
!= null
) { 
 685                         String remotePath 
= download
.getRemotePath(); 
 686                         mPendingDownloads
.remove(account
, remotePath
);