2  *   ownCloud Android client application 
   4  *   Copyright (C) 2012 Bartek Przybylski 
   5  *   Copyright (C) 2012-2015 ownCloud Inc. 
   7  *   This program is free software: you can redistribute it and/or modify 
   8  *   it under the terms of the GNU General Public License version 2, 
   9  *   as published by the Free Software Foundation. 
  11  *   This program is distributed in the hope that it will be useful, 
  12  *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
  13  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  14  *   GNU General Public License for more details. 
  16  *   You should have received a copy of the GNU General Public License 
  17  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. 
  21 package com
.owncloud
.android
.files
.services
; 
  24 import java
.io
.IOException
; 
  25 import java
.util
.AbstractList
; 
  26 import java
.util
.HashMap
; 
  27 import java
.util
.Iterator
; 
  29 import java
.util
.Vector
; 
  30 import java
.util
.concurrent
.ConcurrentHashMap
; 
  31 import java
.util
.concurrent
.ConcurrentMap
; 
  33 import android
.accounts
.Account
; 
  34 import android
.accounts
.AccountManager
; 
  35 import android
.accounts
.AccountsException
; 
  36 import android
.accounts
.OnAccountsUpdateListener
; 
  37 import android
.app
.NotificationManager
; 
  38 import android
.app
.PendingIntent
; 
  39 import android
.app
.Service
; 
  40 import android
.content
.Intent
; 
  41 import android
.os
.Binder
; 
  42 import android
.os
.Handler
; 
  43 import android
.os
.HandlerThread
; 
  44 import android
.os
.IBinder
; 
  45 import android
.os
.Looper
; 
  46 import android
.os
.Message
; 
  47 import android
.os
.Process
; 
  48 import android
.support
.v4
.app
.NotificationCompat
; 
  49 import android
.webkit
.MimeTypeMap
; 
  51 import com
.owncloud
.android
.MainApp
; 
  52 import com
.owncloud
.android
.R
; 
  53 import com
.owncloud
.android
.authentication
.AccountUtils
; 
  54 import com
.owncloud
.android
.authentication
.AuthenticatorActivity
; 
  55 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  56 import com
.owncloud
.android
.datamodel
.OCFile
; 
  57 import com
.owncloud
.android
.db
.DbHandler
; 
  58 import com
.owncloud
.android
.lib
.common
.OwnCloudAccount
; 
  59 import com
.owncloud
.android
.lib
.common
.OwnCloudClient
; 
  60 import com
.owncloud
.android
.lib
.common
.OwnCloudClientManagerFactory
; 
  61 import com
.owncloud
.android
.lib
.common
.accounts
.AccountUtils
.Constants
; 
  62 import com
.owncloud
.android
.lib
.common
.network
.OnDatatransferProgressListener
; 
  63 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperation
; 
  64 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
; 
  65 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
.ResultCode
; 
  66 import com
.owncloud
.android
.lib
.common
.utils
.Log_OC
; 
  67 import com
.owncloud
.android
.lib
.resources
.files
.ExistenceCheckRemoteOperation
; 
  68 import com
.owncloud
.android
.lib
.resources
.files
.FileUtils
; 
  69 import com
.owncloud
.android
.lib
.resources
.files
.ReadRemoteFileOperation
; 
  70 import com
.owncloud
.android
.lib
.resources
.files
.RemoteFile
; 
  71 import com
.owncloud
.android
.lib
.resources
.status
.OwnCloudVersion
; 
  72 import com
.owncloud
.android
.notifications
.NotificationBuilderWithProgressBar
; 
  73 import com
.owncloud
.android
.notifications
.NotificationDelayer
; 
  74 import com
.owncloud
.android
.operations
.CreateFolderOperation
; 
  75 import com
.owncloud
.android
.operations
.UploadFileOperation
; 
  76 import com
.owncloud
.android
.operations
.common
.SyncOperation
; 
  77 import com
.owncloud
.android
.ui
.activity
.FileActivity
; 
  78 import com
.owncloud
.android
.ui
.activity
.FileDisplayActivity
; 
  79 import com
.owncloud
.android
.utils
.ErrorMessageAdapter
; 
  80 import com
.owncloud
.android
.utils
.UriUtils
; 
  83 public class FileUploader 
extends Service
 
  84         implements OnDatatransferProgressListener
