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
.util
.AbstractList
; 
  23 import java
.util
.HashMap
; 
  24 import java
.util
.Iterator
; 
  26 import java
.util
.Vector
; 
  27 import java
.util
.concurrent
.ConcurrentHashMap
; 
  28 import java
.util
.concurrent
.ConcurrentMap
; 
  30 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  31 import com
.owncloud
.android
.datamodel
.OCFile
; 
  32 import eu
.alefzero
.webdav
.OnDatatransferProgressListener
; 
  34 import com
.owncloud
.android
.network
.OwnCloudClientUtils
; 
  35 import com
.owncloud
.android
.operations
.DownloadFileOperation
; 
  36 import com
.owncloud
.android
.operations
.RemoteOperationResult
; 
  37 import com
.owncloud
.android
.ui
.activity
.FileDetailActivity
; 
  38 import com
.owncloud
.android
.ui
.fragment
.FileDetailFragment
; 
  39 import com
.owncloud
.android
.ui
.preview
.PreviewImageActivity
; 
  40 import com
.owncloud
.android
.ui
.preview
.PreviewImageFragment
; 
  42 import android
.accounts
.Account
; 
  43 import android
.app
.Notification
; 
  44 import android
.app
.NotificationManager
; 
  45 import android
.app
.PendingIntent
; 
  46 import android
.app
.Service
; 
  47 import android
.content
.Intent
; 
  48 import android
.os
.Binder
; 
  49 import android
.os
.Handler
; 
  50 import android
.os
.HandlerThread
; 
  51 import android
.os
.IBinder
; 
  52 import android
.os
.Looper
; 
  53 import android
.os
.Message
; 
  54 import android
.os
.Process
; 
  55 import android
.widget
.RemoteViews
; 
  57 import com
.owncloud
.android
.Log_OC
; 
  58 import com
.owncloud
.android
.R
; 
  59 import eu
.alefzero
.webdav
.WebdavClient
; 
  61 public class FileDownloader 
