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 android
.accounts
.Account
; 
  32 import android
.accounts
.AccountManager
; 
  33 import android
.accounts
.AccountsException
; 
  34 import android
.app
.NotificationManager
; 
  35 import android
.app
.PendingIntent
; 
  36 import android
.app
.Service
; 
  37 import android
.content
.Intent
; 
  38 import android
.os
.Binder
; 
  39 import android
.os
.Handler
; 
  40 import android
.os
.HandlerThread
; 
  41 import android
.os
.IBinder
; 
  42 import android
.os
.Looper
; 
  43 import android
.os
.Message
; 
  44 import android
.os
.Process
; 
  45 import android
.support
.v4
.app
.NotificationCompat
; 
  46 import android
.webkit
.MimeTypeMap
; 
  48 import com
.owncloud
.android
.R
; 
  49 import com
.owncloud
.android
.authentication
.AccountUtils
; 
  50 import com
.owncloud
.android
.authentication
.AuthenticatorActivity
; 
  51 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  52 import com
.owncloud
.android
.datamodel
.OCFile
; 
  53 import com
.owncloud
.android
.db
.DbHandler
; 
  54 import com
.owncloud
.android
.lib
.common
.OwnCloudAccount
; 
  55 import com
.owncloud
.android
.lib
.common
.OwnCloudClient
; 
  56 import com
.owncloud
.android
.lib
.common
.OwnCloudClientManagerFactory
; 
  57 import com
.owncloud
.android
.lib
.common
.accounts
.AccountUtils
.Constants
; 
  58 import com
.owncloud
.android
.lib
.common
.network
.OnDatatransferProgressListener
; 
  59 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperation
; 
  60 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
; 
  61 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
.ResultCode
; 
  62 import com
.owncloud
.android
.lib
.common
.utils
.Log_OC
; 
  63 import com
.owncloud
.android
.lib
.resources
.files
.ExistenceCheckRemoteOperation
; 
  64 import com
.owncloud
.android
.lib
.resources
.files
.FileUtils
; 
  65 import com
.owncloud
.android
.lib
.resources
.files
.ReadRemoteFileOperation
; 
  66 import com
.owncloud
.android
.lib
.resources
.files
.RemoteFile
; 
  67 import com
.owncloud
.android
.lib
.resources
.status
.OwnCloudVersion
; 
  68 import com
.owncloud
.android
.notifications
.NotificationBuilderWithProgressBar
; 
  69 import com
.owncloud
.android
.notifications
.NotificationDelayer
; 
  70 import com
.owncloud
.android
.operations
.CreateFolderOperation
; 
  71 import com
.owncloud
.android
.operations
.UploadFileOperation
; 
  72 import com
.owncloud
.android
.operations
.common
.SyncOperation
; 
  73 import com
.owncloud
.android
.ui
.activity
.FileActivity
; 
  74 import com
.owncloud
.android
.ui
.activity
.FileDisplayActivity
; 
  75 import com
.owncloud
.android
.utils
.ErrorMessageAdapter
; 
  76 import com
.owncloud
.android
.utils
.UriUtils
; 
  80 public class FileUploader 
