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
.HashMap
; 
  25 import java
.util
.Iterator
; 
  27 import java
.util
.Vector
; 
  28 import java
.util
.concurrent
.ConcurrentHashMap
; 
  29 import java
.util
.concurrent
.ConcurrentMap
; 
  31 import com
.owncloud
.android
.R
; 
  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
.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 import com
.owncloud
.android
.utils
.Log_OC
; 
  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
; 
  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     private static final String DOWNLOAD_ADDED_MESSAGE 
= "DOWNLOAD_ADDED"; 
  74     private static final String DOWNLOAD_FINISH_MESSAGE 
= "DOWNLOAD_FINISH"; 
  75     public static final String EXTRA_DOWNLOAD_RESULT 
= "RESULT";     
  76     public static final String EXTRA_FILE_PATH 
= "FILE_PATH"; 
  77     public static final String EXTRA_REMOTE_PATH 
= "REMOTE_PATH"; 
  78     public static final String ACCOUNT_NAME 
= "ACCOUNT_NAME"; 
  80     private static final String TAG 
= "FileDownloader"; 
  82     private Looper mServiceLooper
; 
  83     private ServiceHandler mServiceHandler
; 
  84     private IBinder mBinder
; 
  85     private OwnCloudClient mDownloadClient 
= null
; 
  86     private Account mLastAccount 
= null
; 
  87     private FileDataStorageManager mStorageManager
; 
  89     private ConcurrentMap
<String
, DownloadFileOperation
> mPendingDownloads 
= new ConcurrentHashMap
<String
, DownloadFileOperation
>(); 
  90     private DownloadFileOperation mCurrentDownload 
= null
; 
  92     private NotificationManager mNotificationManager
; 
  93     private NotificationCompat
.Builder mNotificationBuilder
; 
  94     private int mLastPercent
; 
  97     public static String 