extends Service 
implements OnDatatransferProgressListener 
{ 
  63     public static final String EXTRA_ACCOUNT 
= "ACCOUNT"; 
  64     public static final String EXTRA_FILE 
= "FILE"; 
  66     public static final String DOWNLOAD_ADDED_MESSAGE 
= "DOWNLOAD_ADDED"; 
  67     public static final String DOWNLOAD_FINISH_MESSAGE 
= "DOWNLOAD_FINISH"; 
  68     public static final String EXTRA_DOWNLOAD_RESULT 
= "RESULT";     
  69     public static final String EXTRA_FILE_PATH 
= "FILE_PATH"; 
  70     public static final String EXTRA_REMOTE_PATH 
= "REMOTE_PATH"; 
  71     public static final String ACCOUNT_NAME 
= "ACCOUNT_NAME"; 
  73     private static final String TAG 
= "FileDownloader"; 
  75     private Looper mServiceLooper
; 
  76     private ServiceHandler mServiceHandler
; 
  77     private IBinder mBinder
; 
  78     private WebdavClient mDownloadClient 
= null
; 
  79     private Account mLastAccount 
= null
; 
  80     private FileDataStorageManager mStorageManager
; 
  82     private ConcurrentMap
<String
, DownloadFileOperation
> mPendingDownloads 
= new ConcurrentHashMap
<String
, DownloadFileOperation
>(); 
  83     private DownloadFileOperation mCurrentDownload 
= null
; 
  85     private NotificationManager mNotificationManager
; 
  86     private Notification mNotification
; 
  87     private int mLastPercent
; 
  91      * Builds a key for mPendingDownloads from the account and file to download 
  93      * @param account   Account where the file to download is stored 
  94      * @param file      File to download 
  96     private String 
buildRemoteName(Account account
, OCFile file
) { 
  97         return account
.name 
+ file
.getRemotePath(); 
 102      * Service initialization 
 105     public void onCreate() { 
 107         mNotificationManager 
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
); 
 108         HandlerThread thread 
= new HandlerThread("FileDownloaderThread", 
 109                 Process
.THREAD_PRIORITY_BACKGROUND
); 
 111         mServiceLooper 
= thread
.getLooper(); 
 112         mServiceHandler 
= new ServiceHandler(mServiceLooper
, this); 
 113         mBinder 
= new FileDownloaderBinder(); 
 118      * Entry point to add one or several files to the queue of downloads. 
 120      * New downloads are added calling to startService(), resulting in a call to this method. This ensures the service will keep on working  
 121      * although the caller activity goes away. 
 124     public int onStartCommand(Intent intent
, int flags
, int startId
) { 
 125         if (    !intent
.hasExtra(EXTRA_ACCOUNT
) || 
 126                 !intent
.hasExtra(EXTRA_FILE
) 
 127                 /*!intent.hasExtra(EXTRA_FILE_PATH) || 
 128                 !intent.hasExtra(EXTRA_REMOTE_PATH)*/ 
 130             Log_OC
.e(TAG
, "Not enough information provided in intent"); 
 131             return START_NOT_STICKY
; 
 133         Account account 
= intent
.getParcelableExtra(EXTRA_ACCOUNT
); 
 134         OCFile file 
= intent
.getParcelableExtra(EXTRA_FILE
); 
 136         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) 
 137         String downloadKey 
= buildRemoteName(account
, file
); 
 139             DownloadFileOperation newDownload 
= new DownloadFileOperation(account
, file
);  
 140             mPendingDownloads
.putIfAbsent(downloadKey
, newDownload
); 
 141             newDownload
.addDatatransferProgressListener(this); 
 142             newDownload
.addDatatransferProgressListener((FileDownloaderBinder
)mBinder
); 
 143             requestedDownloads
.add(downloadKey
); 
 144             sendBroadcastNewDownload(newDownload
); 
 146         } catch (IllegalArgumentException e
) { 
 147             Log_OC
.e(TAG
, "Not enough information provided in intent: " + e
.getMessage()); 
 148             return START_NOT_STICKY
; 
 151         if (requestedDownloads
.size() > 0) { 
 152             Message msg 
= mServiceHandler
.obtainMessage(); 
 154             msg
.obj 
= requestedDownloads
; 
 155             mServiceHandler
.sendMessage(msg
); 
 158         return START_NOT_STICKY
; 
 163      * Provides a binder object that clients can use to perform operations on the queue of downloads, excepting the addition of new files.  
 165      * Implemented to perform cancellation, pause and resume of existing downloads. 
 168     public IBinder 
onBind(Intent arg0
) { 
 174      * Called when ALL the bound clients were onbound. 
 177     public boolean onUnbind(Intent intent
) { 
 178         ((FileDownloaderBinder
)mBinder
).clearListeners(); 
 179         return false
;   // not accepting rebinding (default behaviour) 
 184      *  Binder to let client components to perform operations on the queue of downloads. 
 186      *  It provides by itself the available operations. 
 188     public class FileDownloaderBinder 
extends Binder 
implements OnDatatransferProgressListener 
{ 
 191          * Map of listeners that will be reported about progress of downloads from a {@link FileDownloaderBinder} instance  
 193         private Map
<String
, OnDatatransferProgressListener
> mBoundListeners 
= new HashMap
<String
, OnDatatransferProgressListener
>(); 
 197          * Cancels a pending or current download of a remote file. 
 199          * @param account       Owncloud account where the remote file is stored. 
 200          * @param file          A file in the queue of pending downloads 
 202         public void cancel(Account account
, OCFile file
) { 
 203             DownloadFileOperation download 
= null
; 
 204             synchronized (mPendingDownloads
) { 
 205                 download 
= mPendingDownloads
.remove(buildRemoteName(account
, file
)); 
 207             if (download 
!= null
) { 
 213         public void clearListeners() { 
 214             mBoundListeners
.clear(); 
 219          * Returns True when the file described by 'file' in the ownCloud account 'account' is downloading or waiting to download. 
 221          * If 'file' is a directory, returns 'true' if some of its descendant files is downloading or waiting to download.  
 223          * @param account       Owncloud account where the remote file is stored. 
 224          * @param file          A file that could be in the queue of downloads. 
 226         public boolean isDownloading(Account account
, OCFile file
) { 
 227             if (account 
== null 
|| file 
== null
) return false
; 
 228             String targetKey 
= buildRemoteName(account
, file
); 
 229             synchronized (mPendingDownloads
) { 
 230                 if (file
.isDirectory()) { 
 231                     // this can be slow if there are many downloads :( 
 232                     Iterator
<String
> it 
= mPendingDownloads
.keySet().iterator(); 
 233                     boolean found 
= false
; 
 234                     while (it
.hasNext() && !found
) { 
 235                         found 
= it
.next().startsWith(targetKey
); 
 239                     return (mPendingDownloads
.containsKey(targetKey
)); 
 246          * Adds a listener interested in the progress of the download for a concrete file. 
 248          * @param listener      Object to notify about progress of transfer.     
 249          * @param account       ownCloud account holding the file of interest. 
 250          * @param file          {@link OCfile} of interest for listener.  
 252         public void addDatatransferProgressListener (OnDatatransferProgressListener listener
, Account account
, OCFile file
) { 
 253             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 254             String targetKey 
= buildRemoteName(account
, file
); 
 255             mBoundListeners
.put(targetKey
, listener
); 
 261          * Removes a listener interested in the progress of the download for a concrete file. 
 263          * @param listener      Object to notify about progress of transfer.     
 264          * @param account       ownCloud account holding the file of interest. 
 265          * @param file          {@link OCfile} of interest for listener.  
 267         public void removeDatatransferProgressListener (OnDatatransferProgressListener listener
, Account account
, OCFile file
) { 
 268             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 269             String targetKey 
= buildRemoteName(account
, file
); 
 270             if (mBoundListeners
.get(targetKey
) == listener
) { 
 271                 mBoundListeners
.remove(targetKey
); 
 277         public void onTransferProgress(long progressRate
) { 
 278             // old way, should not be in use any more 
 283         public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, 
 285             String key 
= buildRemoteName(mCurrentDownload
.getAccount(), mCurrentDownload
.getFile()); 
 286             OnDatatransferProgressListener boundListener 
= mBoundListeners
.get(key
); 
 287             if (boundListener 
!= null
) { 
 288                 boundListener
.onTransferProgress(progressRate
, totalTransferredSoFar
, totalToTransfer
, fileName
); 
 296      * Download worker. Performs the pending downloads in the order they were requested.  
 298      * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.  
 300     private static class ServiceHandler 
extends Handler 
{ 
 301         // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak 
 302         FileDownloader mService
; 
 303         public ServiceHandler(Looper looper
, FileDownloader service
) { 
 306                 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); 
 311         public void handleMessage(Message msg
) { 
 312             @SuppressWarnings("unchecked") 
 313             AbstractList
<String
> requestedDownloads 
= (AbstractList
<String
>) msg
.obj
; 
 314             if (msg
.obj 
!= null
) { 
 315                 Iterator
<String
> it 
= requestedDownloads
.iterator(); 
 316                 while (it
.hasNext()) { 
 317                     mService
.downloadFile(it
.next()); 
 320             mService
.stopSelf(msg
.arg1
); 
 327      * Core download method: requests a file to download and stores it. 
 329      * @param downloadKey   Key to access the download to perform, contained in mPendingDownloads  
 331     private void downloadFile(String downloadKey
) { 
 333         synchronized(mPendingDownloads
) { 
 334             mCurrentDownload 
= mPendingDownloads
.get(downloadKey
); 
 337         if (mCurrentDownload 
!= null
) { 
 339             notifyDownloadStart(mCurrentDownload
); 
 341             /// prepare client object to send the request to the ownCloud server 
 342             if (mDownloadClient 
== null 
|| !mLastAccount
.equals(mCurrentDownload
.getAccount())) { 
 343                 mLastAccount 
= mCurrentDownload
.getAccount(); 
 344                 mStorageManager 
= new FileDataStorageManager(mLastAccount
, getContentResolver()); 
 345                 mDownloadClient 
= OwnCloudClientUtils
.createOwnCloudClient(mLastAccount
, getApplicationContext()); 
 348             /// perform the download 
 349             RemoteOperationResult downloadResult 
= null
; 
 351                 downloadResult 
= mCurrentDownload
.execute(mDownloadClient
); 
 352                 if (downloadResult
.isSuccess()) { 
 353                     saveDownloadedFile(); 
 357                 synchronized(mPendingDownloads
) { 
 358                     mPendingDownloads
.remove(downloadKey
); 
 364             notifyDownloadResult(mCurrentDownload
, downloadResult
); 
 366             sendBroadcastDownloadFinished(mCurrentDownload
, downloadResult
); 
 372      * Updates the OC File after a successful download. 
 374     private void saveDownloadedFile() { 
 375         OCFile file 
= mCurrentDownload
.getFile(); 
 376         long syncDate 
= System
.currentTimeMillis(); 
 377         file
.setLastSyncDateForProperties(syncDate
); 
 378         file
.setLastSyncDateForData(syncDate
); 
 379         file
.setModificationTimestamp(mCurrentDownload
.getModificationTimestamp()); 
 380         file
.setModificationTimestampAtLastSyncForData(mCurrentDownload
.getModificationTimestamp()); 
 381         // file.setEtag(mCurrentDownload.getEtag());    // TODO Etag, where available 
 382         file
.setMimetype(mCurrentDownload
.getMimeType()); 
 383         file
.setStoragePath(mCurrentDownload
.getSavePath()); 
 384         file
.setFileLength((new File(mCurrentDownload
.getSavePath()).length())); 
 385         mStorageManager
.saveFile(file
); 
 390      * Creates a status notification to show the download progress 
 392      * @param download  Download operation starting. 
 394     private void notifyDownloadStart(DownloadFileOperation download
) { 
 395         /// create status notification with a progress bar 
 397         mNotification 
= new Notification(R
.drawable
.icon
, getString(R
.string
.downloader_download_in_progress_ticker
), System
.currentTimeMillis()); 
 398         mNotification
.flags 
|= Notification
.FLAG_ONGOING_EVENT
; 
 399         mNotification
.contentView 
= new RemoteViews(getApplicationContext().getPackageName(), R
.layout
.progressbar_layout
); 
 400         mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, 0, download
.getSize() < 0); 
 401         mNotification
.contentView
.setTextViewText(R
.id
.status_text
, String
.format(getString(R
.string
.downloader_download_in_progress_content
), 0, new File(download
.getSavePath()).getName())); 
 402         mNotification
.contentView
.setImageViewResource(R
.id
.status_icon
, R
.drawable
.icon
); 
 404         /// includes a pending intent in the notification showing the details view of the file 
 405         Intent showDetailsIntent 
= null
; 
 406         if (PreviewImageFragment
.canBePreviewed(download
.getFile())) { 
 407             showDetailsIntent 
= new Intent(this, PreviewImageActivity
.class); 
 409             showDetailsIntent 
= new Intent(this, FileDetailActivity
.class); 
 411         showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, download
.getFile()); 
 412         showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, download
.getAccount()); 
 413         showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 414         mNotification
.contentIntent 
= PendingIntent
.getActivity(getApplicationContext(), (int)System
.currentTimeMillis(), showDetailsIntent
, 0); 
 416         mNotificationManager
.notify(R
.string
.downloader_download_in_progress_ticker
, mNotification
); 
 421      * Callback method to update the progress bar in the status notification. 
 424     public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, String fileName
) { 
 425         int percent 
= (int)(100.0*((double)totalTransferredSoFar
)/((double)totalToTransfer
)); 
 426         if (percent 
!= mLastPercent
) { 
 427           mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, percent
, totalToTransfer 
< 0); 
 428           String text 
= String
.format(getString(R
.string
.downloader_download_in_progress_content
), percent
, fileName
); 
 429           mNotification
.contentView
.setTextViewText(R
.id
.status_text
, text
); 
 430           mNotificationManager
.notify(R
.string
.downloader_download_in_progress_ticker
, mNotification
); 
 432         mLastPercent 
= percent
; 
 437      * Callback method to update the progress bar in the status notification (old version) 
 440     public void onTransferProgress(long progressRate
) { 
 441         // NOTHING TO DO HERE ANYMORE 
 446      * Updates the status notification with the result of a download operation. 
 448      * @param downloadResult    Result of the download operation. 
 449      * @param download          Finished download operation 
 451     private void notifyDownloadResult(DownloadFileOperation download
, RemoteOperationResult downloadResult
) { 
 452         mNotificationManager
.cancel(R
.string
.downloader_download_in_progress_ticker
); 
 453         if (!downloadResult
.isCancelled()) { 
 454             int tickerId 
= (downloadResult
.isSuccess()) ? R
.string
.downloader_download_succeeded_ticker 
: R
.string
.downloader_download_failed_ticker
; 
 455             int contentId 
= (downloadResult
.isSuccess()) ? R
.string
.downloader_download_succeeded_content 
: R
.string
.downloader_download_failed_content
; 
 456             Notification finalNotification 
= new Notification(R
.drawable
.icon
, getString(tickerId
), System
.currentTimeMillis()); 
 457             finalNotification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 458             Intent showDetailsIntent 
= null
; 
 459             if (downloadResult
.isSuccess()) { 
 460                 if (PreviewImageFragment
.canBePreviewed(download
.getFile())) { 
 461                     showDetailsIntent 
= new Intent(this, PreviewImageActivity
.class); 
 463                     showDetailsIntent 
= new Intent(this, FileDetailActivity
.class); 
 465                 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, download
.getFile()); 
 466                 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, download
.getAccount()); 
 467                 showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 470                 // TODO put something smart in showDetailsIntent 
 471                 showDetailsIntent 
= new Intent(); 
 473             finalNotification
.contentIntent 
= PendingIntent
.getActivity(getApplicationContext(), (int)System
.currentTimeMillis(), showDetailsIntent
, 0); 
 474             finalNotification
.setLatestEventInfo(getApplicationContext(), getString(tickerId
), String
.format(getString(contentId
), new File(download
.getSavePath()).getName()), finalNotification
.contentIntent
); 
 475             mNotificationManager
.notify(tickerId
, finalNotification
); 
 481      * Sends a broadcast when a download finishes in order to the interested activities can update their view 
 483      * @param download          Finished download operation 
 484      * @param downloadResult    Result of the download operation 
 486     private void sendBroadcastDownloadFinished(DownloadFileOperation download
, RemoteOperationResult downloadResult
) { 
 487         Intent end 
= new Intent(DOWNLOAD_FINISH_MESSAGE
); 
 488         end
.putExtra(EXTRA_DOWNLOAD_RESULT
, downloadResult
.isSuccess()); 
 489         end
.putExtra(ACCOUNT_NAME
, download
.getAccount().name
); 
 490         end
.putExtra(EXTRA_REMOTE_PATH
, download
.getRemotePath()); 
 491         end
.putExtra(EXTRA_FILE_PATH
, download
.getSavePath()); 
 492         sendStickyBroadcast(end
); 
 497      * Sends a broadcast when a new download is added to the queue. 
 499      * @param download          Added download operation 
 501     private void sendBroadcastNewDownload(DownloadFileOperation download
) { 
 502         Intent added 
= new Intent(DOWNLOAD_ADDED_MESSAGE
); 
 503         added
.putExtra(ACCOUNT_NAME
, download
.getAccount().name
); 
 504         added
.putExtra(EXTRA_REMOTE_PATH
, download
.getRemotePath()); 
 505         added
.putExtra(EXTRA_FILE_PATH
, download
.getSavePath()); 
 506         sendStickyBroadcast(added
);