1 /* ownCloud Android client application 
   2  *   Copyright (C) 2012 Bartek Przybylski 
   3  *   Copyright (C) 2012-2013 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
.ArrayList
; 
  25 import java
.util
.HashMap
; 
  26 import java
.util
.Iterator
; 
  28 import java
.util
.Vector
; 
  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
; 
  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
; 
  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 import android
.util
.Pair
; 
  68 public class FileDownloader 
extends Service 
implements OnDatatransferProgressListener 
{ 
  70     public static final String EXTRA_ACCOUNT 
= "ACCOUNT"; 
  71     public static final String EXTRA_FILE 
= "FILE"; 
  73     public static final String ACTION_CANCEL_FILE_DOWNLOAD 
= "CANCEL_FILE_DOWNLOAD"; 
  75     private static final String DOWNLOAD_ADDED_MESSAGE 
= "DOWNLOAD_ADDED"; 
  76     private static final String DOWNLOAD_FINISH_MESSAGE 
= "DOWNLOAD_FINISH"; 
  77     public static final String EXTRA_DOWNLOAD_RESULT 
= "RESULT";     
  78     public static final String EXTRA_FILE_PATH 
= "FILE_PATH"; 
  79     public static final String EXTRA_REMOTE_PATH 
= "REMOTE_PATH"; 
  80     public static final String EXTRA_LINKED_TO_PATH 
= "LINKED_TO"; 
  81     public static final String ACCOUNT_NAME 
= "ACCOUNT_NAME"; 
  83     private static final String TAG 
= "FileDownloader"; 
  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
; 
  92     private IndexedForest
<DownloadFileOperation
> mPendingDownloads 
= new IndexedForest
<DownloadFileOperation
>(); 
  94     private DownloadFileOperation mCurrentDownload 
= null
; 
  96     private NotificationManager mNotificationManager
; 
  97     private NotificationCompat
.Builder mNotificationBuilder
; 
  98     private int mLastPercent
; 
 101     public static String 
getDownloadAddedMessage() { 
 102         return FileDownloader
.class.getName() + DOWNLOAD_ADDED_MESSAGE
; 
 105     public static String 
getDownloadFinishMessage() { 
 106         return FileDownloader
.class.getName() + DOWNLOAD_FINISH_MESSAGE
; 
 110      * Service initialization 
 113     public void onCreate() { 
 115         mNotificationManager 
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
); 
 116         HandlerThread thread 
= new HandlerThread("FileDownloaderThread", 
 117                 Process
.THREAD_PRIORITY_BACKGROUND
); 
 119         mServiceLooper 
= thread
.getLooper(); 
 120         mServiceHandler 
= new ServiceHandler(mServiceLooper
, this); 
 121         mBinder 
= new FileDownloaderBinder(); 
 125      * Entry point to add one or several files to the queue of downloads. 
 127      * New downloads are added calling to startService(), resulting in a call to this method. 
 128      * This ensures the service will keep on working although the caller activity goes away. 
 131     public int onStartCommand(Intent intent
, int flags
, int startId
) { 
 132         if (    !intent
.hasExtra(EXTRA_ACCOUNT
) || 
 133                 !intent
.hasExtra(EXTRA_FILE
) 
 135             Log_OC
.e(TAG
, "Not enough information provided in intent"); 
 136             return START_NOT_STICKY
; 
 138             final Account account 
= intent
.getParcelableExtra(EXTRA_ACCOUNT
); 
 139             final OCFile file 
= intent
.getParcelableExtra(EXTRA_FILE
); 
 142             if (ACTION_CANCEL_FILE_DOWNLOAD.equals(intent.getAction())) { 
 144                 new Thread(new Runnable() { 
 146                         // Cancel the download 
 147                         cancel(account, file); 
 154                 AbstractList
<String
> requestedDownloads 
= new Vector
<String
>(); 
 156                     DownloadFileOperation newDownload 
= new DownloadFileOperation(account
, file
); 
 157                     Pair
<String
, String
> putResult 
= mPendingDownloads
.putIfAbsent( 
 158                         account
, file
.getRemotePath(), newDownload
 
 160                     String downloadKey 
= putResult
.first
; 
 161                     newDownload
.addDatatransferProgressListener(this); 
 162                     newDownload
.addDatatransferProgressListener((FileDownloaderBinder
) mBinder
); 
 163                     requestedDownloads
.add(downloadKey
); 
 165                     // Store file on db with state 'downloading' 
 167                     TODO - check if helps with UI responsiveness, letting only folders use FileDownloaderBinder to check 
 168                     FileDataStorageManager storageManager = new FileDataStorageManager(account, getContentResolver()); 
 169                     file.setDownloading(true); 
 170                     storageManager.saveFile(file); 
 173                     sendBroadcastNewDownload(newDownload
, putResult
.second
); 
 175                 } catch (IllegalArgumentException e
) { 
 176                     Log_OC
.e(TAG
, "Not enough information provided in intent: " + e
.getMessage()); 
 177                     return START_NOT_STICKY
; 
 180                 if (requestedDownloads
.size() > 0) { 
 181                     Message msg 
= mServiceHandler
.obtainMessage(); 
 183                     msg
.obj 
= requestedDownloads
; 
 184                     mServiceHandler
.sendMessage(msg
); 
 189         return START_NOT_STICKY
; 
 194      * Provides a binder object that clients can use to perform operations on the queue of downloads, 
 195      * excepting the addition of new files. 
 197      * Implemented to perform cancellation, pause and resume of existing downloads. 
 200     public IBinder 
onBind(Intent arg0
) { 
 206      * Called when ALL the bound clients were onbound. 
 209     public boolean onUnbind(Intent intent
) { 
 210         ((FileDownloaderBinder
)mBinder
).clearListeners(); 
 211         return false
;   // not accepting rebinding (default behaviour) 
 216      *  Binder to let client components to perform operations on the queue of downloads. 
 218      *  It provides by itself the available operations. 
 220     public class FileDownloaderBinder 
extends Binder 
implements OnDatatransferProgressListener 
{ 
 223          * Map of listeners that will be reported about progress of downloads from a {@link FileDownloaderBinder} 
 226         private Map
<Long
, OnDatatransferProgressListener
> mBoundListeners 
= 
 227                 new HashMap
<Long
, OnDatatransferProgressListener
>(); 
 231          * Cancels a pending or current download of a remote file. 
 233          * @param account       ownCloud account where the remote file is stored. 
 234          * @param file          A file in the queue of pending downloads 
 236         public void cancel(Account account
, OCFile file
) { 
 237             Pair
<DownloadFileOperation
, String
> removeResult 
= mPendingDownloads
.remove(account
, file
.getRemotePath()); 
 238             DownloadFileOperation download 
= removeResult
.first
; 
 239             if (download 
!= null
) { 
 243                 if (mCurrentDownload
.getRemotePath().startsWith(file
.getRemotePath()) && 
 244                         account
.name
.equals(mLastAccount
.name
)) { 
 245                     mCurrentDownload
.cancel(); 
 251         public void clearListeners() { 
 252             mBoundListeners
.clear(); 
 257          * Returns True when the file described by 'file' in the ownCloud account 'account' is downloading or 
 258          * waiting to download. 
 260          * If 'file' is a directory, returns 'true' if any of its descendant files is downloading or 
 261          * waiting to download. 
 263          * @param account       ownCloud account where the remote file is stored. 
 264          * @param file          A file that could be in the queue of downloads. 
 266         public boolean isDownloading(Account account
, OCFile file
) { 
 267             if (account 
== null 
|| file 
== null
) return false
; 
 268             return (mPendingDownloads
.contains(account
, file
.getRemotePath())); 
 273          * Adds a listener interested in the progress of the download for a concrete file. 
 275          * @param listener      Object to notify about progress of transfer.     
 276          * @param account       ownCloud account holding the file of interest. 
 277          * @param file          {@link OCFile} of interest for listener. 
 279         public void addDatatransferProgressListener ( 
 280                 OnDatatransferProgressListener listener
, Account account
, OCFile file
 
 282             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 283             //String targetKey = buildKey(account, file.getRemotePath()); 
 284             mBoundListeners
.put(file
.getFileId(), listener
); 
 289          * Removes a listener interested in the progress of the download for a concrete file. 
 291          * @param listener      Object to notify about progress of transfer.     
 292          * @param account       ownCloud account holding the file of interest. 
 293          * @param file          {@link OCFile} of interest for listener. 
 295         public void removeDatatransferProgressListener ( 
 296                 OnDatatransferProgressListener listener
, Account account
, OCFile file
 
 298             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 299             //String targetKey = buildKey(account, file.getRemotePath()); 
 300             Long fileId 
= file
.getFileId(); 
 301             if (mBoundListeners
.get(fileId
) == listener
) { 
 302                 mBoundListeners
.remove(fileId
); 
 307         public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, 
 309             //String key = buildKey(mCurrentDownload.getAccount(), mCurrentDownload.getFile().getRemotePath()); 
 310             OnDatatransferProgressListener boundListener 
= mBoundListeners
.get(mCurrentDownload
.getFile().getFileId()); 
 311             if (boundListener 
!= null
) { 
 312                 boundListener
.onTransferProgress(progressRate
, totalTransferredSoFar
, totalToTransfer
, fileName
); 
 320      * Download worker. Performs the pending downloads in the order they were requested.  
 322      * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.  
 324     private static class ServiceHandler 
extends Handler 
{ 
 325         // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak 
 326         FileDownloader mService
; 
 327         public ServiceHandler(Looper looper
, FileDownloader service
) { 
 330                 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); 
 335         public void handleMessage(Message msg
) { 
 336             @SuppressWarnings("unchecked") 
 337             AbstractList
<String
> requestedDownloads 
= (AbstractList
<String
>) msg
.obj
; 
 338             if (msg
.obj 
!= null
) { 
 339                 Iterator
<String
> it 
= requestedDownloads
.iterator(); 
 340                 while (it
.hasNext()) { 
 341                     mService
.downloadFile(it
.next()); 
 344             mService
.stopSelf(msg
.arg1
); 
 350      * Core download method: requests a file to download and stores it. 
 352      * @param downloadKey   Key to access the download to perform, contained in mPendingDownloads  
 354     private void downloadFile(String downloadKey
) { 
 356         mCurrentDownload 
= mPendingDownloads
.get(downloadKey
); 
 358         if (mCurrentDownload 
!= null
) { 
 360             notifyDownloadStart(mCurrentDownload
); 
 362             RemoteOperationResult downloadResult 
= null
; 
 364                 /// prepare client object to send the request to the ownCloud server 
 365                 if (mDownloadClient 
== null 
|| !mLastAccount
.equals(mCurrentDownload
.getAccount())) { 
 366                     mLastAccount 
= mCurrentDownload
.getAccount(); 
 368                             new FileDataStorageManager(mLastAccount
, getContentResolver()); 
 369                     OwnCloudAccount ocAccount 
= new OwnCloudAccount(mLastAccount
, this); 
 370                     mDownloadClient 
= OwnCloudClientManagerFactory
.getDefaultSingleton(). 
 371                             getClientFor(ocAccount
, this); 
 374                 /// perform the download 
 375                 downloadResult 
= mCurrentDownload
.execute(mDownloadClient
); 
 376                 if (downloadResult
.isSuccess()) { 
 377                     saveDownloadedFile(); 
 379                     updateUnsuccessfulDownloadedFile(); 
 383             } catch (AccountsException e
) { 
 384                 Log_OC
.e(TAG
, "Error while trying to get authorization for " + mLastAccount
.name
, e
); 
 385                 downloadResult 
= new RemoteOperationResult(e
); 
 386             } catch (IOException e
) { 
 387                 Log_OC
.e(TAG
, "Error while trying to get authorization for " + mLastAccount
.name
, e
); 
 388                 downloadResult 
= new RemoteOperationResult(e
); 
 391                 Pair
<DownloadFileOperation
, String
> removeResult 
= 
 392                         mPendingDownloads
.remove(mLastAccount
, mCurrentDownload
.getRemotePath()); 
 395                 notifyDownloadResult(mCurrentDownload
, downloadResult
); 
 397                 sendBroadcastDownloadFinished(mCurrentDownload
, downloadResult
, removeResult
.second
); 
 405      * Updates the OC File after a successful download. 
 407     private void saveDownloadedFile() { 
 408         OCFile file 
= mStorageManager
.getFileById(mCurrentDownload
.getFile().getFileId()); 
 409         long syncDate 
= System
.currentTimeMillis(); 
 410         file
.setLastSyncDateForProperties(syncDate
); 
 411         file
.setLastSyncDateForData(syncDate
); 
 412         file
.setNeedsUpdateThumbnail(true
); 
 413         file
.setModificationTimestamp(mCurrentDownload
.getModificationTimestamp()); 
 414         file
.setModificationTimestampAtLastSyncForData(mCurrentDownload
.getModificationTimestamp()); 
 415         // file.setEtag(mCurrentDownload.getEtag());    // TODO Etag, where available 
 416         file
.setMimetype(mCurrentDownload
.getMimeType()); 
 417         file
.setStoragePath(mCurrentDownload
.getSavePath()); 
 418         file
.setFileLength((new File(mCurrentDownload
.getSavePath()).length())); 
 419         file
.setRemoteId(mCurrentDownload
.getFile().getRemoteId()); 
 420         //file.setDownloading(false); 
 421         mStorageManager
.saveFile(file
); 
 422         mStorageManager
.triggerMediaScan(file
.getStoragePath()); 
 426      * Update the OC File after a unsuccessful download 
 428     private void updateUnsuccessfulDownloadedFile() { 
 429         OCFile file 
= mStorageManager
.getFileById(mCurrentDownload
.getFile().getFileId()); 
 430         file
.setDownloading(false
); 
 431         mStorageManager
.saveFile(file
); 
 436      * Creates a status notification to show the download progress 
 438      * @param download  Download operation starting. 
 440     private void notifyDownloadStart(DownloadFileOperation download
) { 
 441         /// create status notification with a progress bar 
 443         mNotificationBuilder 
=  
 444                 NotificationBuilderWithProgressBar
.newNotificationBuilderWithProgressBar(this); 
 446                 .setSmallIcon(R
.drawable
.notification_icon
) 
 447                 .setTicker(getString(R
.string
.downloader_download_in_progress_ticker
)) 
 448                 .setContentTitle(getString(R
.string
.downloader_download_in_progress_ticker
)) 
 450                 .setProgress(100, 0, download
.getSize() < 0) 
 452                         String
.format(getString(R
.string
.downloader_download_in_progress_content
), 0, 
 453                                 new File(download
.getSavePath()).getName()) 
 456         /// includes a pending intent in the notification showing the details view of the file 
 457         Intent showDetailsIntent 
= null
; 
 458         if (PreviewImageFragment
.canBePreviewed(download
.getFile())) { 
 459             showDetailsIntent 
= new Intent(this, PreviewImageActivity
.class); 
 461             showDetailsIntent 
= new Intent(this, FileDisplayActivity
.class); 
 463         showDetailsIntent
.putExtra(FileActivity
.EXTRA_FILE
, download
.getFile()); 
 464         showDetailsIntent
.putExtra(FileActivity
.EXTRA_ACCOUNT
, download
.getAccount()); 
 465         showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 467         mNotificationBuilder
.setContentIntent(PendingIntent
.getActivity( 
 468             this, (int) System
.currentTimeMillis(), showDetailsIntent
, 0 
 471         mNotificationManager
.notify(R
.string
.downloader_download_in_progress_ticker
, mNotificationBuilder
.build()); 
 476      * Callback method to update the progress bar in the status notification. 
 479     public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, String filePath
) 
 481         int percent 
= (int)(100.0*((double)totalTransferredSoFar
)/((double)totalToTransfer
)); 
 482         if (percent 
!= mLastPercent
) { 
 483             mNotificationBuilder
.setProgress(100, percent
, totalToTransfer 
< 0); 
 484             String fileName 
= filePath
.substring(filePath
.lastIndexOf(FileUtils
.PATH_SEPARATOR
) + 1); 
 485             String text 
= String
.format(getString(R
.string
.downloader_download_in_progress_content
), percent
, fileName
); 
 486             mNotificationBuilder
.setContentText(text
); 
 487             mNotificationManager
.notify(R
.string
.downloader_download_in_progress_ticker
, mNotificationBuilder
.build()); 
 489         mLastPercent 
= percent
; 
 494      * Updates the status notification with the result of a download operation. 
 496      * @param downloadResult    Result of the download operation. 
 497      * @param download          Finished download operation 
 499     private void notifyDownloadResult(DownloadFileOperation download
, RemoteOperationResult downloadResult
) { 
 500         mNotificationManager
.cancel(R
.string
.downloader_download_in_progress_ticker
); 
 501         if (!downloadResult
.isCancelled()) { 
 502             int tickerId 
= (downloadResult
.isSuccess()) ? R
.string
.downloader_download_succeeded_ticker 
:  
 503                 R
.string
.downloader_download_failed_ticker
; 
 505             boolean needsToUpdateCredentials 
= ( 
 506                     downloadResult
.getCode() == ResultCode
.UNAUTHORIZED 
|| 
 507                     downloadResult
.isIdPRedirection() 
 509             tickerId 
= (needsToUpdateCredentials
) ? 
 
 510                     R
.string
.downloader_download_failed_credentials_error 
: tickerId
; 
 513             .setTicker(getString(tickerId
)) 
 514             .setContentTitle(getString(tickerId
)) 
 517             .setProgress(0, 0, false
); 
 519             if (needsToUpdateCredentials
) { 
 521                 // let the user update credentials with one click 
 522                 Intent updateAccountCredentials 
= new Intent(this, AuthenticatorActivity
.class); 
 523                 updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACCOUNT
, download
.getAccount()); 
 524                 updateAccountCredentials
.putExtra( 
 525                         AuthenticatorActivity
.EXTRA_ACTION
, AuthenticatorActivity
.ACTION_UPDATE_EXPIRED_TOKEN
 
 527                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
); 
 528                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
); 
 529                 updateAccountCredentials
.addFlags(Intent
.FLAG_FROM_BACKGROUND
); 
 531                     .setContentIntent(PendingIntent
.getActivity( 
 532                         this, (int) System
.currentTimeMillis(), updateAccountCredentials
, PendingIntent
.FLAG_ONE_SHOT
)); 
 534                 mDownloadClient 
= null
;  // grant that future retries on the same account will get the fresh credentials 
 537                 // TODO put something smart in showDetailsIntent 
 538                 Intent   showDetailsIntent 
= new Intent(); 
 540                     .setContentIntent(PendingIntent
.getActivity( 
 541                         this, (int) System
.currentTimeMillis(), showDetailsIntent
, 0)); 
 544             mNotificationBuilder
.setContentText( 
 545                     ErrorMessageAdapter
.getErrorCauseMessage(downloadResult
, download
, getResources()) 
 547             mNotificationManager
.notify(tickerId
, mNotificationBuilder
.build()); 
 549             // Remove success notification 
 550             if (downloadResult
.isSuccess()) {    
 551                 // Sleep 2 seconds, so show the notification before remove it 
 552                 NotificationDelayer
.cancelWithDelay( 
 553                         mNotificationManager
,  
 554                         R
.string
.downloader_download_succeeded_ticker
,  
 563      * Sends a broadcast when a download finishes in order to the interested activities can update their view 
 565      * @param download                  Finished download operation 
 566      * @param downloadResult            Result of the download operation 
 567      * @param unlinkedFromRemotePath    Path in the downloads tree where the download was unlinked from 
 569     private void sendBroadcastDownloadFinished( 
 570             DownloadFileOperation download
, 
 571             RemoteOperationResult downloadResult
, 
 572             String unlinkedFromRemotePath
) { 
 573         Intent end 
= new Intent(getDownloadFinishMessage()); 
 574         end
.putExtra(EXTRA_DOWNLOAD_RESULT
, downloadResult
.isSuccess()); 
 575         end
.putExtra(ACCOUNT_NAME
, download
.getAccount().name
); 
 576         end
.putExtra(EXTRA_REMOTE_PATH
, download
.getRemotePath()); 
 577         end
.putExtra(EXTRA_FILE_PATH
, download
.getSavePath()); 
 578         if (unlinkedFromRemotePath 
!= null
) { 
 579             end
.putExtra(EXTRA_LINKED_TO_PATH
, unlinkedFromRemotePath
); 
 581         sendStickyBroadcast(end
); 
 586      * Sends a broadcast when a new download is added to the queue. 
 588      * @param download              Added download operation 
 589      * @param linkedToRemotePath    Path in the downloads tree where the download was linked to 
 591     private void sendBroadcastNewDownload(DownloadFileOperation download
, String linkedToRemotePath
) { 
 592         Intent added 
= new Intent(getDownloadAddedMessage()); 
 593         added
.putExtra(ACCOUNT_NAME
, download
.getAccount().name
); 
 594         added
.putExtra(EXTRA_REMOTE_PATH
, download
.getRemotePath()); 
 595         added
.putExtra(EXTRA_FILE_PATH
, download
.getSavePath()); 
 596         added
.putExtra(EXTRA_LINKED_TO_PATH
, linkedToRemotePath
); 
 597         sendStickyBroadcast(added
); 
 602      * @param account       ownCloud account where the remote file is stored. 
 603      * @param file          File OCFile 
 605     public void cancel(Account account, OCFile file){ 
 606         DownloadFileOperation download = null; 
 607         //String targetKey = buildKey(account, file.getRemotePath()); 
 608         ArrayList<String> keyItems = new ArrayList<String>(); 
 609         if (file.isFolder()) { 
 610             Log_OC.d(TAG, "Folder download. Canceling pending downloads (from folder)"); 
 614             Iterator<String> it = mPendingDownloads.keySet().iterator(); 
 615             boolean found = false; 
 616             while (it.hasNext()) { 
 617                 String keyDownloadOperation = it.next(); 
 618                 found = keyDownloadOperation.startsWith(targetKey); 
 620                     keyItems.add(keyDownloadOperation); 
 624             for (String item: keyItems) { 
 625                 download = mPendingDownloads.remove(item); 
 626                 Log_OC.d(TAG, "Key removed: " + item); 
 628                 if (download != null) { 
 636             // this is not really expected... 
 637             Log_OC.d(TAG, "Canceling file download"); 
 638             download = mPendingDownloads.remove(account, file.getRemotePath()); 
 639             if (download != null) {