getDownloadAddedMessage() { 
  98         return FileDownloader
.class.getName().toString() + DOWNLOAD_ADDED_MESSAGE
; 
 101     public static String 
getDownloadFinishMessage() { 
 102         return FileDownloader
.class.getName().toString() + DOWNLOAD_FINISH_MESSAGE
; 
 106      * Builds a key for mPendingDownloads from the account and file to download 
 108      * @param account   Account where the file to download is stored 
 109      * @param file      File to download 
 111     private String 
buildRemoteName(Account account
, OCFile file
) { 
 112         return account
.name 
+ file
.getRemotePath(); 
 117      * Service initialization 
 120     public void onCreate() { 
 122         mNotificationManager 
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
); 
 123         HandlerThread thread 
= new HandlerThread("FileDownloaderThread", 
 124                 Process
.THREAD_PRIORITY_BACKGROUND
); 
 126         mServiceLooper 
= thread
.getLooper(); 
 127         mServiceHandler 
= new ServiceHandler(mServiceLooper
, this); 
 128         mBinder 
= new FileDownloaderBinder(); 
 132      * Entry point to add one or several files to the queue of downloads. 
 134      * New downloads are added calling to startService(), resulting in a call to this method. This ensures the service will keep on working  
 135      * although the caller activity goes away. 
 138     public int onStartCommand(Intent intent
, int flags
, int startId
) { 
 139         if (    !intent
.hasExtra(EXTRA_ACCOUNT
) || 
 140                 !intent
.hasExtra(EXTRA_FILE
) 
 141                 /*!intent.hasExtra(EXTRA_FILE_PATH) || 
 142                 !intent.hasExtra(EXTRA_REMOTE_PATH)*/ 
 144             Log_OC
.e(TAG
, "Not enough information provided in intent"); 
 145             return START_NOT_STICKY
; 
 147         Account account 
= intent
.getParcelableExtra(EXTRA_ACCOUNT
); 
 148         OCFile file 
= intent
.getParcelableExtra(EXTRA_FILE
); 
 150         AbstractList
<String
> requestedDownloads 
= new Vector
<String
>(); // dvelasco: now this always contains just one element, but that can change in a near future (download of multiple selection) 
 151         String downloadKey 
= buildRemoteName(account
, file
); 
 153             DownloadFileOperation newDownload 
= new DownloadFileOperation(account
, file
);  
 154             mPendingDownloads
.putIfAbsent(downloadKey
, newDownload
); 
 155             newDownload
.addDatatransferProgressListener(this); 
 156             newDownload
.addDatatransferProgressListener((FileDownloaderBinder
)mBinder
); 
 157             requestedDownloads
.add(downloadKey
); 
 158             sendBroadcastNewDownload(newDownload
); 
 160         } catch (IllegalArgumentException e
) { 
 161             Log_OC
.e(TAG
, "Not enough information provided in intent: " + e
.getMessage()); 
 162             return START_NOT_STICKY
; 
 165         if (requestedDownloads
.size() > 0) { 
 166             Message msg 
= mServiceHandler
.obtainMessage(); 
 168             msg
.obj 
= requestedDownloads
; 
 169             mServiceHandler
.sendMessage(msg
); 
 172         return START_NOT_STICKY
; 
 177      * Provides a binder object that clients can use to perform operations on the queue of downloads, excepting the addition of new files.  
 179      * Implemented to perform cancellation, pause and resume of existing downloads. 
 182     public IBinder 
onBind(Intent arg0
) { 
 188      * Called when ALL the bound clients were onbound. 
 191     public boolean onUnbind(Intent intent
) { 
 192         ((FileDownloaderBinder
)mBinder
).clearListeners(); 
 193         return false
;   // not accepting rebinding (default behaviour) 
 198      *  Binder to let client components to perform operations on the queue of downloads. 
 200      *  It provides by itself the available operations. 
 202     public class FileDownloaderBinder 
extends Binder 
implements OnDatatransferProgressListener 
{ 
 205          * Map of listeners that will be reported about progress of downloads from a {@link FileDownloaderBinder} instance  
 207         private Map
<String
, OnDatatransferProgressListener
> mBoundListeners 
= new HashMap
<String
, OnDatatransferProgressListener
>(); 
 211          * Cancels a pending or current download of a remote file. 
 213          * @param account       Owncloud account where the remote file is stored. 
 214          * @param file          A file in the queue of pending downloads 
 216         public void cancel(Account account
, OCFile file
) { 
 217             DownloadFileOperation download 
= null
; 
 218             synchronized (mPendingDownloads
) { 
 219                 download 
= mPendingDownloads
.remove(buildRemoteName(account
, file
)); 
 221             if (download 
!= null
) { 
 227         public void clearListeners() { 
 228             mBoundListeners
.clear(); 
 233          * Returns True when the file described by 'file' in the ownCloud account 'account' is downloading or waiting to download. 
 235          * If 'file' is a directory, returns 'true' if some of its descendant files is downloading or waiting to download.  
 237          * @param account       Owncloud account where the remote file is stored. 
 238          * @param file          A file that could be in the queue of downloads. 
 240         public boolean isDownloading(Account account
, OCFile file
) { 
 241             if (account 
== null 
|| file 
== null
) return false
; 
 242             String targetKey 
= buildRemoteName(account
, file
); 
 243             synchronized (mPendingDownloads
) { 
 244                 if (file
.isFolder()) { 
 245                     // this can be slow if there are many downloads :( 
 246                     Iterator
<String
> it 
= mPendingDownloads
.keySet().iterator(); 
 247                     boolean found 
= false
; 
 248                     while (it
.hasNext() && !found
) { 
 249                         found 
= it
.next().startsWith(targetKey
); 
 253                     return (mPendingDownloads
.containsKey(targetKey
)); 
 260          * Adds a listener interested in the progress of the download for a concrete file. 
 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.  
 266         public void addDatatransferProgressListener (OnDatatransferProgressListener listener
, Account account
, OCFile file
) { 
 267             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 268             String targetKey 
= buildRemoteName(account
, file
); 
 269             mBoundListeners
.put(targetKey
, listener
); 
 274          * Removes a listener interested in the progress of the download for a concrete file. 
 276          * @param listener      Object to notify about progress of transfer.     
 277          * @param account       ownCloud account holding the file of interest. 
 278          * @param file          {@link OCfile} of interest for listener.  
 280         public void removeDatatransferProgressListener (OnDatatransferProgressListener listener
, Account account
, OCFile file
) { 
 281             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 282             String targetKey 
= buildRemoteName(account
, file
); 
 283             if (mBoundListeners
.get(targetKey
) == listener
) { 
 284                 mBoundListeners
.remove(targetKey
); 
 289         public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, 
 291             String key 
= buildRemoteName(mCurrentDownload
.getAccount(), mCurrentDownload
.getFile()); 
 292             OnDatatransferProgressListener boundListener 
= mBoundListeners
.get(key
); 
 293             if (boundListener 
!= null
) { 
 294                 boundListener
.onTransferProgress(progressRate
, totalTransferredSoFar
, totalToTransfer
, fileName
); 
 302      * Download worker. Performs the pending downloads in the order they were requested.  
 304      * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.  
 306     private static class ServiceHandler 
extends Handler 
{ 
 307         // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak 
 308         FileDownloader mService
; 
 309         public ServiceHandler(Looper looper
, FileDownloader service
) { 
 312                 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); 
 317         public void handleMessage(Message msg
) { 
 318             @SuppressWarnings("unchecked") 
 319             AbstractList
<String
> requestedDownloads 
= (AbstractList
<String
>) msg
.obj
; 
 320             if (msg
.obj 
!= null
) { 
 321                 Iterator
<String
> it 
= requestedDownloads
.iterator(); 
 322                 while (it
.hasNext()) { 
 323                     mService
.downloadFile(it
.next()); 
 326             mService
.stopSelf(msg
.arg1
); 
 332      * Core download method: requests a file to download and stores it. 
 334      * @param downloadKey   Key to access the download to perform, contained in mPendingDownloads  
 336     private void downloadFile(String downloadKey
) { 
 338         synchronized(mPendingDownloads
) { 
 339             mCurrentDownload 
= mPendingDownloads
.get(downloadKey
); 
 342         if (mCurrentDownload 
!= null
) { 
 344             notifyDownloadStart(mCurrentDownload
); 
 346             RemoteOperationResult downloadResult 
= null
; 
 348                 /// prepare client object to send the request to the ownCloud server 
 349                 if (mDownloadClient 
== null 
|| !mLastAccount
.equals(mCurrentDownload
.getAccount())) { 
 350                     mLastAccount 
= mCurrentDownload
.getAccount(); 
 352                             new FileDataStorageManager(mLastAccount
, getContentResolver()); 
 353                     OwnCloudAccount ocAccount 
= new OwnCloudAccount(mLastAccount
, this); 
 354                     mDownloadClient 
= OwnCloudClientManagerFactory
.getDefaultSingleton(). 
 355                             getClientFor(ocAccount
, this); 
 358                 /// perform the download 
 359                 downloadResult 
= mCurrentDownload
.execute(mDownloadClient
); 
 360                 if (downloadResult
.isSuccess()) { 
 361                     saveDownloadedFile(); 
 364             } catch (AccountsException e
) { 
 365                 Log_OC
.e(TAG
, "Error while trying to get autorization for " + mLastAccount
.name
, e
); 
 366                 downloadResult 
= new RemoteOperationResult(e
); 
 367             } catch (IOException e
) { 
 368                 Log_OC
.e(TAG
, "Error while trying to get autorization for " + mLastAccount
.name
, e
); 
 369                 downloadResult 
= new RemoteOperationResult(e
); 
 372                 synchronized(mPendingDownloads
) { 
 373                     mPendingDownloads
.remove(downloadKey
); 
 379             notifyDownloadResult(mCurrentDownload
, downloadResult
); 
 381             sendBroadcastDownloadFinished(mCurrentDownload
, downloadResult
); 
 387      * Updates the OC File after a successful download. 
 389     private void saveDownloadedFile() { 
 390         OCFile file 
= mStorageManager
.getFileById(mCurrentDownload
.getFile().getFileId()); 
 391         long syncDate 
= System
.currentTimeMillis(); 
 392         file
.setLastSyncDateForProperties(syncDate
); 
 393         file
.setLastSyncDateForData(syncDate
); 
 394         file
.setModificationTimestamp(mCurrentDownload
.getModificationTimestamp()); 
 395         file
.setModificationTimestampAtLastSyncForData(mCurrentDownload
.getModificationTimestamp()); 
 396         // file.setEtag(mCurrentDownload.getEtag());    // TODO Etag, where available 
 397         file
.setMimetype(mCurrentDownload
.getMimeType()); 
 398         file
.setStoragePath(mCurrentDownload
.getSavePath()); 
 399         file
.setFileLength((new File(mCurrentDownload
.getSavePath()).length())); 
 400         mStorageManager
.saveFile(file
); 
 405      * Creates a status notification to show the download progress 
 407      * @param download  Download operation starting. 
 409     private void notifyDownloadStart(DownloadFileOperation download
) { 
 410         /// create status notification with a progress bar 
 412         mNotificationBuilder 
=  
 413                 NotificationBuilderWithProgressBar
.newNotificationBuilderWithProgressBar(this); 
 415                 .setSmallIcon(R
.drawable
.notification_icon
) 
 416                 .setTicker(getString(R
.string
.downloader_download_in_progress_ticker
)) 
 417                 .setContentTitle(getString(R
.string
.downloader_download_in_progress_ticker
)) 
 419                 .setProgress(100, 0, download
.getSize() < 0) 
 421                         String
.format(getString(R
.string
.downloader_download_in_progress_content
), 0, 
 422                                 new File(download
.getSavePath()).getName()) 
 425         /// includes a pending intent in the notification showing the details view of the file 
 426         Intent showDetailsIntent 
= null
; 
 427         if (PreviewImageFragment
.canBePreviewed(download
.getFile())) { 
 428             showDetailsIntent 
= new Intent(this, PreviewImageActivity
.class); 
 430             showDetailsIntent 
= new Intent(this, FileDisplayActivity
.class); 
 432         showDetailsIntent
.putExtra(FileActivity
.EXTRA_FILE
, download
.getFile()); 
 433         showDetailsIntent
.putExtra(FileActivity
.EXTRA_ACCOUNT
, download
.getAccount()); 
 434         showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 436         mNotificationBuilder
.setContentIntent(PendingIntent
.getActivity( 
 437             this, (int) System
.currentTimeMillis(), showDetailsIntent
, 0 
 440         mNotificationManager
.notify(R
.string
.downloader_download_in_progress_ticker
, mNotificationBuilder
.build()); 
 445      * Callback method to update the progress bar in the status notification. 
 448     public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, String filePath
) { 
 449         int percent 
= (int)(100.0*((double)totalTransferredSoFar
)/((double)totalToTransfer
)); 
 450         if (percent 
!= mLastPercent
) { 
 451             mNotificationBuilder
.setProgress(100, percent
, totalToTransfer 
< 0); 
 452             String fileName 
= filePath
.substring(filePath
.lastIndexOf(FileUtils
.PATH_SEPARATOR
) + 1); 
 453             String text 
= String
.format(getString(R
.string
.downloader_download_in_progress_content
), percent
, fileName
); 
 454             mNotificationBuilder
.setContentText(text
); 
 455             mNotificationManager
.notify(R
.string
.downloader_download_in_progress_ticker
, mNotificationBuilder
.build()); 
 457         mLastPercent 
= percent
; 
 462      * Updates the status notification with the result of a download operation. 
 464      * @param downloadResult    Result of the download operation. 
 465      * @param download          Finished download operation 
 467     private void notifyDownloadResult(DownloadFileOperation download
, RemoteOperationResult downloadResult
) { 
 468         mNotificationManager
.cancel(R
.string
.downloader_download_in_progress_ticker
); 
 469         if (!downloadResult
.isCancelled()) { 
 470             int tickerId 
= (downloadResult
.isSuccess()) ? R
.string
.downloader_download_succeeded_ticker 
:  
 471                 R
.string
.downloader_download_failed_ticker
; 
 473             boolean needsToUpdateCredentials 
= ( 
 474                     downloadResult
.getCode() == ResultCode
.UNAUTHORIZED 
|| 
 475                     downloadResult
.isIdPRedirection() 
 477             tickerId 
= (needsToUpdateCredentials
) ? 
 
 478                     R
.string
.downloader_download_failed_credentials_error 
: tickerId
; 
 481             .setTicker(getString(tickerId
)) 
 482             .setContentTitle(getString(tickerId
)) 
 485             .setProgress(0, 0, false
); 
 487             if (needsToUpdateCredentials
) { 
 489                 // let the user update credentials with one click 
 490                 Intent updateAccountCredentials 
= new Intent(this, AuthenticatorActivity
.class); 
 491                 updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACCOUNT
, download
.getAccount()); 
 492                 updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACTION
, AuthenticatorActivity
.ACTION_UPDATE_EXPIRED_TOKEN
); 
 493                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
); 
 494                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
); 
 495                 updateAccountCredentials
.addFlags(Intent
.FLAG_FROM_BACKGROUND
); 
 497                     .setContentIntent(PendingIntent
.getActivity( 
 498                         this, (int) System
.currentTimeMillis(), updateAccountCredentials
, PendingIntent
.FLAG_ONE_SHOT
)); 
 500                 mDownloadClient 
= null
;   // grant that future retries on the same account will get the fresh credentials 
 503                 // TODO put something smart in showDetailsIntent 
 504                 Intent   showDetailsIntent 
= new Intent(); 
 506                     .setContentIntent(PendingIntent
.getActivity( 
 507                         this, (int) System
.currentTimeMillis(), showDetailsIntent
, 0)); 
 510             mNotificationBuilder
.setContentText(ErrorMessageAdapter
.getErrorCauseMessage(downloadResult
, download
, getResources())); 
 511             mNotificationManager
.notify(tickerId
, mNotificationBuilder
.build()); 
 513             // Remove success notification 
 514             if (downloadResult
.isSuccess()) {    
 515                 // Sleep 2 seconds, so show the notification before remove it 
 516                 NotificationDelayer
.cancelWithDelay( 
 517                         mNotificationManager
,  
 518                         R
.string
.downloader_download_succeeded_ticker
,  
 527      * Sends a broadcast when a download finishes in order to the interested activities can update their view 
 529      * @param download          Finished download operation 
 530      * @param downloadResult    Result of the download operation 
 532     private void sendBroadcastDownloadFinished(DownloadFileOperation download
, RemoteOperationResult downloadResult
) { 
 533         Intent end 
= new Intent(getDownloadFinishMessage()); 
 534         end
.putExtra(EXTRA_DOWNLOAD_RESULT
, downloadResult
.isSuccess()); 
 535         end
.putExtra(ACCOUNT_NAME
, download
.getAccount().name
); 
 536         end
.putExtra(EXTRA_REMOTE_PATH
, download
.getRemotePath()); 
 537         end
.putExtra(EXTRA_FILE_PATH
, download
.getSavePath()); 
 538         sendStickyBroadcast(end
); 
 543      * Sends a broadcast when a new download is added to the queue. 
 545      * @param download          Added download operation 
 547     private void sendBroadcastNewDownload(DownloadFileOperation download
) { 
 548         Intent added 
= new Intent(getDownloadAddedMessage()); 
 549         added
.putExtra(ACCOUNT_NAME
, download
.getAccount().name
); 
 550         added
.putExtra(EXTRA_REMOTE_PATH
, download
.getRemotePath()); 
 551         added
.putExtra(EXTRA_FILE_PATH
, download
.getSavePath()); 
 552         sendStickyBroadcast(added
);