1 /* ownCloud Android client application 
   2  *   Copyright (C) 2012 Bartek Przybylski 
   4  *   This program is free software: you can redistribute it and/or modify 
   5  *   it under the terms of the GNU General Public License as published by 
   6  *   the Free Software Foundation, either version 3 of the License, or 
   7  *   (at your option) any later version. 
   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
.Iterator
; 
  24 import java
.util
.Vector
; 
  25 import java
.util
.concurrent
.ConcurrentHashMap
; 
  26 import java
.util
.concurrent
.ConcurrentMap
; 
  28 import com
.owncloud
.android
.authenticator
.AccountAuthenticator
; 
  29 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  30 import com
.owncloud
.android
.datamodel
.OCFile
; 
  31 import com
.owncloud
.android
.files
.InstantUploadBroadcastReceiver
; 
  32 import com
.owncloud
.android
.operations
.ChunkedUploadFileOperation
; 
  33 import com
.owncloud
.android
.operations
.RemoteOperationResult
; 
  34 import com
.owncloud
.android
.operations
.UploadFileOperation
; 
  35 import com
.owncloud
.android
.ui
.activity
.FileDetailActivity
; 
  36 import com
.owncloud
.android
.ui
.fragment
.FileDetailFragment
; 
  37 import com
.owncloud
.android
.utils
.OwnCloudVersion
; 
  39 import eu
.alefzero
.webdav
.OnDatatransferProgressListener
; 
  41 import com
.owncloud
.android
.network
.OwnCloudClientUtils
; 
  43 import android
.accounts
.Account
; 
  44 import android
.accounts
.AccountManager
; 
  45 import android
.app
.Notification
; 
  46 import android
.app
.NotificationManager
; 
  47 import android
.app
.PendingIntent
; 
  48 import android
.app
.Service
; 
  49 import android
.content
.Intent
; 
  50 import android
.os
.Binder
; 
  51 import android
.os
.Handler
; 
  52 import android
.os
.HandlerThread
; 
  53 import android
.os
.IBinder
; 
  54 import android
.os
.Looper
; 
  55 import android
.os
.Message
; 
  56 import android
.os
.Process
; 
  57 import android
.util
.Log
; 
  58 import android
.webkit
.MimeTypeMap
; 
  59 import android
.widget
.RemoteViews
; 
  61 import com
.owncloud
.android
.R
; 
  62 import eu
.alefzero
.webdav
.WebdavClient
; 
  64 public class FileUploader 
extends Service 
implements OnDatatransferProgressListener 
{ 
  66     public static final String UPLOAD_FINISH_MESSAGE 
= "UPLOAD_FINISH"; 
  67     public static final String EXTRA_UPLOAD_RESULT 
= "RESULT"; 
  68     public static final String EXTRA_REMOTE_PATH 
= "REMOTE_PATH"; 
  69     public static final String EXTRA_FILE_PATH 
= "FILE_PATH"; 
  71     public static final String KEY_LOCAL_FILE 
= "LOCAL_FILE"; 
  72     public static final String KEY_REMOTE_FILE 
= "REMOTE_FILE"; 
  73     public static final String KEY_ACCOUNT 
= "ACCOUNT"; 
  74     public static final String KEY_UPLOAD_TYPE 
= "UPLOAD_TYPE"; 
  75     public static final String KEY_FORCE_OVERWRITE 
= "KEY_FORCE_OVERWRITE"; 
  76     public static final String ACCOUNT_NAME 
= "ACCOUNT_NAME";     
  77     public static final String KEY_MIME_TYPE 
= "MIME_TYPE"; 
  78     public static final String KEY_INSTANT_UPLOAD 
= "INSTANT_UPLOAD"; 
  80     public static final int UPLOAD_SINGLE_FILE 
= 0; 
  81     public static final int UPLOAD_MULTIPLE_FILES 
= 1; 
  83     private static final String TAG 
= FileUploader
.class.getSimpleName(); 
  85     private Looper mServiceLooper
; 
  86     private ServiceHandler mServiceHandler
; 
  87     private IBinder mBinder
; 
  88     private WebdavClient mUploadClient 
= null
; 
  89     private Account mLastAccount 
= null
; 
  90     private FileDataStorageManager mStorageManager
; 
  92     private ConcurrentMap
<String
, UploadFileOperation
> mPendingUploads 
= new ConcurrentHashMap
<String
, UploadFileOperation
>(); 
  93     private UploadFileOperation mCurrentUpload 
= null
; 
  95     private NotificationManager mNotificationManager
; 
  96     private Notification mNotification
; 
  97     private int mLastPercent
; 
  98     private RemoteViews mDefaultNotificationContentView
; 
 102      * Builds a key for mPendingUploads from the account and file to upload 
 104      * @param account   Account where the file to download is stored 
 105      * @param file      File to download 
 107     private String 
buildRemoteName(Account account
, OCFile file
) { 
 108         return account
.name 
+ file
.getRemotePath(); 
 111     private String 
buildRemoteName(Account account
, String remotePath
) { 
 112         return account
.name 
+ remotePath
; 
 117      * Checks if an ownCloud server version should support chunked uploads. 
 119      * @param version   OwnCloud version instance corresponding to an ownCloud server. 
 120      * @return          'True' if the ownCloud server with version supports chunked uploads. 
 122     private static boolean chunkedUploadIsSupported(OwnCloudVersion version
) { 
 123         return (version 
!= null 
&& version
.compareTo(OwnCloudVersion
.owncloud_v4_5
) >= 0); 
 129      * Service initialization 
 132     public void onCreate() { 
 134         mNotificationManager 
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
); 
 135         HandlerThread thread 
= new HandlerThread("FileUploaderThread", 
 136                 Process
.THREAD_PRIORITY_BACKGROUND
); 
 138         mServiceLooper 
= thread
.getLooper(); 
 139         mServiceHandler 
= new ServiceHandler(mServiceLooper
, this); 
 140         mBinder 
= new FileUploaderBinder(); 
 145      * Entry point to add one or several files to the queue of uploads. 
 147      * New uploads are added calling to startService(), resulting in a call to this method. This ensures the service will keep on working  
 148      * although the caller activity goes away. 
 151     public int onStartCommand(Intent intent
, int flags
, int startId
) { 
 152         if (!intent
.hasExtra(KEY_ACCOUNT
) || !intent
.hasExtra(KEY_UPLOAD_TYPE
)) { 
 153             Log
.e(TAG
, "Not enough information provided in intent"); 
 154             return Service
.START_NOT_STICKY
; 
 156         int uploadType 
= intent
.getIntExtra(KEY_UPLOAD_TYPE
, -1); 
 157         if (uploadType 
== -1) { 
 158             Log
.e(TAG
, "Incorrect upload type provided"); 
 159             return Service
.START_NOT_STICKY
; 
 161         Account account 
= intent
.getParcelableExtra(KEY_ACCOUNT
); 
 163         String
[] localPaths
, remotePaths
, mimeTypes
;  
 164         if (uploadType 
== UPLOAD_SINGLE_FILE
) { 
 165             localPaths 
= new String
[] { intent
.getStringExtra(KEY_LOCAL_FILE
) }; 
 166             remotePaths 
= new String
[] { intent
 
 167                     .getStringExtra(KEY_REMOTE_FILE
) }; 
 168             mimeTypes 
= new String
[] { intent
.getStringExtra(KEY_MIME_TYPE
) }; 
 170         } else { // mUploadType == UPLOAD_MULTIPLE_FILES 
 171             localPaths 
= intent
.getStringArrayExtra(KEY_LOCAL_FILE
); 
 172             remotePaths 
= intent
.getStringArrayExtra(KEY_REMOTE_FILE
); 
 173             mimeTypes 
= intent
.getStringArrayExtra(KEY_MIME_TYPE
); 
 176         if (localPaths 
== null
) { 
 177             Log
.e(TAG
, "Incorrect array for local paths provided in upload intent"); 
 178             return Service
.START_NOT_STICKY
; 
 180         if (remotePaths 
== null
) { 
 181             Log
.e(TAG
, "Incorrect array for remote paths provided in upload intent"); 
 182             return Service
.START_NOT_STICKY
; 
 185         if (localPaths
.length 
!= remotePaths
.length
) { 
 186             Log
.e(TAG
, "Different number of remote paths and local paths!"); 
 187             return Service
.START_NOT_STICKY
; 
 190         boolean isInstant 
= intent
.getBooleanExtra(KEY_INSTANT_UPLOAD
, false
);  
 191         boolean forceOverwrite 
= intent
.getBooleanExtra(KEY_FORCE_OVERWRITE
, false
); 
 193         OwnCloudVersion ocv 
= new OwnCloudVersion(AccountManager
.get(this).getUserData(account
, AccountAuthenticator
.KEY_OC_VERSION
)); 
 194         boolean chunked 
= FileUploader
.chunkedUploadIsSupported(ocv
); 
 195         AbstractList
<String
> requestedUploads 
= new Vector
<String
>(); 
 196         String uploadKey 
= null
; 
 197         UploadFileOperation newUpload 
= null
; 
 199         FileDataStorageManager storageManager 
= new FileDataStorageManager(account
, getContentResolver()); 
 200         boolean fixed 
= false
; 
 202             fixed 
= checkAndFixInstantUploadDirectory(storageManager
); 
 205             for (int i
=0; i 
< localPaths
.length
; i
++) { 
 206                 uploadKey 
= buildRemoteName(account
, remotePaths
[i
]); 
 207                 file 
= obtainNewOCFileToUpload(remotePaths
[i
], localPaths
[i
], ((mimeTypes
!=null
)?mimeTypes
[i
]:(String
)null
), isInstant
, forceOverwrite
, storageManager
); 
 209                     newUpload 
= new ChunkedUploadFileOperation(account
, file
, isInstant
, forceOverwrite
); 
 211                     newUpload 
= new UploadFileOperation(account
, file
, isInstant
, forceOverwrite
); 
 214                     newUpload
.setRemoteFolderToBeCreated(); 
 216                 mPendingUploads
.putIfAbsent(uploadKey
, newUpload
); 
 217                 newUpload
.addDatatransferProgressListener(this); 
 218                 requestedUploads
.add(uploadKey
); 
 221         } catch (IllegalArgumentException e
) { 
 222             Log
.e(TAG
, "Not enough information provided in intent: " + e
.getMessage()); 
 223             return START_NOT_STICKY
; 
 225         } catch (IllegalStateException e
) { 
 226             Log
.e(TAG
, "Bad information provided in intent: " + e
.getMessage()); 
 227             return START_NOT_STICKY
; 
 229         } catch (Exception e
) { 
 230             Log
.e(TAG
, "Unexpected exception while processing upload intent", e
); 
 231             return START_NOT_STICKY
; 
 235         if (requestedUploads
.size() > 0) { 
 236             Message msg 
= mServiceHandler
.obtainMessage(); 
 238             msg
.obj 
= requestedUploads
; 
 239             mServiceHandler
.sendMessage(msg
); 
 242         return Service
.START_NOT_STICKY
; 
 247      * Provides a binder object that clients can use to perform operations on the queue of uploads, excepting the addition of new files.  
 249      * Implemented to perform cancellation, pause and resume of existing uploads. 
 252     public IBinder 
onBind(Intent arg0
) { 
 257      *  Binder to let client components to perform operations on the queue of uploads. 
 259      *  It provides by itself the available operations. 
 261     public class FileUploaderBinder 
extends Binder 
{ 
 264          * Cancels a pending or current upload of a remote file. 
 266          * @param account       Owncloud account where the remote file will be stored. 
 267          * @param file          A file in the queue of pending uploads 
 269         public void cancel(Account account
, OCFile file
) { 
 270             UploadFileOperation upload 
= null
; 
 271             synchronized (mPendingUploads
) { 
 272                 upload 
= mPendingUploads
.remove(buildRemoteName(account
, file
)); 
 274             if (upload 
!= null
) { 
 281          * Returns True when the file described by 'file' is being uploaded to the ownCloud account 'account' or waiting for it 
 283          * If 'file' is a directory, returns 'true' if some of its descendant files is downloading or waiting to download.  
 285          * @param account       Owncloud account where the remote file will be stored. 
 286          * @param file          A file that could be in the queue of pending uploads 
 288         public boolean isUploading(Account account
, OCFile file
) { 
 289             String targetKey 
= buildRemoteName(account
, file
); 
 290             synchronized (mPendingUploads
) { 
 291                 if (file
.isDirectory()) { 
 292                     // this can be slow if there are many downloads :( 
 293                     Iterator
<String
> it 
= mPendingUploads
.keySet().iterator(); 
 294                     boolean found 
= false
; 
 295                     while (it
.hasNext() && !found
) { 
 296                         found 
= it
.next().startsWith(targetKey
); 
 300                     return (mPendingUploads
.containsKey(targetKey
)); 
 310      * Upload worker. Performs the pending uploads in the order they were requested.  
 312      * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.  
 314     private static class ServiceHandler 
extends Handler 
{ 
 315         // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak 
 316         FileUploader mService
; 
 317         public ServiceHandler(Looper looper
, FileUploader service
) { 
 320                 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); 
 325         public void handleMessage(Message msg
) { 
 326             @SuppressWarnings("unchecked") 
 327             AbstractList
<String
> requestedUploads 
= (AbstractList
<String
>) msg
.obj
; 
 328             if (msg
.obj 
!= null
) { 
 329                 Iterator
<String
> it 
= requestedUploads
.iterator(); 
 330                 while (it
.hasNext()) { 
 331                     mService
.uploadFile(it
.next()); 
 334             mService
.stopSelf(msg
.arg1
); 
 342      * Core upload method: sends the file(s) to upload 
 344      * @param uploadKey   Key to access the upload to perform, contained in mPendingUploads 
 346     public void uploadFile(String uploadKey
) { 
 348         synchronized(mPendingUploads
) { 
 349             mCurrentUpload 
= mPendingUploads
.get(uploadKey
); 
 352         if (mCurrentUpload 
!= null
) { 
 354             notifyUploadStart(mCurrentUpload
); 
 357             /// prepare client object to send requests to the ownCloud server 
 358             if (mUploadClient 
== null 
|| !mLastAccount
.equals(mCurrentUpload
.getAccount())) { 
 359                 mLastAccount 
= mCurrentUpload
.getAccount(); 
 360                 mStorageManager 
= new FileDataStorageManager(mLastAccount
, getContentResolver()); 
 361                 mUploadClient 
= OwnCloudClientUtils
.createOwnCloudClient(mLastAccount
, getApplicationContext()); 
 364             /// create remote folder for instant uploads 
 365             if (mCurrentUpload
.isRemoteFolderToBeCreated()) { 
 366                 mUploadClient
.createDirectory(InstantUploadBroadcastReceiver
.INSTANT_UPLOAD_DIR
);    // ignoring result; fail could just mean that it already exists, but local database is not synchronized; the upload will be tried anyway 
 370             /// perform the upload 
 371             RemoteOperationResult uploadResult 
= null
; 
 373                 uploadResult 
= mCurrentUpload
.execute(mUploadClient
); 
 374                 if (uploadResult
.isSuccess()) { 
 375                     saveUploadedFile(mCurrentUpload
.getFile(), mStorageManager
); 
 379                 synchronized(mPendingUploads
) { 
 380                     mPendingUploads
.remove(uploadKey
); 
 385             notifyUploadResult(uploadResult
, mCurrentUpload
); 
 387             sendFinalBroadcast(mCurrentUpload
, uploadResult
); 
 394      * Saves a new OC File after a successful upload. 
 396      * @param file              OCFile describing the uploaded file 
 397      * @param storageManager    Interface to the database where the new OCFile has to be stored. 
 398      * @param parentDirId       Id of the parent OCFile. 
 400     private void saveUploadedFile(OCFile file
, FileDataStorageManager storageManager
) { 
 401         file
.setModificationTimestamp(System
.currentTimeMillis()); 
 402         storageManager
.saveFile(file
); 
 406     private boolean checkAndFixInstantUploadDirectory(FileDataStorageManager storageManager
) { 
 407         OCFile instantUploadDir 
= storageManager
.getFileByPath(InstantUploadBroadcastReceiver
.INSTANT_UPLOAD_DIR
); 
 408         if (instantUploadDir 
== null
) { 
 409             // first instant upload in the account, or never account not synchronized after the remote InstantUpload folder was created 
 410             OCFile newDir 
= new OCFile(InstantUploadBroadcastReceiver
.INSTANT_UPLOAD_DIR
); 
 411             newDir
.setMimetype("DIR"); 
 412             newDir
.setParentId(storageManager
.getFileByPath(OCFile
.PATH_SEPARATOR
).getFileId()); 
 413             storageManager
.saveFile(newDir
); 
 420     private OCFile 
obtainNewOCFileToUpload(String remotePath
, String localPath
, String mimeType
, boolean isInstant
, boolean forceOverwrite
, FileDataStorageManager storageManager
) { 
 421         OCFile newFile 
= new OCFile(remotePath
); 
 422         newFile
.setStoragePath(localPath
); 
 423         newFile
.setLastSyncDate(0); 
 424         newFile
.setKeepInSync(forceOverwrite
); 
 427         if (localPath 
!= null 
&& localPath
.length() > 0) { 
 428             File localFile 
= new File(localPath
); 
 429             newFile
.setFileLength(localFile
.length()); 
 430         }   // don't worry about not assigning size, the problems with localPath are checked when the UploadFileOperation instance is created 
 433         if (mimeType 
== null 
|| mimeType
.length() <= 0) { 
 435                 mimeType 
= MimeTypeMap
.getSingleton() 
 436                     .getMimeTypeFromExtension( 
 437                             remotePath
.substring(remotePath
.lastIndexOf('.') + 1)); 
 438             } catch (IndexOutOfBoundsException e
) { 
 439                 Log
.e(TAG
, "Trying to find out MIME type of a file without extension: " + remotePath
); 
 442         if (mimeType 
== null
) { 
 443             mimeType 
= "application/octet-stream"; 
 445         newFile
.setMimetype(mimeType
); 
 448         String parentPath 
= new File(remotePath
).getParent(); 
 449         parentPath 
= parentPath
.endsWith(OCFile
.PATH_SEPARATOR
) ? parentPath 
: parentPath 
+ OCFile
.PATH_SEPARATOR 
; 
 450         OCFile parentDir 
= storageManager
.getFileByPath(parentPath
); 
 451         if (parentDir 
== null
) { 
 452             throw new IllegalStateException("Can not upload a file to a non existing remote location: " + parentPath
); 
 454         long parentDirId 
= parentDir
.getFileId(); 
 455         newFile
.setParentId(parentDirId
); 
 461      * Creates a status notification to show the upload progress 
 463      * @param upload    Upload operation starting. 
 465     private void notifyUploadStart(UploadFileOperation upload
) { 
 466         /// create status notification with a progress bar 
 468         mNotification 
= new Notification(R
.drawable
.icon
, getString(R
.string
.uploader_upload_in_progress_ticker
), System
.currentTimeMillis()); 
 469         mNotification
.flags 
|= Notification
.FLAG_ONGOING_EVENT
; 
 470         mDefaultNotificationContentView 
= mNotification
.contentView
; 
 471         mNotification
.contentView 
= new RemoteViews(getApplicationContext().getPackageName(), R
.layout
.progressbar_layout
); 
 472         mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, 0, false
); 
 473         mNotification
.contentView
.setTextViewText(R
.id
.status_text
, String
.format(getString(R
.string
.uploader_upload_in_progress_content
), 0, new File(upload
.getStoragePath()).getName())); 
 474         mNotification
.contentView
.setImageViewResource(R
.id
.status_icon
, R
.drawable
.icon
); 
 476         /// includes a pending intent in the notification showing the details view of the file 
 477         Intent showDetailsIntent 
= new Intent(this, FileDetailActivity
.class); 
 478         showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, upload
.getFile()); 
 479         showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, upload
.getAccount()); 
 480         showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 481         mNotification
.contentIntent 
= PendingIntent
.getActivity(getApplicationContext(), (int)System
.currentTimeMillis(), showDetailsIntent
, 0); 
 483         mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
); 
 488      * Callback method to update the progress bar in the status notification 
 491     public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, String fileName
) { 
 492         int percent 
= (int)(100.0*((double)totalTransferredSoFar
)/((double)totalToTransfer
)); 
 493         if (percent 
!= mLastPercent
) { 
 494             mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, percent
, false
); 
 495             String text 
= String
.format(getString(R
.string
.uploader_upload_in_progress_content
), percent
, fileName
); 
 496             mNotification
.contentView
.setTextViewText(R
.id
.status_text
, text
); 
 497             mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
); 
 499         mLastPercent 
= percent
; 
 504      * Callback method to update the progress bar in the status notification  (old version) 
 507     public void onTransferProgress(long progressRate
) { 
 508         // NOTHING TO DO HERE ANYMORE 
 513      * Updates the status notification with the result of an upload operation. 
 515      * @param uploadResult    Result of the upload operation. 
 516      * @param upload          Finished upload operation 
 518     private void notifyUploadResult(RemoteOperationResult uploadResult
, UploadFileOperation upload
) { 
 519         if (uploadResult
.isCancelled()) { 
 520             /// cancelled operation -> silent removal of progress notification 
 521             mNotificationManager
.cancel(R
.string
.uploader_upload_in_progress_ticker
); 
 523         } else if (uploadResult
.isSuccess()) { 
 524             /// success -> silent update of progress notification to success message  
 525             mNotification
.flags ^
= Notification
.FLAG_ONGOING_EVENT
; // remove the ongoing flag 
 526             mNotification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 527             mNotification
.contentView 
= mDefaultNotificationContentView
; 
 529             /// includes a pending intent in the notification showing the details view of the file 
 530             Intent showDetailsIntent 
= new Intent(this, FileDetailActivity
.class); 
 531             showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, upload
.getFile()); 
 532             showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, upload
.getAccount()); 
 533             showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 534             mNotification
.contentIntent 
= PendingIntent
.getActivity(getApplicationContext(), (int)System
.currentTimeMillis(), showDetailsIntent
, 0); 
 536             mNotification
.setLatestEventInfo(   getApplicationContext(),  
 537                                                 getString(R
.string
.uploader_upload_succeeded_ticker
),  
 538                                                 String
.format(getString(R
.string
.uploader_upload_succeeded_content_single
), (new File(upload
.getStoragePath())).getName()),  
 539                                                 mNotification
.contentIntent
); 
 541             mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
);    // NOT AN ERROR; uploader_upload_in_progress_ticker is the target, not a new notification 
 543             /* Notification about multiple uploads: pending of update 
 544             mNotification.setLatestEventInfo(   getApplicationContext(),  
 545                                                     getString(R.string.uploader_upload_succeeded_ticker),  
 546                                                     String.format(getString(R.string.uploader_upload_succeeded_content_multiple), mSuccessCounter),  
 547                                                     mNotification.contentIntent); 
 551             /// fail -> explicit failure notification 
 552             mNotificationManager
.cancel(R
.string
.uploader_upload_in_progress_ticker
); 
 553             Notification finalNotification 
= new Notification(R
.drawable
.icon
, getString(R
.string
.uploader_upload_failed_ticker
), System
.currentTimeMillis()); 
 554             finalNotification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 555             // TODO put something smart in the contentIntent below 
 556             finalNotification
.contentIntent 
= PendingIntent
.getActivity(getApplicationContext(), (int)System
.currentTimeMillis(), new Intent(), 0); 
 557             finalNotification
.setLatestEventInfo(   getApplicationContext(),  
 558                                                     getString(R
.string
.uploader_upload_failed_ticker
),  
 559                                                     String
.format(getString(R
.string
.uploader_upload_failed_content_single
), (new File(upload
.getStoragePath())).getName()),  
 560                                                     finalNotification
.contentIntent
); 
 562             mNotificationManager
.notify(R
.string
.uploader_upload_failed_ticker
, finalNotification
); 
 564             /* Notification about multiple uploads failure: pending of update 
 565             finalNotification.setLatestEventInfo(   getApplicationContext(),  
 566                                                         getString(R.string.uploader_upload_failed_ticker),  
 567                                                         String.format(getString(R.string.uploader_upload_failed_content_multiple), mSuccessCounter, mTotalFilesToSend),  
 568                                                         finalNotification.contentIntent); 
 576      * Sends a broadcast in order to the interested activities can update their view 
 578      * @param upload          Finished upload operation 
 579      * @param uploadResult    Result of the upload operation 
 581     private void sendFinalBroadcast(UploadFileOperation upload
, RemoteOperationResult uploadResult
) { 
 582         Intent end 
= new Intent(UPLOAD_FINISH_MESSAGE
); 
 583         end
.putExtra(EXTRA_REMOTE_PATH
, upload
.getRemotePath());    // real remote path, after possible automatic renaming 
 584         end
.putExtra(EXTRA_FILE_PATH
, upload
.getStoragePath()); 
 585         end
.putExtra(ACCOUNT_NAME
, upload
.getAccount().name
); 
 586         end
.putExtra(EXTRA_UPLOAD_RESULT
, uploadResult
.isSuccess());