extends Service 
implements OnDatatransferProgressListener 
{ 
  82     private static final String UPLOAD_FINISH_MESSAGE 
= "UPLOAD_FINISH"; 
  83     public static final String EXTRA_UPLOAD_RESULT 
= "RESULT"; 
  84     public static final String EXTRA_REMOTE_PATH 
= "REMOTE_PATH"; 
  85     public static final String EXTRA_OLD_REMOTE_PATH 
= "OLD_REMOTE_PATH"; 
  86     public static final String EXTRA_OLD_FILE_PATH 
= "OLD_FILE_PATH"; 
  87     public static final String ACCOUNT_NAME 
= "ACCOUNT_NAME"; 
  89     public static final String KEY_FILE 
= "FILE"; 
  90     public static final String KEY_LOCAL_FILE 
= "LOCAL_FILE"; 
  91     public static final String KEY_REMOTE_FILE 
= "REMOTE_FILE"; 
  92     public static final String KEY_MIME_TYPE 
= "MIME_TYPE"; 
  94     public static final String KEY_ACCOUNT 
= "ACCOUNT"; 
  96     public static final String KEY_UPLOAD_TYPE 
= "UPLOAD_TYPE"; 
  97     public static final String KEY_FORCE_OVERWRITE 
= "KEY_FORCE_OVERWRITE"; 
  98     public static final String KEY_INSTANT_UPLOAD 
= "INSTANT_UPLOAD"; 
  99     public static final String KEY_LOCAL_BEHAVIOUR 
= "BEHAVIOUR"; 
 101     public static final int LOCAL_BEHAVIOUR_COPY 
= 0; 
 102     public static final int LOCAL_BEHAVIOUR_MOVE 
= 1; 
 103     public static final int LOCAL_BEHAVIOUR_FORGET 
= 2; 
 105     public static final int UPLOAD_SINGLE_FILE 
= 0; 
 106     public static final int UPLOAD_MULTIPLE_FILES 
= 1; 
 108     private static final String TAG 
= FileUploader
.class.getSimpleName(); 
 110     private Looper mServiceLooper
; 
 111     private ServiceHandler mServiceHandler
; 
 112     private IBinder mBinder
; 
 113     private OwnCloudClient mUploadClient 
= null
; 
 114     private Account mLastAccount 
= null
; 
 115     private FileDataStorageManager mStorageManager
; 
 117     private ConcurrentMap
<String
, UploadFileOperation
> mPendingUploads 
= new ConcurrentHashMap
<String
, UploadFileOperation
>(); 
 118     private UploadFileOperation mCurrentUpload 
= null
; 
 120     private NotificationManager mNotificationManager
; 
 121     private NotificationCompat
.Builder mNotificationBuilder
; 
 122     private int mLastPercent
; 
 124     private static final String MIME_TYPE_PDF 
= "application/pdf"; 
 125     private static final String FILE_EXTENSION_PDF 
= ".pdf"; 
 128     public static String 
getUploadFinishMessage() { 
 129         return FileUploader
.class.getName().toString() + UPLOAD_FINISH_MESSAGE
; 
 133      * Builds a key for mPendingUploads from the account and file to upload 
 135      * @param account   Account where the file to upload is stored 
 136      * @param file      File to upload 
 138     private String 
buildRemoteName(Account account
, OCFile file
) { 
 139         return account
.name 
+ file
.getRemotePath(); 
 142     private String 
buildRemoteName(Account account
, String remotePath
) { 
 143         return account
.name 
+ remotePath
; 
 147      * Checks if an ownCloud server version should support chunked uploads. 
 149      * @param version OwnCloud version instance corresponding to an ownCloud 
 151      * @return 'True' if the ownCloud server with version supports chunked 
 154     private static boolean chunkedUploadIsSupported(OwnCloudVersion version
) { 
 155         return (version 
!= null 
&& version
.compareTo(OwnCloudVersion
.owncloud_v4_5
) >= 0); 
 159      * Service initialization 
 162     public void onCreate() { 
 164         Log_OC
.i(TAG
, "mPendingUploads size:" + mPendingUploads
.size()); 
 165         mNotificationManager 
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
); 
 166         HandlerThread thread 
= new HandlerThread("FileUploaderThread", Process
.THREAD_PRIORITY_BACKGROUND
); 
 168         mServiceLooper 
= thread
.getLooper(); 
 169         mServiceHandler 
= new ServiceHandler(mServiceLooper
, this); 
 170         mBinder 
= new FileUploaderBinder(); 
 174      * Entry point to add one or several files to the queue of uploads. 
 176      * New uploads are added calling to startService(), resulting in a call to 
 177      * this method. This ensures the service will keep on working although the 
 178      * caller activity goes away. 
 181     public int onStartCommand(Intent intent
, int flags
, int startId
) { 
 182         if (!intent
.hasExtra(KEY_ACCOUNT
) || !intent
.hasExtra(KEY_UPLOAD_TYPE
) 
 183                 || !(intent
.hasExtra(KEY_LOCAL_FILE
) || intent
.hasExtra(KEY_FILE
))) { 
 184             Log_OC
.e(TAG
, "Not enough information provided in intent"); 
 185             return Service
.START_NOT_STICKY
; 
 187         int uploadType 
= intent
.getIntExtra(KEY_UPLOAD_TYPE
, -1); 
 188         if (uploadType 
== -1) { 
 189             Log_OC
.e(TAG
, "Incorrect upload type provided"); 
 190             return Service
.START_NOT_STICKY
; 
 192         Account account 
= intent
.getParcelableExtra(KEY_ACCOUNT
); 
 193         if (!AccountUtils
.exists(account
, getApplicationContext())) { 
 194             return Service
.START_NOT_STICKY
; 
 197         String
[] localPaths 
= null
, remotePaths 
= null
, mimeTypes 
= null
; 
 198         OCFile
[] files 
= null
; 
 199         if (uploadType 
== UPLOAD_SINGLE_FILE
) { 
 201             if (intent
.hasExtra(KEY_FILE
)) { 
 202                 files 
= new OCFile
[] { (OCFile
) intent
.getParcelableExtra(KEY_FILE
) }; 
 205                 localPaths 
= new String
[] { intent
.getStringExtra(KEY_LOCAL_FILE
) }; 
 206                 remotePaths 
= new String
[] { intent
.getStringExtra(KEY_REMOTE_FILE
) }; 
 207                 mimeTypes 
= new String
[] { intent
.getStringExtra(KEY_MIME_TYPE
) }; 
 210         } else { // mUploadType == UPLOAD_MULTIPLE_FILES 
 212             if (intent
.hasExtra(KEY_FILE
)) { 
 213                 files 
= (OCFile
[]) intent
.getParcelableArrayExtra(KEY_FILE
); // TODO 
 221                 localPaths 
= intent
.getStringArrayExtra(KEY_LOCAL_FILE
); 
 222                 remotePaths 
= intent
.getStringArrayExtra(KEY_REMOTE_FILE
); 
 223                 mimeTypes 
= intent
.getStringArrayExtra(KEY_MIME_TYPE
); 
 227         FileDataStorageManager storageManager 
= new FileDataStorageManager(account
, getContentResolver()); 
 229         boolean forceOverwrite 
= intent
.getBooleanExtra(KEY_FORCE_OVERWRITE
, false
); 
 230         boolean isInstant 
= intent
.getBooleanExtra(KEY_INSTANT_UPLOAD
, false
); 
 231         int localAction 
= intent
.getIntExtra(KEY_LOCAL_BEHAVIOUR
, LOCAL_BEHAVIOUR_COPY
); 
 233         if (intent
.hasExtra(KEY_FILE
) && files 
== null
) { 
 234             Log_OC
.e(TAG
, "Incorrect array for OCFiles provided in upload intent"); 
 235             return Service
.START_NOT_STICKY
; 
 237         } else if (!intent
.hasExtra(KEY_FILE
)) { 
 238             if (localPaths 
== null
) { 
 239                 Log_OC
.e(TAG
, "Incorrect array for local paths provided in upload intent"); 
 240                 return Service
.START_NOT_STICKY
; 
 242             if (remotePaths 
== null
) { 
 243                 Log_OC
.e(TAG
, "Incorrect array for remote paths provided in upload intent"); 
 244                 return Service
.START_NOT_STICKY
; 
 246             if (localPaths
.length 
!= remotePaths
.length
) { 
 247                 Log_OC
.e(TAG
, "Different number of remote paths and local paths!"); 
 248                 return Service
.START_NOT_STICKY
; 
 251             files 
= new OCFile
[localPaths
.length
]; 
 252             for (int i 
= 0; i 
< localPaths
.length
; i
++) { 
 253                 files
[i
] = obtainNewOCFileToUpload(remotePaths
[i
], localPaths
[i
], ((mimeTypes 
!= null
) ? mimeTypes
[i
] 
 254                         : (String
) null
), storageManager
); 
 255                 if (files
[i
] == null
) { 
 256                     // TODO @andomaex add failure Notification 
 257                     return Service
.START_NOT_STICKY
; 
 262         AccountManager aMgr 
= AccountManager
.get(this); 
 263         String version 
= aMgr
.getUserData(account
, Constants
.KEY_OC_VERSION
); 
 264         OwnCloudVersion ocv 
= new OwnCloudVersion(version
); 
 266         boolean chunked 
= FileUploader
.chunkedUploadIsSupported(ocv
); 
 267         AbstractList
<String
> requestedUploads 
= new Vector
<String
>(); 
 268         String uploadKey 
= null
; 
 269         UploadFileOperation newUpload 
= null
; 
 271             for (int i 
= 0; i 
< files
.length
; i
++) { 
 272                 uploadKey 
= buildRemoteName(account
, files
[i
].getRemotePath()); 
 273                 newUpload 
= new UploadFileOperation(account
, files
[i
], chunked
, isInstant
, forceOverwrite
, localAction
, 
 274                         getApplicationContext()); 
 276                     newUpload
.setRemoteFolderToBeCreated(); 
 278                 mPendingUploads
.putIfAbsent(uploadKey
, newUpload
); // Grants that the file only upload once time 
 280                 newUpload
.addDatatransferProgressListener(this); 
 281                 newUpload
.addDatatransferProgressListener((FileUploaderBinder
)mBinder
); 
 282                 requestedUploads
.add(uploadKey
); 
 285         } catch (IllegalArgumentException e
) { 
 286             Log_OC
.e(TAG
, "Not enough information provided in intent: " + e
.getMessage()); 
 287             return START_NOT_STICKY
; 
 289         } catch (IllegalStateException e
) { 
 290             Log_OC
.e(TAG
, "Bad information provided in intent: " + e
.getMessage()); 
 291             return START_NOT_STICKY
; 
 293         } catch (Exception e
) { 
 294             Log_OC
.e(TAG
, "Unexpected exception while processing upload intent", e
); 
 295             return START_NOT_STICKY
; 
 299         if (requestedUploads
.size() > 0) { 
 300             Message msg 
= mServiceHandler
.obtainMessage(); 
 302             msg
.obj 
= requestedUploads
; 
 303             mServiceHandler
.sendMessage(msg
); 
 305         Log_OC
.i(TAG
, "mPendingUploads size:" + mPendingUploads
.size()); 
 306         return Service
.START_NOT_STICKY
; 
 310      * Provides a binder object that clients can use to perform operations on 
 311      * the queue of uploads, excepting the addition of new files. 
 313      * Implemented to perform cancellation, pause and resume of existing 
 317     public IBinder 
onBind(Intent arg0
) { 
 322      * Called when ALL the bound clients were onbound. 
 325     public boolean onUnbind(Intent intent
) { 
 326         ((FileUploaderBinder
)mBinder
).clearListeners(); 
 327         return false
;   // not accepting rebinding (default behaviour) 
 331      * Binder to let client components to perform operations on the queue of 
 334      * It provides by itself the available operations. 
 336     public class FileUploaderBinder 
extends Binder 
implements OnDatatransferProgressListener 
{ 
 339          * Map of listeners that will be reported about progress of uploads from a {@link FileUploaderBinder} instance  
 341         private Map
<String
, OnDatatransferProgressListener
> mBoundListeners 
= new HashMap
<String
, OnDatatransferProgressListener
>(); 
 344          * Cancels a pending or current upload of a remote file. 
 346          * @param account Owncloud account where the remote file will be stored. 
 347          * @param file A file in the queue of pending uploads 
 349         public void cancel(Account account
, OCFile file
) { 
 350             UploadFileOperation upload 
= null
; 
 351             synchronized (mPendingUploads
) { 
 352                 upload 
= mPendingUploads
.remove(buildRemoteName(account
, file
)); 
 354             if (upload 
!= null
) { 
 360          * Cancels a pending or current upload for an account 
 362          * @param account Owncloud accountName where the remote file will be stored. 
 364         public void cancel(Account account
) { 
 365             Log_OC
.d(TAG
, "Account= " + account
.name
); 
 367             if (mCurrentUpload 
!= null
) { 
 368                 Log_OC
.d(TAG
, "Current Upload Account= " + mCurrentUpload
.getAccount().name
); 
 369                 if (mCurrentUpload
.getAccount().name 
== account
.name
) { 
 370                     mCurrentUpload
.cancel(); 
 373             // Cancel pending uploads 
 374             Iterator
<String
> it 
= mPendingUploads
.keySet().iterator(); 
 375             Log_OC
.d(TAG
, "Number of pending updloads= "  + mPendingUploads
.size()); 
 376             while (it
.hasNext()) { 
 377                 String key 
= it
.next(); 
 378                 Log_OC
.d(TAG
, "mPendingUploads CANCELLED " + key
); 
 379                 if (key
.startsWith(account
.name
)) { 
 380                     UploadFileOperation upload
; 
 381                     synchronized (mPendingUploads
) { 
 382                         upload 
= mPendingUploads
.remove(key
); 
 384                     if (upload 
!= null
) { 
 391         public void clearListeners() { 
 392             mBoundListeners
.clear(); 
 396          * Returns True when the file described by 'file' is being uploaded to 
 397          * the ownCloud account 'account' or waiting for it 
 399          * If 'file' is a directory, returns 'true' if some of its descendant files is uploading or waiting to upload.  
 401          * @param account   ownCloud account where the remote file will be stored. 
 402          * @param file      A file that could be in the queue of pending uploads 
 404         public boolean isUploading(Account account
, OCFile file
) { 
 405             if (account 
== null 
|| file 
== null
) 
 407             String targetKey 
= buildRemoteName(account
, file
); 
 408             synchronized (mPendingUploads
) { 
 409                 if (file
.isFolder()) { 
 410                     // this can be slow if there are many uploads :( 
 411                     Iterator
<String
> it 
= mPendingUploads
.keySet().iterator(); 
 412                     boolean found 
= false
; 
 413                     while (it
.hasNext() && !found
) { 
 414                         found 
= it
.next().startsWith(targetKey
); 
 418                     return (mPendingUploads
.containsKey(targetKey
)); 
 425          * Adds a listener interested in the progress of the upload for a concrete file. 
 427          * @param listener      Object to notify about progress of transfer.     
 428          * @param account       ownCloud account holding the file of interest. 
 429          * @param file          {@link OCFile} of interest for listener. 
 431         public void addDatatransferProgressListener (OnDatatransferProgressListener listener
, Account account
, OCFile file
) { 
 432             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 433             String targetKey 
= buildRemoteName(account
, file
); 
 434             mBoundListeners
.put(targetKey
, listener
); 
 440          * Removes a listener interested in the progress of the upload for a concrete file. 
 442          * @param listener      Object to notify about progress of transfer.     
 443          * @param account       ownCloud account holding the file of interest. 
 444          * @param file          {@link OCFile} of interest for listener. 
 446         public void removeDatatransferProgressListener (OnDatatransferProgressListener listener
, Account account
, OCFile file
) { 
 447             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 448             String targetKey 
= buildRemoteName(account
, file
); 
 449             if (mBoundListeners
.get(targetKey
) == listener
) { 
 450                 mBoundListeners
.remove(targetKey
); 
 456         public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, 
 458             String key 
= buildRemoteName(mCurrentUpload
.getAccount(), mCurrentUpload
.getFile()); 
 459             OnDatatransferProgressListener boundListener 
= mBoundListeners
.get(key
); 
 460             if (boundListener 
!= null
) { 
 461                 boundListener
.onTransferProgress(progressRate
, totalTransferredSoFar
, totalToTransfer
, fileName
); 
 468      * Upload worker. Performs the pending uploads in the order they were 
 471      * Created with the Looper of a new thread, started in 
 472      * {@link FileUploader#onCreate()}. 
 474     private static class ServiceHandler 
extends Handler 
{ 
 475         // don't make it a final class, and don't remove the static ; lint will 
 476         // warn about a possible memory leak 
 477         FileUploader mService
; 
 479         public ServiceHandler(Looper looper
, FileUploader service
) { 
 482                 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); 
 487         public void handleMessage(Message msg
) { 
 488             @SuppressWarnings("unchecked") 
 489             AbstractList
<String
> requestedUploads 
= (AbstractList
<String
>) msg
.obj
; 
 490             if (msg
.obj 
!= null
) { 
 491                 Iterator
<String
> it 
= requestedUploads
.iterator(); 
 492                 while (it
.hasNext()) { 
 493                     mService
.uploadFile(it
.next()); 
 496             mService
.stopSelf(msg
.arg1
); 
 501      * Core upload method: sends the file(s) to upload 
 503      * @param uploadKey Key to access the upload to perform, contained in 
 506     public void uploadFile(String uploadKey
) { 
 508         synchronized (mPendingUploads
) { 
 509             mCurrentUpload 
= mPendingUploads
.get(uploadKey
); 
 512         if (mCurrentUpload 
!= null
) { 
 514             // Detect if the account exists 
 515             if (AccountUtils
.exists(mCurrentUpload
.getAccount(), getApplicationContext())) { 
 516                 Log_OC
.d(TAG
, "Account " + mCurrentUpload
.getAccount().toString() + " exists"); 
 518                 notifyUploadStart(mCurrentUpload
); 
 520                 RemoteOperationResult uploadResult 
= null
, grantResult 
= null
; 
 523                     /// prepare client object to send requests to the ownCloud server 
 524                     if (mUploadClient 
== null 
|| !mLastAccount
.equals(mCurrentUpload
.getAccount())) { 
 525                         mLastAccount 
= mCurrentUpload
.getAccount(); 
 527                                 new FileDataStorageManager(mLastAccount
, getContentResolver()); 
 528                         OwnCloudAccount ocAccount 
= new OwnCloudAccount(mLastAccount
, this); 
 529                         mUploadClient 
= OwnCloudClientManagerFactory
.getDefaultSingleton(). 
 530                                 getClientFor(ocAccount
, this); 
 533                     /// check the existence of the parent folder for the file to upload 
 534                     String remoteParentPath 
= new File(mCurrentUpload
.getRemotePath()).getParent(); 
 535                     remoteParentPath 
= remoteParentPath
.endsWith(OCFile
.PATH_SEPARATOR
) ? remoteParentPath 
: remoteParentPath 
+ OCFile
.PATH_SEPARATOR
; 
 536                     grantResult 
= grantFolderExistence(remoteParentPath
); 
 538                     /// perform the upload 
 539                     if (grantResult
.isSuccess()) { 
 540                         OCFile parent 
= mStorageManager
.getFileByPath(remoteParentPath
); 
 541                         mCurrentUpload
.getFile().setParentId(parent
.getFileId()); 
 542                         uploadResult 
= mCurrentUpload
.execute(mUploadClient
); 
 543                         if (uploadResult
.isSuccess()) { 
 547                         uploadResult 
= grantResult
; 
 550                 } catch (AccountsException e
) { 
 551                     Log_OC
.e(TAG
, "Error while trying to get autorization for " + mLastAccount
.name
, e
); 
 552                     uploadResult 
= new RemoteOperationResult(e
); 
 554                 } catch (IOException e
) { 
 555                     Log_OC
.e(TAG
, "Error while trying to get autorization for " + mLastAccount
.name
, e
); 
 556                     uploadResult 
= new RemoteOperationResult(e
); 
 559                     synchronized (mPendingUploads
) { 
 560                         mPendingUploads
.remove(uploadKey
); 
 561                         Log_OC
.i(TAG
, "Remove CurrentUploadItem from pending upload Item Map."); 
 563                     if (uploadResult
.isException()) { 
 564                         // enforce the creation of a new client object for next uploads; this grant that a new socket will 
 565                         // be created in the future if the current exception is due to an abrupt lose of network connection 
 566                         mUploadClient 
= null
; 
 571                 notifyUploadResult(uploadResult
, mCurrentUpload
); 
 572                 sendFinalBroadcast(mCurrentUpload
, uploadResult
); 
 575                 // Cancel the transfer 
 576                 Log_OC
.d(TAG
, "Account " + mCurrentUpload
.getAccount().toString() + " doesn't exist"); 
 577                 cancelUploadForAccount(mCurrentUpload
.getAccount().name
); 
 585      * Checks the existence of the folder where the current file will be uploaded both in the remote server  
 586      * and in the local database. 
 588      * If the upload is set to enforce the creation of the folder, the method tries to create it both remote 
 591      *  @param  pathToGrant     Full remote path whose existence will be granted. 
 592      *  @return  An {@link OCFile} instance corresponding to the folder where the file will be uploaded. 
 594     private RemoteOperationResult 
grantFolderExistence(String pathToGrant
) { 
 595         RemoteOperation operation 
= new ExistenceCheckRemoteOperation(pathToGrant
, this, false
); 
 596         RemoteOperationResult result 
= operation
.execute(mUploadClient
); 
 597         if (!result
.isSuccess() && result
.getCode() == ResultCode
.FILE_NOT_FOUND 
&& mCurrentUpload
.isRemoteFolderToBeCreated()) { 
 598             SyncOperation syncOp 
= new CreateFolderOperation( pathToGrant
, true
); 
 599             result 
= syncOp
.execute(mUploadClient
, mStorageManager
); 
 601         if (result
.isSuccess()) { 
 602             OCFile parentDir 
= mStorageManager
.getFileByPath(pathToGrant
); 
 603             if (parentDir 
== null
) { 
 604                 parentDir 
= createLocalFolder(pathToGrant
); 
 606             if (parentDir 
!= null
) { 
 607                 result 
= new RemoteOperationResult(ResultCode
.OK
); 
 609                 result 
= new RemoteOperationResult(ResultCode
.UNKNOWN_ERROR
); 
 616     private OCFile 
createLocalFolder(String remotePath
) { 
 617         String parentPath 
= new File(remotePath
).getParent(); 
 618         parentPath 
= parentPath
.endsWith(OCFile
.PATH_SEPARATOR
) ? parentPath 
: parentPath 
+ OCFile
.PATH_SEPARATOR
; 
 619         OCFile parent 
= mStorageManager
.getFileByPath(parentPath
); 
 620         if (parent 
== null
) { 
 621             parent 
= createLocalFolder(parentPath
); 
 623         if (parent 
!= null
) { 
 624             OCFile createdFolder 
= new OCFile(remotePath
); 
 625             createdFolder
.setMimetype("DIR"); 
 626             createdFolder
.setParentId(parent
.getFileId()); 
 627             mStorageManager
.saveFile(createdFolder
); 
 628             return createdFolder
; 
 635      * Saves a OC File after a successful upload. 
 637      * A PROPFIND is necessary to keep the props in the local database 
 638      * synchronized with the server, specially the modification time and Etag 
 641      * TODO refactor this ugly thing 
 643     private void saveUploadedFile() { 
 644         OCFile file 
= mCurrentUpload
.getFile(); 
 645         if (file
.fileExists()) { 
 646             file 
= mStorageManager
.getFileById(file
.getFileId()); 
 648         long syncDate 
= System
.currentTimeMillis(); 
 649         file
.setLastSyncDateForData(syncDate
); 
 651         // new PROPFIND to keep data consistent with server  
 652         // in theory, should return the same we already have 
 653         ReadRemoteFileOperation operation 
= new ReadRemoteFileOperation(mCurrentUpload
.getRemotePath()); 
 654         RemoteOperationResult result 
= operation
.execute(mUploadClient
); 
 655         if (result
.isSuccess()) { 
 656             updateOCFile(file
, (RemoteFile
) result
.getData().get(0)); 
 657             file
.setLastSyncDateForProperties(syncDate
); 
 660         // / maybe this would be better as part of UploadFileOperation... or 
 661         // maybe all this method 
 662         if (mCurrentUpload
.wasRenamed()) { 
 663             OCFile oldFile 
= mCurrentUpload
.getOldFile(); 
 664             if (oldFile
.fileExists()) { 
 665                 oldFile
.setStoragePath(null
); 
 666                 mStorageManager
.saveFile(oldFile
); 
 668             } // else: it was just an automatic renaming due to a name 
 669             // coincidence; nothing else is needed, the storagePath is right 
 670             // in the instance returned by mCurrentUpload.getFile() 
 672         file
.setNeedsUpdateThumbnail(true
); 
 673         mStorageManager
.saveFile(file
); 
 676     private void updateOCFile(OCFile file
, RemoteFile remoteFile
) { 
 677         file
.setCreationTimestamp(remoteFile
.getCreationTimestamp()); 
 678         file
.setFileLength(remoteFile
.getLength()); 
 679         file
.setMimetype(remoteFile
.getMimeType()); 
 680         file
.setModificationTimestamp(remoteFile
.getModifiedTimestamp()); 
 681         file
.setModificationTimestampAtLastSyncForData(remoteFile
.getModifiedTimestamp()); 
 682         // file.setEtag(remoteFile.getEtag());    // TODO Etag, where available 
 683         file
.setRemoteId(remoteFile
.getRemoteId()); 
 686     private OCFile 
obtainNewOCFileToUpload(String remotePath
, String localPath
, String mimeType
, 
 687                                            FileDataStorageManager storageManager
) { 
 690         if (mimeType 
== null 
|| mimeType
.length() <= 0) { 
 692                 mimeType 
= MimeTypeMap
.getSingleton().getMimeTypeFromExtension( 
 693                         remotePath
.substring(remotePath
.lastIndexOf('.') + 1)); 
 694             } catch (IndexOutOfBoundsException e
) { 
 695                 Log_OC
.e(TAG
, "Trying to find out MIME type of a file without extension: " + remotePath
); 
 698         if (mimeType 
== null
) { 
 699             mimeType 
= "application/octet-stream"; 
 702         if (isPdfFileFromContentProviderWithoutExtension(localPath
, mimeType
)){ 
 703             remotePath 
+= FILE_EXTENSION_PDF
; 
 706         OCFile newFile 
= new OCFile(remotePath
); 
 707         newFile
.setStoragePath(localPath
); 
 708         newFile
.setLastSyncDateForProperties(0); 
 709         newFile
.setLastSyncDateForData(0); 
 712         if (localPath 
!= null 
&& localPath
.length() > 0) { 
 713             File localFile 
= new File(localPath
); 
 714             newFile
.setFileLength(localFile
.length()); 
 715             newFile
.setLastSyncDateForData(localFile
.lastModified()); 
 716         } // don't worry about not assigning size, the problems with localPath 
 717         // are checked when the UploadFileOperation instance is created 
 720         newFile
.setMimetype(mimeType
); 
 726      * Creates a status notification to show the upload progress 
 728      * @param upload Upload operation starting. 
 730     private void notifyUploadStart(UploadFileOperation upload
) { 
 731         // / create status notification with a progress bar 
 733         mNotificationBuilder 
= 
 734                 NotificationBuilderWithProgressBar
.newNotificationBuilderWithProgressBar(this); 
 737                 .setSmallIcon(R
.drawable
.notification_icon
) 
 738                 .setTicker(getString(R
.string
.uploader_upload_in_progress_ticker
)) 
 739                 .setContentTitle(getString(R
.string
.uploader_upload_in_progress_ticker
)) 
 740                 .setProgress(100, 0, false
) 
 742                         String
.format(getString(R
.string
.uploader_upload_in_progress_content
), 0, upload
.getFileName())); 
 744         /// includes a pending intent in the notification showing the details view of the file 
 745         Intent showDetailsIntent 
= new Intent(this, FileDisplayActivity
.class); 
 746         showDetailsIntent
.putExtra(FileActivity
.EXTRA_FILE
, upload
.getFile()); 
 747         showDetailsIntent
.putExtra(FileActivity
.EXTRA_ACCOUNT
, upload
.getAccount()); 
 748         showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 749         mNotificationBuilder
.setContentIntent(PendingIntent
.getActivity( 
 750                 this, (int) System
.currentTimeMillis(), showDetailsIntent
, 0 
 753         mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotificationBuilder
.build()); 
 757      * Callback method to update the progress bar in the status notification 
 760     public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, String filePath
) { 
 761         int percent 
= (int) (100.0 * ((double) totalTransferredSoFar
) / ((double) totalToTransfer
)); 
 762         if (percent 
!= mLastPercent
) { 
 763             mNotificationBuilder
.setProgress(100, percent
, false
); 
 764             String fileName 
= filePath
.substring(filePath
.lastIndexOf(FileUtils
.PATH_SEPARATOR
) + 1); 
 765             String text 
= String
.format(getString(R
.string
.uploader_upload_in_progress_content
), percent
, fileName
); 
 766             mNotificationBuilder
.setContentText(text
); 
 767             mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotificationBuilder
.build()); 
 769         mLastPercent 
= percent
; 
 773      * Updates the status notification with the result of an upload operation. 
 775      * @param uploadResult Result of the upload operation. 
 776      * @param upload Finished upload operation 
 778     private void notifyUploadResult( 
 779             RemoteOperationResult uploadResult
, UploadFileOperation upload
) { 
 780         Log_OC
.d(TAG
, "NotifyUploadResult with resultCode: " + uploadResult
.getCode()); 
 781         // / cancelled operation or success -> silent removal of progress notification 
 782         mNotificationManager
.cancel(R
.string
.uploader_upload_in_progress_ticker
); 
 784         // Show the result: success or fail notification 
 785         if (!uploadResult
.isCancelled()) { 
 786             int tickerId 
= (uploadResult
.isSuccess()) ? R
.string
.uploader_upload_succeeded_ticker 
: 
 787                     R
.string
.uploader_upload_failed_ticker
; 
 789             String content 
= null
; 
 791             // check credentials error 
 792             boolean needsToUpdateCredentials 
= ( 
 793                     uploadResult
.getCode() == ResultCode
.UNAUTHORIZED 
|| 
 794                             uploadResult
.isIdPRedirection() 
 796             tickerId 
= (needsToUpdateCredentials
) ?
 
 797                     R
.string
.uploader_upload_failed_credentials_error 
: tickerId
; 
 800                     .setTicker(getString(tickerId
)) 
 801                     .setContentTitle(getString(tickerId
)) 
 804                     .setProgress(0, 0, false
); 
 806             content 
=  ErrorMessageAdapter
.getErrorCauseMessage( 
 807                     uploadResult
, upload
, getResources() 
 810             if (needsToUpdateCredentials
) { 
 811                 // let the user update credentials with one click 
 812                 Intent updateAccountCredentials 
= new Intent(this, AuthenticatorActivity
.class); 
 813                 updateAccountCredentials
.putExtra( 
 814                         AuthenticatorActivity
.EXTRA_ACCOUNT
, upload
.getAccount() 
 816                 updateAccountCredentials
.putExtra( 
 817                         AuthenticatorActivity
.EXTRA_ACTION
, 
 818                         AuthenticatorActivity
.ACTION_UPDATE_EXPIRED_TOKEN
 
 820                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
); 
 821                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
); 
 822                 updateAccountCredentials
.addFlags(Intent
.FLAG_FROM_BACKGROUND
); 
 823                 mNotificationBuilder
.setContentIntent(PendingIntent
.getActivity( 
 825                         (int) System
.currentTimeMillis(), 
 826                         updateAccountCredentials
, 
 827                         PendingIntent
.FLAG_ONE_SHOT
 
 830                 mUploadClient 
= null
; 
 831                 // grant that future retries on the same account will get the fresh credentials 
 833                 mNotificationBuilder
.setContentText(content
); 
 835                 if (upload
.isInstant()) { 
 838                         db 
= new DbHandler(this.getBaseContext()); 
 839                         String message 
= uploadResult
.getLogMessage() + " errorCode: " + 
 840                                 uploadResult
.getCode(); 
 841                         Log_OC
.e(TAG
, message 
+ " Http-Code: " + uploadResult
.getHttpCode()); 
 842                         if (uploadResult
.getCode() == ResultCode
.QUOTA_EXCEEDED
) { 
 843                             //message = getString(R.string.failed_upload_quota_exceeded_text); 
 844                             if (db
.updateFileState( 
 845                                     upload
.getOriginalStoragePath(), 
 846                                     DbHandler
.UPLOAD_STATUS_UPLOAD_FAILED
, 
 849                                         upload
.getOriginalStoragePath(), 
 850                                         upload
.getAccount().name
, 
 863             mNotificationBuilder
.setContentText(content
); 
 864             mNotificationManager
.notify(tickerId
, mNotificationBuilder
.build()); 
 866             if (uploadResult
.isSuccess()) { 
 868                 DbHandler db 
= new DbHandler(this.getBaseContext()); 
 869                 db
.removeIUPendingFile(mCurrentUpload
.getOriginalStoragePath()); 
 872                 // remove success notification, with a delay of 2 seconds 
 873                 NotificationDelayer
.cancelWithDelay( 
 874                         mNotificationManager
, 
 875                         R
.string
.uploader_upload_succeeded_ticker
, 
 883      * Sends a broadcast in order to the interested activities can update their 
 886      * @param upload Finished upload operation 
 887      * @param uploadResult Result of the upload operation 
 889     private void sendFinalBroadcast(UploadFileOperation upload
, RemoteOperationResult uploadResult
) { 
 890         Intent end 
= new Intent(getUploadFinishMessage()); 
 891         end
.putExtra(EXTRA_REMOTE_PATH
, upload
.getRemotePath()); // real remote 
 896         if (upload
.wasRenamed()) { 
 897             end
.putExtra(EXTRA_OLD_REMOTE_PATH
, upload
.getOldFile().getRemotePath()); 
 899         end
.putExtra(EXTRA_OLD_FILE_PATH
, upload
.getOriginalStoragePath()); 
 900         end
.putExtra(ACCOUNT_NAME
, upload
.getAccount().name
); 
 901         end
.putExtra(EXTRA_UPLOAD_RESULT
, uploadResult
.isSuccess()); 
 902         sendStickyBroadcast(end
); 
 906      * Checks if content provider, using the content:// scheme, returns a file with mime-type  
 907      * 'application/pdf' but file has not extension 
 910      * @return true if is needed to add the pdf file extension to the file 
 912     private boolean isPdfFileFromContentProviderWithoutExtension(String localPath
, String mimeType
) { 
 913         return localPath
.startsWith(UriUtils
.URI_CONTENT_SCHEME
) && 
 914                 mimeType
.equals(MIME_TYPE_PDF
) && 
 915                 !localPath
.endsWith(FILE_EXTENSION_PDF
); 
 919      * Remove uploads of an account 
 922     private void cancelUploadForAccount(String accountName
){ 
 923         // this can be slow if there are many uploads :( 
 924         Iterator
<String
> it 
= mPendingUploads
.keySet().iterator(); 
 925         Log_OC
.d(TAG
, "Number of pending updloads= "  + mPendingUploads
.size()); 
 926         while (it
.hasNext()) { 
 927             String key 
= it
.next(); 
 928             Log_OC
.d(TAG
, "mPendingUploads CANCELLED " + key
); 
 929             if (key
.startsWith(accountName
)) { 
 930                 synchronized (mPendingUploads
) { 
 931                     mPendingUploads
.remove(key
);