, OnAccountsUpdateListener 
{ 
  86     private static final String UPLOAD_FINISH_MESSAGE 
= "UPLOAD_FINISH"; 
  87     public static final String EXTRA_UPLOAD_RESULT 
= "RESULT"; 
  88     public static final String EXTRA_REMOTE_PATH 
= "REMOTE_PATH"; 
  89     public static final String EXTRA_OLD_REMOTE_PATH 
= "OLD_REMOTE_PATH"; 
  90     public static final String EXTRA_OLD_FILE_PATH 
= "OLD_FILE_PATH"; 
  91     public static final String ACCOUNT_NAME 
= "ACCOUNT_NAME"; 
  93     public static final String KEY_FILE 
= "FILE"; 
  94     public static final String KEY_LOCAL_FILE 
= "LOCAL_FILE"; 
  95     public static final String KEY_REMOTE_FILE 
= "REMOTE_FILE"; 
  96     public static final String KEY_MIME_TYPE 
= "MIME_TYPE"; 
  98     public static final String KEY_ACCOUNT 
= "ACCOUNT"; 
 100     public static final String KEY_UPLOAD_TYPE 
= "UPLOAD_TYPE"; 
 101     public static final String KEY_FORCE_OVERWRITE 
= "KEY_FORCE_OVERWRITE"; 
 102     public static final String KEY_INSTANT_UPLOAD 
= "INSTANT_UPLOAD"; 
 103     public static final String KEY_LOCAL_BEHAVIOUR 
= "BEHAVIOUR"; 
 105     public static final int LOCAL_BEHAVIOUR_COPY 
= 0; 
 106     public static final int LOCAL_BEHAVIOUR_MOVE 
= 1; 
 107     public static final int LOCAL_BEHAVIOUR_FORGET 
= 2; 
 109     public static final int UPLOAD_SINGLE_FILE 
= 0; 
 110     public static final int UPLOAD_MULTIPLE_FILES 
= 1; 
 112     private static final String TAG 
= FileUploader
.class.getSimpleName(); 
 114     private Looper mServiceLooper
; 
 115     private ServiceHandler mServiceHandler
; 
 116     private IBinder mBinder
; 
 117     private OwnCloudClient mUploadClient 
= null
; 
 118     private Account mLastAccount 
= null
; 
 119     private FileDataStorageManager mStorageManager
; 
 121     private ConcurrentMap
<String
, UploadFileOperation
> mPendingUploads 
= new ConcurrentHashMap
<String
, UploadFileOperation
>(); 
 122     private UploadFileOperation mCurrentUpload 
= null
; 
 124     private NotificationManager mNotificationManager
; 
 125     private NotificationCompat
.Builder mNotificationBuilder
; 
 126     private int mLastPercent
; 
 128     private static final String MIME_TYPE_PDF 
= "application/pdf"; 
 129     private static final String FILE_EXTENSION_PDF 
= ".pdf"; 
 132     public static String 
getUploadFinishMessage() { 
 133         return FileUploader
.class.getName().toString() + UPLOAD_FINISH_MESSAGE
; 
 137      * Builds a key for mPendingUploads from the account and file to upload 
 139      * @param account   Account where the file to upload is stored 
 140      * @param file      File to upload 
 142     private String 
buildRemoteName(Account account
, OCFile file
) { 
 143         return account
.name 
+ file
.getRemotePath(); 
 146     private String 
buildRemoteName(Account account
, String remotePath
) { 
 147         return account
.name 
+ remotePath
; 
 151      * Checks if an ownCloud server version should support chunked uploads. 
 153      * @param version OwnCloud version instance corresponding to an ownCloud 
 155      * @return 'True' if the ownCloud server with version supports chunked 
 158     private static boolean chunkedUploadIsSupported(OwnCloudVersion version
) { 
 159         return (version 
!= null 
&& version
.compareTo(OwnCloudVersion
.owncloud_v4_5
) >= 0); 
 163      * Service initialization 
 166     public void onCreate() { 
 168         Log_OC
.d(TAG
, "Creating service"); 
 169         mNotificationManager 
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
); 
 170         HandlerThread thread 
= new HandlerThread("FileUploaderThread", 
 171                 Process
.THREAD_PRIORITY_BACKGROUND
); 
 173         mServiceLooper 
= thread
.getLooper(); 
 174         mServiceHandler 
= new ServiceHandler(mServiceLooper
, this); 
 175         mBinder 
= new FileUploaderBinder(); 
 177         // add AccountsUpdatedListener 
 178         AccountManager am 
= AccountManager
.get(getApplicationContext()); 
 179         am
.addOnAccountsUpdatedListener(this, null
, false
); 
 186     public void onDestroy() { 
 187         Log_OC
.v(TAG
, "Destroying service" ); 
 189         mServiceHandler 
= null
; 
 190         mServiceLooper
.quit(); 
 191         mServiceLooper 
= null
; 
 192         mNotificationManager 
= null
; 
 194         // remove AccountsUpdatedListener 
 195         AccountManager am 
= AccountManager
.get(getApplicationContext()); 
 196         am
.removeOnAccountsUpdatedListener(this); 
 203      * Entry point to add one or several files to the queue of uploads. 
 205      * New uploads are added calling to startService(), resulting in a call to 
 206      * this method. This ensures the service will keep on working although the 
 207      * caller activity goes away. 
 210     public int onStartCommand(Intent intent
, int flags
, int startId
) { 
 211         Log_OC
.d(TAG
, "Starting command with id " + startId
); 
 213         if (!intent
.hasExtra(KEY_ACCOUNT
) || !intent
.hasExtra(KEY_UPLOAD_TYPE
) 
 214                 || !(intent
.hasExtra(KEY_LOCAL_FILE
) || intent
.hasExtra(KEY_FILE
))) { 
 215             Log_OC
.e(TAG
, "Not enough information provided in intent"); 
 216             return Service
.START_NOT_STICKY
; 
 218         int uploadType 
= intent
.getIntExtra(KEY_UPLOAD_TYPE
, -1); 
 219         if (uploadType 
== -1) { 
 220             Log_OC
.e(TAG
, "Incorrect upload type provided"); 
 221             return Service
.START_NOT_STICKY
; 
 223         Account account 
= intent
.getParcelableExtra(KEY_ACCOUNT
); 
 224         if (!AccountUtils
.exists(account
, getApplicationContext())) { 
 225             return Service
.START_NOT_STICKY
; 
 228         String
[] localPaths 
= null
, remotePaths 
= null
, mimeTypes 
= null
; 
 229         OCFile
[] files 
= null
; 
 230         if (uploadType 
== UPLOAD_SINGLE_FILE
) { 
 232             if (intent
.hasExtra(KEY_FILE
)) { 
 233                 files 
= new OCFile
[] { (OCFile
) intent
.getParcelableExtra(KEY_FILE
) }; 
 236                 localPaths 
= new String
[] { intent
.getStringExtra(KEY_LOCAL_FILE
) }; 
 237                 remotePaths 
= new String
[] { intent
.getStringExtra(KEY_REMOTE_FILE
) }; 
 238                 mimeTypes 
= new String
[] { intent
.getStringExtra(KEY_MIME_TYPE
) }; 
 241         } else { // mUploadType == UPLOAD_MULTIPLE_FILES 
 243             if (intent
.hasExtra(KEY_FILE
)) { 
 244                 files 
= (OCFile
[]) intent
.getParcelableArrayExtra(KEY_FILE
); // TODO 
 252                 localPaths 
= intent
.getStringArrayExtra(KEY_LOCAL_FILE
); 
 253                 remotePaths 
= intent
.getStringArrayExtra(KEY_REMOTE_FILE
); 
 254                 mimeTypes 
= intent
.getStringArrayExtra(KEY_MIME_TYPE
); 
 258         FileDataStorageManager storageManager 
= new FileDataStorageManager(account
, 
 259                 getContentResolver()); 
 261         boolean forceOverwrite 
= intent
.getBooleanExtra(KEY_FORCE_OVERWRITE
, false
); 
 262         boolean isInstant 
= intent
.getBooleanExtra(KEY_INSTANT_UPLOAD
, false
); 
 263         int localAction 
= intent
.getIntExtra(KEY_LOCAL_BEHAVIOUR
, LOCAL_BEHAVIOUR_COPY
); 
 265         if (intent
.hasExtra(KEY_FILE
) && files 
== null
) { 
 266             Log_OC
.e(TAG
, "Incorrect array for OCFiles provided in upload intent"); 
 267             return Service
.START_NOT_STICKY
; 
 269         } else if (!intent
.hasExtra(KEY_FILE
)) { 
 270             if (localPaths 
== null
) { 
 271                 Log_OC
.e(TAG
, "Incorrect array for local paths provided in upload intent"); 
 272                 return Service
.START_NOT_STICKY
; 
 274             if (remotePaths 
== null
) { 
 275                 Log_OC
.e(TAG
, "Incorrect array for remote paths provided in upload intent"); 
 276                 return Service
.START_NOT_STICKY
; 
 278             if (localPaths
.length 
!= remotePaths
.length
) { 
 279                 Log_OC
.e(TAG
, "Different number of remote paths and local paths!"); 
 280                 return Service
.START_NOT_STICKY
; 
 283             files 
= new OCFile
[localPaths
.length
]; 
 284             for (int i 
= 0; i 
< localPaths
.length
; i
++) { 
 285                 files
[i
] = obtainNewOCFileToUpload(remotePaths
[i
], localPaths
[i
], 
 286                         ((mimeTypes 
!= null
) ? mimeTypes
[i
] : (String
) null
), storageManager
); 
 287                 if (files
[i
] == null
) { 
 288                     // TODO @andomaex add failure Notification 
 289                     return Service
.START_NOT_STICKY
; 
 294         AccountManager aMgr 
= AccountManager
.get(this); 
 295         String version 
= aMgr
.getUserData(account
, Constants
.KEY_OC_VERSION
); 
 296         OwnCloudVersion ocv 
= new OwnCloudVersion(version
); 
 298         boolean chunked 
= FileUploader
.chunkedUploadIsSupported(ocv
); 
 299         AbstractList
<String
> requestedUploads 
= new Vector
<String
>(); 
 300         String uploadKey 
= null
; 
 301         UploadFileOperation newUpload 
= null
; 
 303             for (int i 
= 0; i 
< files
.length
; i
++) { 
 304                 uploadKey 
= buildRemoteName(account
, files
[i
].getRemotePath()); 
 305                 newUpload 
= new UploadFileOperation(account
, files
[i
], chunked
, isInstant
, 
 306                         forceOverwrite
, localAction
, 
 307                         getApplicationContext()); 
 309                     newUpload
.setRemoteFolderToBeCreated(); 
 311                 // Grants that the file only upload once time 
 312                 mPendingUploads
.putIfAbsent(uploadKey
, newUpload
); 
 314                 newUpload
.addDatatransferProgressListener(this); 
 315                 newUpload
.addDatatransferProgressListener((FileUploaderBinder
)mBinder
); 
 316                 requestedUploads
.add(uploadKey
); 
 319         } catch (IllegalArgumentException e
) { 
 320             Log_OC
.e(TAG
, "Not enough information provided in intent: " + e
.getMessage()); 
 321             return START_NOT_STICKY
; 
 323         } catch (IllegalStateException e
) { 
 324             Log_OC
.e(TAG
, "Bad information provided in intent: " + e
.getMessage()); 
 325             return START_NOT_STICKY
; 
 327         } catch (Exception e
) { 
 328             Log_OC
.e(TAG
, "Unexpected exception while processing upload intent", e
); 
 329             return START_NOT_STICKY
; 
 333         if (requestedUploads
.size() > 0) { 
 334             Message msg 
= mServiceHandler
.obtainMessage(); 
 336             msg
.obj 
= requestedUploads
; 
 337             mServiceHandler
.sendMessage(msg
); 
 339         Log_OC
.i(TAG
, "mPendingUploads size:" + mPendingUploads
.size()); 
 340         return Service
.START_NOT_STICKY
; 
 344      * Provides a binder object that clients can use to perform operations on 
 345      * the queue of uploads, excepting the addition of new files. 
 347      * Implemented to perform cancellation, pause and resume of existing 
 351     public IBinder 
onBind(Intent arg0
) { 
 356      * Called when ALL the bound clients were onbound. 
 359     public boolean onUnbind(Intent intent
) { 
 360         ((FileUploaderBinder
)mBinder
).clearListeners(); 
 361         return false
;   // not accepting rebinding (default behaviour) 
 365     public void onAccountsUpdated(Account
[] accounts
) { 
 366         // Review current upload, and cancel it if its account doen't exist 
 367         if (mCurrentUpload 
!= null 
&& 
 368                 !AccountUtils
.exists(mCurrentUpload
.getAccount(), getApplicationContext())) { 
 369             mCurrentUpload
.cancel(); 
 371         // The rest of uploads are cancelled when they try to start 
 375      * Binder to let client components to perform operations on the queue of 
 378      * It provides by itself the available operations. 
 380     public class FileUploaderBinder 
extends Binder 
implements OnDatatransferProgressListener 
{ 
 383          * Map of listeners that will be reported about progress of uploads from a 
 384          * {@link FileUploaderBinder} instance 
 386         private Map
<String
, OnDatatransferProgressListener
> mBoundListeners 
= new HashMap
<String
, OnDatatransferProgressListener
>(); 
 389          * Cancels a pending or current upload of a remote file. 
 391          * @param account Owncloud account where the remote file will be stored. 
 392          * @param file A file in the queue of pending uploads 
 394         public void cancel(Account account
, OCFile file
) { 
 395             UploadFileOperation upload 
= null
; 
 396             synchronized (mPendingUploads
) { 
 397                 upload 
= mPendingUploads
.remove(buildRemoteName(account
, file
)); 
 399             if (upload 
!= null
) { 
 405          * Cancels a pending or current upload for an account 
 407          * @param account Owncloud accountName where the remote file will be stored. 
 409         public void cancel(Account account
) { 
 410             Log_OC
.d(TAG
, "Account= " + account
.name
); 
 412             if (mCurrentUpload 
!= null
) { 
 413                 Log_OC
.d(TAG
, "Current Upload Account= " + mCurrentUpload
.getAccount().name
); 
 414                 if (mCurrentUpload
.getAccount().name
.equals(account
.name
)) { 
 415                     mCurrentUpload
.cancel(); 
 418             // Cancel pending uploads 
 419             cancelUploadForAccount(account
.name
); 
 422         public void clearListeners() { 
 423             mBoundListeners
.clear(); 
 427          * Returns True when the file described by 'file' is being uploaded to 
 428          * the ownCloud account 'account' or waiting for it 
 430          * If 'file' is a directory, returns 'true' if some of its descendant files 
 431          * is uploading or waiting to upload. 
 433          * @param account   ownCloud account where the remote file will be stored. 
 434          * @param file      A file that could be in the queue of pending uploads 
 436         public boolean isUploading(Account account
, OCFile file
) { 
 437             if (account 
== null 
|| file 
== null
) 
 439             String targetKey 
= buildRemoteName(account
, file
); 
 440             synchronized (mPendingUploads
) { 
 441                 if (file
.isFolder()) { 
 442                     // this can be slow if there are many uploads :( 
 443                     Iterator
<String
> it 
= mPendingUploads
.keySet().iterator(); 
 444                     boolean found 
= false
; 
 445                     while (it
.hasNext() && !found
) { 
 446                         found 
= it
.next().startsWith(targetKey
); 
 450                     return (mPendingUploads
.containsKey(targetKey
)); 
 457          * Adds a listener interested in the progress of the upload for a concrete file. 
 459          * @param listener      Object to notify about progress of transfer.     
 460          * @param account       ownCloud account holding the file of interest. 
 461          * @param file          {@link OCFile} of interest for listener. 
 463         public void addDatatransferProgressListener (OnDatatransferProgressListener listener
, 
 464                                                      Account account
, OCFile file
) { 
 465             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 466             String targetKey 
= buildRemoteName(account
, file
); 
 467             mBoundListeners
.put(targetKey
, listener
); 
 473          * Removes a listener interested in the progress of the upload for a concrete file. 
 475          * @param listener      Object to notify about progress of transfer.     
 476          * @param account       ownCloud account holding the file of interest. 
 477          * @param file          {@link OCFile} of interest for listener. 
 479         public void removeDatatransferProgressListener (OnDatatransferProgressListener listener
, 
 480                                                         Account account
, OCFile file
) { 
 481             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 482             String targetKey 
= buildRemoteName(account
, file
); 
 483             if (mBoundListeners
.get(targetKey
) == listener
) { 
 484                 mBoundListeners
.remove(targetKey
); 
 490         public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, 
 491                                        long totalToTransfer
, String fileName
) { 
 492             String key 
= buildRemoteName(mCurrentUpload
.getAccount(), mCurrentUpload
.getFile()); 
 493             OnDatatransferProgressListener boundListener 
= mBoundListeners
.get(key
); 
 494             if (boundListener 
!= null
) { 
 495                 boundListener
.onTransferProgress(progressRate
, totalTransferredSoFar
, 
 496                         totalToTransfer
, fileName
); 
 501          * Review uploads and cancel it if its account doesn't exist 
 503         public void checkAccountOfCurrentUpload() { 
 504             if (mCurrentUpload 
!= null 
&& 
 505                     !AccountUtils
.exists(mCurrentUpload
.getAccount(), getApplicationContext())) { 
 506                 mCurrentUpload
.cancel(); 
 508             // The rest of uploads are cancelled when they try to start 
 513      * Upload worker. Performs the pending uploads in the order they were 
 516      * Created with the Looper of a new thread, started in 
 517      * {@link FileUploader#onCreate()}. 
 519     private static class ServiceHandler 
extends Handler 
{ 
 520         // don't make it a final class, and don't remove the static ; lint will 
 521         // warn about a possible memory leak 
 522         FileUploader mService
; 
 524         public ServiceHandler(Looper looper
, FileUploader service
) { 
 527                 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); 
 532         public void handleMessage(Message msg
) { 
 533             @SuppressWarnings("unchecked") 
 534             AbstractList
<String
> requestedUploads 
= (AbstractList
<String
>) msg
.obj
; 
 535             if (msg
.obj 
!= null
) { 
 536                 Iterator
<String
> it 
= requestedUploads
.iterator(); 
 537                 while (it
.hasNext()) { 
 538                     mService
.uploadFile(it
.next()); 
 541             Log_OC
.d(TAG
, "Stopping command after id " + msg
.arg1
); 
 542             mService
.stopSelf(msg
.arg1
); 
 547      * Core upload method: sends the file(s) to upload 
 549      * @param uploadKey Key to access the upload to perform, contained in 
 552     public void uploadFile(String uploadKey
) { 
 554         synchronized (mPendingUploads
) { 
 555             mCurrentUpload 
= mPendingUploads
.get(uploadKey
); 
 558         if (mCurrentUpload 
!= null
) { 
 560             // Detect if the account exists 
 561             if (AccountUtils
.exists(mCurrentUpload
.getAccount(), getApplicationContext())) { 
 562                 Log_OC
.d(TAG
, "Account " + mCurrentUpload
.getAccount().name 
+ " exists"); 
 564                 notifyUploadStart(mCurrentUpload
); 
 566                 RemoteOperationResult uploadResult 
= null
, grantResult 
= null
; 
 569                     /// prepare client object to send requests to the ownCloud server 
 570                     if (mUploadClient 
== null 
|| 
 571                             !mLastAccount
.equals(mCurrentUpload
.getAccount())) { 
 572                         mLastAccount 
= mCurrentUpload
.getAccount(); 
 574                                 new FileDataStorageManager(mLastAccount
, getContentResolver()); 
 575                         OwnCloudAccount ocAccount 
= new OwnCloudAccount(mLastAccount
, this); 
 576                         mUploadClient 
= OwnCloudClientManagerFactory
.getDefaultSingleton(). 
 577                                 getClientFor(ocAccount
, this, MainApp
.getUserAgent()); 
 580                     /// check the existence of the parent folder for the file to upload 
 581                     String remoteParentPath 
= new File(mCurrentUpload
.getRemotePath()).getParent(); 
 582                     remoteParentPath 
= remoteParentPath
.endsWith(OCFile
.PATH_SEPARATOR
) ?
 
 583                             remoteParentPath 
: remoteParentPath 
+ OCFile
.PATH_SEPARATOR
; 
 584                     grantResult 
= grantFolderExistence(remoteParentPath
); 
 586                     /// perform the upload 
 587                     if (grantResult
.isSuccess()) { 
 588                         OCFile parent 
= mStorageManager
.getFileByPath(remoteParentPath
); 
 589                         mCurrentUpload
.getFile().setParentId(parent
.getFileId()); 
 590                         uploadResult 
= mCurrentUpload
.execute(mUploadClient
, 
 591                                 MainApp
.getUserAgent()); 
 592                         if (uploadResult
.isSuccess()) { 
 596                         uploadResult 
= grantResult
; 
 599                 } catch (AccountsException e
) { 
 600                     Log_OC
.e(TAG
, "Error while trying to get autorization for " + 
 601                             mLastAccount
.name
, e
); 
 602                     uploadResult 
= new RemoteOperationResult(e
); 
 604                 } catch (IOException e
) { 
 605                     Log_OC
.e(TAG
, "Error while trying to get autorization for " + 
 606                             mLastAccount
.name
, e
); 
 607                     uploadResult 
= new RemoteOperationResult(e
); 
 610                     synchronized (mPendingUploads
) { 
 611                         mPendingUploads
.remove(uploadKey
); 
 612                         Log_OC
.i(TAG
, "Remove CurrentUploadItem from pending upload Item Map."); 
 614                     if (uploadResult
.isException()) { 
 615                         // enforce the creation of a new client object for next uploads; 
 616                         // this grant that a new socket will be created in the future if 
 617                         // the current exception is due to an abrupt lose of network connection 
 618                         mUploadClient 
= null
; 
 623                 notifyUploadResult(uploadResult
, mCurrentUpload
); 
 624                 sendFinalBroadcast(mCurrentUpload
, uploadResult
); 
 627                 // Cancel the transfer 
 628                 Log_OC
.d(TAG
, "Account " + mCurrentUpload
.getAccount().toString() + 
 630                 cancelUploadForAccount(mCurrentUpload
.getAccount().name
); 
 638      * Checks the existence of the folder where the current file will be uploaded both 
 639      * in the remote server and in the local database. 
 641      * If the upload is set to enforce the creation of the folder, the method tries to 
 642      * create it both remote and locally. 
 644      *  @param  pathToGrant     Full remote path whose existence will be granted. 
 645      *  @return  An {@link OCFile} instance corresponding to the folder where the file 
 648     private RemoteOperationResult 
grantFolderExistence(String pathToGrant
) { 
 649         RemoteOperation operation 
= new ExistenceCheckRemoteOperation(pathToGrant
, this, false
); 
 650         RemoteOperationResult result 
= operation
.execute(mUploadClient
, MainApp
.getUserAgent()); 
 651         if (!result
.isSuccess() && result
.getCode() == ResultCode
.FILE_NOT_FOUND 
&& 
 652                 mCurrentUpload
.isRemoteFolderToBeCreated()) { 
 653             SyncOperation syncOp 
= new CreateFolderOperation( pathToGrant
, true
); 
 654             result 
= syncOp
.execute(mUploadClient
, mStorageManager
); 
 656         if (result
.isSuccess()) { 
 657             OCFile parentDir 
= mStorageManager
.getFileByPath(pathToGrant
); 
 658             if (parentDir 
== null
) { 
 659                 parentDir 
= createLocalFolder(pathToGrant
); 
 661             if (parentDir 
!= null
) { 
 662                 result 
= new RemoteOperationResult(ResultCode
.OK
); 
 664                 result 
= new RemoteOperationResult(ResultCode
.UNKNOWN_ERROR
); 
 671     private OCFile 
createLocalFolder(String remotePath
) { 
 672         String parentPath 
= new File(remotePath
).getParent(); 
 673         parentPath 
= parentPath
.endsWith(OCFile
.PATH_SEPARATOR
) ?
 
 674                 parentPath 
: parentPath 
+ OCFile
.PATH_SEPARATOR
; 
 675         OCFile parent 
= mStorageManager
.getFileByPath(parentPath
); 
 676         if (parent 
== null
) { 
 677             parent 
= createLocalFolder(parentPath
); 
 679         if (parent 
!= null
) { 
 680             OCFile createdFolder 
= new OCFile(remotePath
); 
 681             createdFolder
.setMimetype("DIR"); 
 682             createdFolder
.setParentId(parent
.getFileId()); 
 683             mStorageManager
.saveFile(createdFolder
); 
 684             return createdFolder
; 
 691      * Saves a OC File after a successful upload. 
 693      * A PROPFIND is necessary to keep the props in the local database 
 694      * synchronized with the server, specially the modification time and Etag 
 697      * TODO refactor this ugly thing 
 699     private void saveUploadedFile() { 
 700         OCFile file 
= mCurrentUpload
.getFile(); 
 701         if (file
.fileExists()) { 
 702             file 
= mStorageManager
.getFileById(file
.getFileId()); 
 704         long syncDate 
= System
.currentTimeMillis(); 
 705         file
.setLastSyncDateForData(syncDate
); 
 707         // new PROPFIND to keep data consistent with server  
 708         // in theory, should return the same we already have 
 709         ReadRemoteFileOperation operation 
= 
 710                 new ReadRemoteFileOperation(mCurrentUpload
.getRemotePath()); 
 711         RemoteOperationResult result 
= operation
.execute(mUploadClient
, MainApp
.getUserAgent()); 
 712         if (result
.isSuccess()) { 
 713             updateOCFile(file
, (RemoteFile
) result
.getData().get(0)); 
 714             file
.setLastSyncDateForProperties(syncDate
); 
 717         // / maybe this would be better as part of UploadFileOperation... or 
 718         // maybe all this method 
 719         if (mCurrentUpload
.wasRenamed()) { 
 720             OCFile oldFile 
= mCurrentUpload
.getOldFile(); 
 721             if (oldFile
.fileExists()) { 
 722                 oldFile
.setStoragePath(null
); 
 723                 mStorageManager
.saveFile(oldFile
); 
 725             } // else: it was just an automatic renaming due to a name 
 726             // coincidence; nothing else is needed, the storagePath is right 
 727             // in the instance returned by mCurrentUpload.getFile() 
 729         file
.setNeedsUpdateThumbnail(true
); 
 730         mStorageManager
.saveFile(file
); 
 733     private void updateOCFile(OCFile file
, RemoteFile remoteFile
) { 
 734         file
.setCreationTimestamp(remoteFile
.getCreationTimestamp()); 
 735         file
.setFileLength(remoteFile
.getLength()); 
 736         file
.setMimetype(remoteFile
.getMimeType()); 
 737         file
.setModificationTimestamp(remoteFile
.getModifiedTimestamp()); 
 738         file
.setModificationTimestampAtLastSyncForData(remoteFile
.getModifiedTimestamp()); 
 739         // file.setEtag(remoteFile.getEtag());    // TODO Etag, where available 
 740         file
.setRemoteId(remoteFile
.getRemoteId()); 
 743     private OCFile 
obtainNewOCFileToUpload(String remotePath
, String localPath
, String mimeType
, 
 744                                            FileDataStorageManager storageManager
) { 
 747         if (mimeType 
== null 
|| mimeType
.length() <= 0) { 
 749                 mimeType 
= MimeTypeMap
.getSingleton().getMimeTypeFromExtension( 
 750                         remotePath
.substring(remotePath
.lastIndexOf('.') + 1)); 
 751             } catch (IndexOutOfBoundsException e
) { 
 752                 Log_OC
.e(TAG
, "Trying to find out MIME type of a file without extension: " + 
 756         if (mimeType 
== null
) { 
 757             mimeType 
= "application/octet-stream"; 
 760         if (isPdfFileFromContentProviderWithoutExtension(localPath
, mimeType
)){ 
 761             remotePath 
+= FILE_EXTENSION_PDF
; 
 764         OCFile newFile 
= new OCFile(remotePath
); 
 765         newFile
.setStoragePath(localPath
); 
 766         newFile
.setLastSyncDateForProperties(0); 
 767         newFile
.setLastSyncDateForData(0); 
 770         if (localPath 
!= null 
&& localPath
.length() > 0) { 
 771             File localFile 
= new File(localPath
); 
 772             newFile
.setFileLength(localFile
.length()); 
 773             newFile
.setLastSyncDateForData(localFile
.lastModified()); 
 774         } // don't worry about not assigning size, the problems with localPath 
 775         // are checked when the UploadFileOperation instance is created 
 778         newFile
.setMimetype(mimeType
); 
 784      * Creates a status notification to show the upload progress 
 786      * @param upload Upload operation starting. 
 788     private void notifyUploadStart(UploadFileOperation upload
) { 
 789         // / create status notification with a progress bar 
 791         mNotificationBuilder 
= 
 792                 NotificationBuilderWithProgressBar
.newNotificationBuilderWithProgressBar(this); 
 795                 .setSmallIcon(R
.drawable
.notification_icon
) 
 796                 .setTicker(getString(R
.string
.uploader_upload_in_progress_ticker
)) 
 797                 .setContentTitle(getString(R
.string
.uploader_upload_in_progress_ticker
)) 
 798                 .setProgress(100, 0, false
) 
 800                         String
.format(getString(R
.string
.uploader_upload_in_progress_content
), 0, upload
.getFileName())); 
 802         /// includes a pending intent in the notification showing the details view of the file 
 803         Intent showDetailsIntent 
= new Intent(this, FileDisplayActivity
.class); 
 804         showDetailsIntent
.putExtra(FileActivity
.EXTRA_FILE
, upload
.getFile()); 
 805         showDetailsIntent
.putExtra(FileActivity
.EXTRA_ACCOUNT
, upload
.getAccount()); 
 806         showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 807         mNotificationBuilder
.setContentIntent(PendingIntent
.getActivity( 
 808                 this, (int) System
.currentTimeMillis(), showDetailsIntent
, 0 
 811         mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotificationBuilder
.build()); 
 815      * Callback method to update the progress bar in the status notification 
 818     public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, 
 819                                    long totalToTransfer
, String filePath
) { 
 820         int percent 
= (int) (100.0 * ((double) totalTransferredSoFar
) / ((double) totalToTransfer
)); 
 821         if (percent 
!= mLastPercent
) { 
 822             mNotificationBuilder
.setProgress(100, percent
, false
); 
 823             String fileName 
= filePath
.substring( 
 824                     filePath
.lastIndexOf(FileUtils
.PATH_SEPARATOR
) + 1); 
 825             String text 
= String
.format(getString(R
.string
.uploader_upload_in_progress_content
), percent
, fileName
); 
 826             mNotificationBuilder
.setContentText(text
); 
 827             mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotificationBuilder
.build()); 
 829         mLastPercent 
= percent
; 
 833      * Updates the status notification with the result of an upload operation. 
 835      * @param uploadResult Result of the upload operation. 
 836      * @param upload Finished upload operation 
 838     private void notifyUploadResult( 
 839             RemoteOperationResult uploadResult
, UploadFileOperation upload
) { 
 840         Log_OC
.d(TAG
, "NotifyUploadResult with resultCode: " + uploadResult
.getCode()); 
 841         // / cancelled operation or success -> silent removal of progress notification 
 842         mNotificationManager
.cancel(R
.string
.uploader_upload_in_progress_ticker
); 
 844         // Show the result: success or fail notification 
 845         if (!uploadResult
.isCancelled()) { 
 846             int tickerId 
= (uploadResult
.isSuccess()) ? R
.string
.uploader_upload_succeeded_ticker 
: 
 847                     R
.string
.uploader_upload_failed_ticker
; 
 849             String content 
= null
; 
 851             // check credentials error 
 852             boolean needsToUpdateCredentials 
= ( 
 853                     uploadResult
.getCode() == ResultCode
.UNAUTHORIZED 
|| 
 854                             uploadResult
.isIdPRedirection() 
 856             tickerId 
= (needsToUpdateCredentials
) ?
 
 857                     R
.string
.uploader_upload_failed_credentials_error 
: tickerId
; 
 860                     .setTicker(getString(tickerId
)) 
 861                     .setContentTitle(getString(tickerId
)) 
 864                     .setProgress(0, 0, false
); 
 866             content 
=  ErrorMessageAdapter
.getErrorCauseMessage( 
 867                     uploadResult
, upload
, getResources() 
 870             if (needsToUpdateCredentials
) { 
 871                 // let the user update credentials with one click 
 872                 Intent updateAccountCredentials 
= new Intent(this, AuthenticatorActivity
.class); 
 873                 updateAccountCredentials
.putExtra( 
 874                         AuthenticatorActivity
.EXTRA_ACCOUNT
, upload
.getAccount() 
 876                 updateAccountCredentials
.putExtra( 
 877                         AuthenticatorActivity
.EXTRA_ACTION
, 
 878                         AuthenticatorActivity
.ACTION_UPDATE_EXPIRED_TOKEN
 
 880                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
); 
 881                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
); 
 882                 updateAccountCredentials
.addFlags(Intent
.FLAG_FROM_BACKGROUND
); 
 883                 mNotificationBuilder
.setContentIntent(PendingIntent
.getActivity( 
 885                         (int) System
.currentTimeMillis(), 
 886                         updateAccountCredentials
, 
 887                         PendingIntent
.FLAG_ONE_SHOT
 
 890                 mUploadClient 
= null
; 
 891                 // grant that future retries on the same account will get the fresh credentials 
 893                 mNotificationBuilder
.setContentText(content
); 
 895                 if (upload
.isInstant()) { 
 898                         db 
= new DbHandler(this.getBaseContext()); 
 899                         String message 
= uploadResult
.getLogMessage() + " errorCode: " + 
 900                                 uploadResult
.getCode(); 
 901                         Log_OC
.e(TAG
, message 
+ " Http-Code: " + uploadResult
.getHttpCode()); 
 902                         if (uploadResult
.getCode() == ResultCode
.QUOTA_EXCEEDED
) { 
 903                             //message = getString(R.string.failed_upload_quota_exceeded_text); 
 904                             if (db
.updateFileState( 
 905                                     upload
.getOriginalStoragePath(), 
 906                                     DbHandler
.UPLOAD_STATUS_UPLOAD_FAILED
, 
 909                                         upload
.getOriginalStoragePath(), 
 910                                         upload
.getAccount().name
, 
 923             mNotificationBuilder
.setContentText(content
); 
 924             mNotificationManager
.notify(tickerId
, mNotificationBuilder
.build()); 
 926             if (uploadResult
.isSuccess()) { 
 928                 DbHandler db 
= new DbHandler(this.getBaseContext()); 
 929                 db
.removeIUPendingFile(mCurrentUpload
.getOriginalStoragePath()); 
 932                 // remove success notification, with a delay of 2 seconds 
 933                 NotificationDelayer
.cancelWithDelay( 
 934                         mNotificationManager
, 
 935                         R
.string
.uploader_upload_succeeded_ticker
, 
 943      * Sends a broadcast in order to the interested activities can update their 
 946      * @param upload Finished upload operation 
 947      * @param uploadResult Result of the upload operation 
 949     private void sendFinalBroadcast(UploadFileOperation upload
, RemoteOperationResult uploadResult
) { 
 950         Intent end 
= new Intent(getUploadFinishMessage()); 
 951         end
.putExtra(EXTRA_REMOTE_PATH
, upload
.getRemotePath()); // real remote 
 956         if (upload
.wasRenamed()) { 
 957             end
.putExtra(EXTRA_OLD_REMOTE_PATH
, upload
.getOldFile().getRemotePath()); 
 959         end
.putExtra(EXTRA_OLD_FILE_PATH
, upload
.getOriginalStoragePath()); 
 960         end
.putExtra(ACCOUNT_NAME
, upload
.getAccount().name
); 
 961         end
.putExtra(EXTRA_UPLOAD_RESULT
, uploadResult
.isSuccess()); 
 962         sendStickyBroadcast(end
); 
 966      * Checks if content provider, using the content:// scheme, returns a file with mime-type  
 967      * 'application/pdf' but file has not extension 
 970      * @return true if is needed to add the pdf file extension to the file 
 972     private boolean isPdfFileFromContentProviderWithoutExtension(String localPath
, 
 974         return localPath
.startsWith(UriUtils
.URI_CONTENT_SCHEME
) && 
 975                 mimeType
.equals(MIME_TYPE_PDF
) && 
 976                 !localPath
.endsWith(FILE_EXTENSION_PDF
); 
 980      * Remove uploads of an account 
 983     private void cancelUploadForAccount(String accountName
){ 
 984         // this can be slow if there are many uploads :( 
 985         Iterator
<String
> it 
= mPendingUploads
.keySet().iterator(); 
 986         Log_OC
.d(TAG
, "Number of pending updloads= "  + mPendingUploads
.size()); 
 987         while (it
.hasNext()) { 
 988             String key 
= it
.next(); 
 989             Log_OC
.d(TAG
, "mPendingUploads CANCELLED " + key
); 
 990             if (key
.startsWith(accountName
)) { 
 991                 synchronized (mPendingUploads
) { 
 992                     mPendingUploads
.remove(key
);