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
.AuthenticatorActivity
; 
  50 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  51 import com
.owncloud
.android
.datamodel
.OCFile
; 
  52 import com
.owncloud
.android
.db
.DbHandler
; 
  53 import com
.owncloud
.android
.lib
.common
.OwnCloudAccount
; 
  54 import com
.owncloud
.android
.lib
.common
.OwnCloudClient
; 
  55 import com
.owncloud
.android
.lib
.common
.OwnCloudClientManagerFactory
; 
  56 import com
.owncloud
.android
.lib
.common
.accounts
.AccountUtils
.Constants
; 
  57 import com
.owncloud
.android
.lib
.common
.network
.OnDatatransferProgressListener
; 
  58 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperation
; 
  59 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
; 
  60 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
.ResultCode
; 
  61 import com
.owncloud
.android
.lib
.resources
.files
.ExistenceCheckRemoteOperation
; 
  62 import com
.owncloud
.android
.lib
.resources
.files
.FileUtils
; 
  63 import com
.owncloud
.android
.lib
.resources
.files
.ReadRemoteFileOperation
; 
  64 import com
.owncloud
.android
.lib
.resources
.files
.RemoteFile
; 
  65 import com
.owncloud
.android
.lib
.resources
.status
.OwnCloudVersion
; 
  66 import com
.owncloud
.android
.notifications
.NotificationBuilderWithProgressBar
; 
  67 import com
.owncloud
.android
.notifications
.NotificationDelayer
; 
  68 import com
.owncloud
.android
.operations
.CreateFolderOperation
; 
  69 import com
.owncloud
.android
.operations
.UploadFileOperation
; 
  70 import com
.owncloud
.android
.operations
.common
.SyncOperation
; 
  71 import com
.owncloud
.android
.ui
.activity
.FileActivity
; 
  72 import com
.owncloud
.android
.ui
.activity
.FileDisplayActivity
; 
  73 import com
.owncloud
.android
.utils
.ErrorMessageAdapter
; 
  74 import com
.owncloud
.android
.utils
.Log_OC
; 
  78 public class FileUploader 
extends Service 
implements OnDatatransferProgressListener 
{ 
  80     private static final String UPLOAD_FINISH_MESSAGE 
= "UPLOAD_FINISH"; 
  81     public static final String EXTRA_UPLOAD_RESULT 
= "RESULT"; 
  82     public static final String EXTRA_REMOTE_PATH 
= "REMOTE_PATH"; 
  83     public static final String EXTRA_OLD_REMOTE_PATH 
= "OLD_REMOTE_PATH"; 
  84     public static final String EXTRA_OLD_FILE_PATH 
= "OLD_FILE_PATH"; 
  85     public static final String ACCOUNT_NAME 
= "ACCOUNT_NAME"; 
  87     public static final String KEY_FILE 
= "FILE"; 
  88     public static final String KEY_LOCAL_FILE 
= "LOCAL_FILE"; 
  89     public static final String KEY_REMOTE_FILE 
= "REMOTE_FILE"; 
  90     public static final String KEY_MIME_TYPE 
= "MIME_TYPE"; 
  92     public static final String KEY_ACCOUNT 
= "ACCOUNT"; 
  94     public static final String KEY_UPLOAD_TYPE 
= "UPLOAD_TYPE"; 
  95     public static final String KEY_FORCE_OVERWRITE 
= "KEY_FORCE_OVERWRITE"; 
  96     public static final String KEY_INSTANT_UPLOAD 
= "INSTANT_UPLOAD"; 
  97     public static final String KEY_LOCAL_BEHAVIOUR 
= "BEHAVIOUR"; 
  99     public static final int LOCAL_BEHAVIOUR_COPY 
= 0; 
 100     public static final int LOCAL_BEHAVIOUR_MOVE 
= 1; 
 101     public static final int LOCAL_BEHAVIOUR_FORGET 
= 2; 
 103     public static final int UPLOAD_SINGLE_FILE 
= 0; 
 104     public static final int UPLOAD_MULTIPLE_FILES 
= 1; 
 106     private static final String TAG 
= FileUploader
.class.getSimpleName(); 
 108     private Looper mServiceLooper
; 
 109     private ServiceHandler mServiceHandler
; 
 110     private IBinder mBinder
; 
 111     private OwnCloudClient mUploadClient 
= null
; 
 112     private Account mLastAccount 
= null
; 
 113     private FileDataStorageManager mStorageManager
; 
 115     private ConcurrentMap
<String
, UploadFileOperation
> mPendingUploads 
= new ConcurrentHashMap
<String
, UploadFileOperation
>(); 
 116     private UploadFileOperation mCurrentUpload 
= null
; 
 118     private NotificationManager mNotificationManager
; 
 119     private NotificationCompat
.Builder mNotificationBuilder
; 
 120     private int mLastPercent
; 
 123     public static String 
getUploadFinishMessage() { 
 124         return FileUploader
.class.getName().toString() + UPLOAD_FINISH_MESSAGE
; 
 128      * Builds a key for mPendingUploads from the account and file to upload 
 130      * @param account   Account where the file to upload is stored 
 131      * @param file      File to upload 
 133     private String 
buildRemoteName(Account account
, OCFile file
) { 
 134         return account
.name 
+ file
.getRemotePath(); 
 137     private String 
buildRemoteName(Account account
, String remotePath
) { 
 138         return account
.name 
+ remotePath
; 
 142      * Checks if an ownCloud server version should support chunked uploads. 
 144      * @param version OwnCloud version instance corresponding to an ownCloud 
 146      * @return 'True' if the ownCloud server with version supports chunked 
 149     private static boolean chunkedUploadIsSupported(OwnCloudVersion version
) { 
 150         return (version 
!= null 
&& version
.compareTo(OwnCloudVersion
.owncloud_v4_5
) >= 0); 
 154      * Service initialization 
 157     public void onCreate() { 
 159         Log_OC
.i(TAG
, "mPendingUploads size:" + mPendingUploads
.size()); 
 160         mNotificationManager 
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
); 
 161         HandlerThread thread 
= new HandlerThread("FileUploaderThread", Process
.THREAD_PRIORITY_BACKGROUND
); 
 163         mServiceLooper 
= thread
.getLooper(); 
 164         mServiceHandler 
= new ServiceHandler(mServiceLooper
, this); 
 165         mBinder 
= new FileUploaderBinder(); 
 169      * Entry point to add one or several files to the queue of uploads. 
 171      * New uploads are added calling to startService(), resulting in a call to 
 172      * this method. This ensures the service will keep on working although the 
 173      * caller activity goes away. 
 176     public int onStartCommand(Intent intent
, int flags
, int startId
) { 
 177         if (!intent
.hasExtra(KEY_ACCOUNT
) || !intent
.hasExtra(KEY_UPLOAD_TYPE
) 
 178                 || !(intent
.hasExtra(KEY_LOCAL_FILE
) || intent
.hasExtra(KEY_FILE
))) { 
 179             Log_OC
.e(TAG
, "Not enough information provided in intent"); 
 180             return Service
.START_NOT_STICKY
; 
 182         int uploadType 
= intent
.getIntExtra(KEY_UPLOAD_TYPE
, -1); 
 183         if (uploadType 
== -1) { 
 184             Log_OC
.e(TAG
, "Incorrect upload type provided"); 
 185             return Service
.START_NOT_STICKY
; 
 187         Account account 
= intent
.getParcelableExtra(KEY_ACCOUNT
); 
 189         String
[] localPaths 
= null
, remotePaths 
= null
, mimeTypes 
= null
; 
 190         OCFile
[] files 
= null
; 
 191         if (uploadType 
== UPLOAD_SINGLE_FILE
) { 
 193             if (intent
.hasExtra(KEY_FILE
)) { 
 194                 files 
= new OCFile
[] { intent
.getParcelableExtra(KEY_FILE
) }; 
 197                 localPaths 
= new String
[] { intent
.getStringExtra(KEY_LOCAL_FILE
) }; 
 198                 remotePaths 
= new String
[] { intent
.getStringExtra(KEY_REMOTE_FILE
) }; 
 199                 mimeTypes 
= new String
[] { intent
.getStringExtra(KEY_MIME_TYPE
) }; 
 202         } else { // mUploadType == UPLOAD_MULTIPLE_FILES 
 204             if (intent
.hasExtra(KEY_FILE
)) { 
 205                 files 
= (OCFile
[]) intent
.getParcelableArrayExtra(KEY_FILE
); // TODO 
 213                 localPaths 
= intent
.getStringArrayExtra(KEY_LOCAL_FILE
); 
 214                 remotePaths 
= intent
.getStringArrayExtra(KEY_REMOTE_FILE
); 
 215                 mimeTypes 
= intent
.getStringArrayExtra(KEY_MIME_TYPE
); 
 219         FileDataStorageManager storageManager 
= new FileDataStorageManager(account
, getContentResolver()); 
 221         boolean forceOverwrite 
= intent
.getBooleanExtra(KEY_FORCE_OVERWRITE
, false
); 
 222         boolean isInstant 
= intent
.getBooleanExtra(KEY_INSTANT_UPLOAD
, false
); 
 223         int localAction 
= intent
.getIntExtra(KEY_LOCAL_BEHAVIOUR
, LOCAL_BEHAVIOUR_COPY
); 
 225         if (intent
.hasExtra(KEY_FILE
) && files 
== null
) { 
 226             Log_OC
.e(TAG
, "Incorrect array for OCFiles provided in upload intent"); 
 227             return Service
.START_NOT_STICKY
; 
 229         } else if (!intent
.hasExtra(KEY_FILE
)) { 
 230             if (localPaths 
== null
) { 
 231                 Log_OC
.e(TAG
, "Incorrect array for local paths provided in upload intent"); 
 232                 return Service
.START_NOT_STICKY
; 
 234             if (remotePaths 
== null
) { 
 235                 Log_OC
.e(TAG
, "Incorrect array for remote paths provided in upload intent"); 
 236                 return Service
.START_NOT_STICKY
; 
 238             if (localPaths
.length 
!= remotePaths
.length
) { 
 239                 Log_OC
.e(TAG
, "Different number of remote paths and local paths!"); 
 240                 return Service
.START_NOT_STICKY
; 
 243             files 
= new OCFile
[localPaths
.length
]; 
 244             for (int i 
= 0; i 
< localPaths
.length
; i
++) { 
 245                 files
[i
] = obtainNewOCFileToUpload(remotePaths
[i
], localPaths
[i
], ((mimeTypes 
!= null
) ? mimeTypes
[i
] 
 246                         : (String
) null
), storageManager
); 
 247                 if (files
[i
] == null
) { 
 248                     // TODO @andomaex add failure Notification 
 249                     return Service
.START_NOT_STICKY
; 
 254         AccountManager aMgr 
= AccountManager
.get(this); 
 255         String version 
= aMgr
.getUserData(account
, Constants
.KEY_OC_VERSION
); 
 256         OwnCloudVersion ocv 
= new OwnCloudVersion(version
); 
 258         boolean chunked 
= FileUploader
.chunkedUploadIsSupported(ocv
); 
 259         AbstractList
<String
> requestedUploads 
= new Vector
<String
>(); 
 260         String uploadKey 
= null
; 
 261         UploadFileOperation newUpload 
= null
; 
 263             for (int i 
= 0; i 
< files
.length
; i
++) { 
 264                 uploadKey 
= buildRemoteName(account
, files
[i
].getRemotePath()); 
 265                 newUpload 
= new UploadFileOperation(account
, files
[i
], chunked
, isInstant
, forceOverwrite
, localAction
,  
 266                         getApplicationContext()); 
 268                     newUpload
.setRemoteFolderToBeCreated(); 
 270                 mPendingUploads
.putIfAbsent(uploadKey
, newUpload
); // Grants that the file only upload once time 
 272                 newUpload
.addDatatransferProgressListener(this); 
 273                 newUpload
.addDatatransferProgressListener((FileUploaderBinder
)mBinder
); 
 274                 requestedUploads
.add(uploadKey
); 
 277         } catch (IllegalArgumentException e
) { 
 278             Log_OC
.e(TAG
, "Not enough information provided in intent: " + e
.getMessage()); 
 279             return START_NOT_STICKY
; 
 281         } catch (IllegalStateException e
) { 
 282             Log_OC
.e(TAG
, "Bad information provided in intent: " + e
.getMessage()); 
 283             return START_NOT_STICKY
; 
 285         } catch (Exception e
) { 
 286             Log_OC
.e(TAG
, "Unexpected exception while processing upload intent", e
); 
 287             return START_NOT_STICKY
; 
 291         if (requestedUploads
.size() > 0) { 
 292             Message msg 
= mServiceHandler
.obtainMessage(); 
 294             msg
.obj 
= requestedUploads
; 
 295             mServiceHandler
.sendMessage(msg
); 
 297         Log_OC
.i(TAG
, "mPendingUploads size:" + mPendingUploads
.size()); 
 298         return Service
.START_NOT_STICKY
; 
 302      * Provides a binder object that clients can use to perform operations on 
 303      * the queue of uploads, excepting the addition of new files. 
 305      * Implemented to perform cancellation, pause and resume of existing 
 309     public IBinder 
onBind(Intent arg0
) { 
 314      * Called when ALL the bound clients were onbound. 
 317     public boolean onUnbind(Intent intent
) { 
 318         ((FileUploaderBinder
)mBinder
).clearListeners(); 
 319         return false
;   // not accepting rebinding (default behaviour) 
 324      * Binder to let client components to perform operations on the queue of 
 327      * It provides by itself the available operations. 
 329     public class FileUploaderBinder 
extends Binder 
implements OnDatatransferProgressListener 
{ 
 332          * Map of listeners that will be reported about progress of uploads from a {@link FileUploaderBinder} instance  
 334         private Map
<String
, OnDatatransferProgressListener
> mBoundListeners 
= new HashMap
<String
, OnDatatransferProgressListener
>(); 
 337          * Cancels a pending or current upload of a remote file. 
 339          * @param account Owncloud account where the remote file will be stored. 
 340          * @param file A file in the queue of pending uploads 
 342         public void cancel(Account account
, OCFile file
) { 
 343             UploadFileOperation upload 
= null
; 
 344             synchronized (mPendingUploads
) { 
 345                 upload 
= mPendingUploads
.remove(buildRemoteName(account
, file
)); 
 347             if (upload 
!= null
) { 
 354         public void clearListeners() { 
 355             mBoundListeners
.clear(); 
 362          * Returns True when the file described by 'file' is being uploaded to 
 363          * the ownCloud account 'account' or waiting for it 
 365          * If 'file' is a directory, returns 'true' if some of its descendant files is uploading or waiting to upload.  
 367          * @param account Owncloud account where the remote file will be stored. 
 368          * @param file A file that could be in the queue of pending uploads 
 370         public boolean isUploading(Account account
, OCFile file
) { 
 371             if (account 
== null 
|| file 
== null
) 
 373             String targetKey 
= buildRemoteName(account
, file
); 
 374             synchronized (mPendingUploads
) { 
 375                 if (file
.isFolder()) { 
 376                     // this can be slow if there are many uploads :( 
 377                     Iterator
<String
> it 
= mPendingUploads
.keySet().iterator(); 
 378                     boolean found 
= false
; 
 379                     while (it
.hasNext() && !found
) { 
 380                         found 
= it
.next().startsWith(targetKey
); 
 384                     return (mPendingUploads
.containsKey(targetKey
)); 
 391          * Adds a listener interested in the progress of the upload for a concrete file. 
 393          * @param listener      Object to notify about progress of transfer.     
 394          * @param account       ownCloud account holding the file of interest. 
 395          * @param file          {@link OCfile} of interest for listener.  
 397         public void addDatatransferProgressListener (OnDatatransferProgressListener listener
, Account account
, OCFile file
) { 
 398             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 399             String targetKey 
= buildRemoteName(account
, file
); 
 400             mBoundListeners
.put(targetKey
, listener
); 
 406          * Removes a listener interested in the progress of the upload for a concrete file. 
 408          * @param listener      Object to notify about progress of transfer.     
 409          * @param account       ownCloud account holding the file of interest. 
 410          * @param file          {@link OCfile} of interest for listener.  
 412         public void removeDatatransferProgressListener (OnDatatransferProgressListener listener
, Account account
, OCFile file
) { 
 413             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 414             String targetKey 
= buildRemoteName(account
, file
); 
 415             if (mBoundListeners
.get(targetKey
) == listener
) { 
 416                 mBoundListeners
.remove(targetKey
); 
 422         public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, 
 424             String key 
= buildRemoteName(mCurrentUpload
.getAccount(), mCurrentUpload
.getFile()); 
 425             OnDatatransferProgressListener boundListener 
= mBoundListeners
.get(key
); 
 426             if (boundListener 
!= null
) { 
 427                 boundListener
.onTransferProgress(progressRate
, totalTransferredSoFar
, totalToTransfer
, fileName
); 
 434      * Upload worker. Performs the pending uploads in the order they were 
 437      * Created with the Looper of a new thread, started in 
 438      * {@link FileUploader#onCreate()}. 
 440     private static class ServiceHandler 
extends Handler 
{ 
 441         // don't make it a final class, and don't remove the static ; lint will 
 442         // warn about a possible memory leak 
 443         FileUploader mService
; 
 445         public ServiceHandler(Looper looper
, FileUploader service
) { 
 448                 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); 
 453         public void handleMessage(Message msg
) { 
 454             @SuppressWarnings("unchecked") 
 455             AbstractList
<String
> requestedUploads 
= (AbstractList
<String
>) msg
.obj
; 
 456             if (msg
.obj 
!= null
) { 
 457                 Iterator
<String
> it 
= requestedUploads
.iterator(); 
 458                 while (it
.hasNext()) { 
 459                     mService
.uploadFile(it
.next()); 
 462             mService
.stopSelf(msg
.arg1
); 
 467      * Core upload method: sends the file(s) to upload 
 469      * @param uploadKey Key to access the upload to perform, contained in 
 472     public void uploadFile(String uploadKey
) { 
 474         synchronized (mPendingUploads
) { 
 475             mCurrentUpload 
= mPendingUploads
.get(uploadKey
); 
 478         if (mCurrentUpload 
!= null
) { 
 480             notifyUploadStart(mCurrentUpload
); 
 482             RemoteOperationResult uploadResult 
= null
, grantResult 
= null
; 
 485                 /// prepare client object to send requests to the ownCloud server 
 486                 if (mUploadClient 
== null 
|| !mLastAccount
.equals(mCurrentUpload
.getAccount())) { 
 487                     mLastAccount 
= mCurrentUpload
.getAccount(); 
 489                             new FileDataStorageManager(mLastAccount
, getContentResolver()); 
 490                     OwnCloudAccount ocAccount 
= new OwnCloudAccount(mLastAccount
, this); 
 491                     mUploadClient 
= OwnCloudClientManagerFactory
.getDefaultSingleton(). 
 492                             getClientFor(ocAccount
, this); 
 495                 /// check the existence of the parent folder for the file to upload 
 496                 String remoteParentPath 
= new File(mCurrentUpload
.getRemotePath()).getParent(); 
 497                 remoteParentPath 
= remoteParentPath
.endsWith(OCFile
.PATH_SEPARATOR
) ? remoteParentPath 
: remoteParentPath 
+ OCFile
.PATH_SEPARATOR
; 
 498                 grantResult 
= grantFolderExistence(remoteParentPath
); 
 500                 /// perform the upload 
 501                 if (grantResult
.isSuccess()) { 
 502                     OCFile parent 
= mStorageManager
.getFileByPath(remoteParentPath
); 
 503                     mCurrentUpload
.getFile().setParentId(parent
.getFileId()); 
 504                     uploadResult 
= mCurrentUpload
.execute(mUploadClient
); 
 505                     if (uploadResult
.isSuccess()) { 
 509                     uploadResult 
= grantResult
; 
 512             } catch (AccountsException e
) { 
 513                 Log_OC
.e(TAG
, "Error while trying to get autorization for " + mLastAccount
.name
, e
); 
 514                 uploadResult 
= new RemoteOperationResult(e
); 
 516             } catch (IOException e
) { 
 517                 Log_OC
.e(TAG
, "Error while trying to get autorization for " + mLastAccount
.name
, e
); 
 518                 uploadResult 
= new RemoteOperationResult(e
); 
 521                 synchronized (mPendingUploads
) { 
 522                     mPendingUploads
.remove(uploadKey
); 
 523                     Log_OC
.i(TAG
, "Remove CurrentUploadItem from pending upload Item Map."); 
 525                 if (uploadResult
.isException()) { 
 526                     // enforce the creation of a new client object for next uploads; this grant that a new socket will  
 527                     // be created in the future if the current exception is due to an abrupt lose of network connection 
 528                     mUploadClient 
= null
; 
 534             notifyUploadResult(uploadResult
, mCurrentUpload
); 
 535             sendFinalBroadcast(mCurrentUpload
, uploadResult
); 
 542      * Checks the existence of the folder where the current file will be uploaded both in the remote server  
 543      * and in the local database. 
 545      * If the upload is set to enforce the creation of the folder, the method tries to create it both remote 
 548      *  @param  pathToGrant     Full remote path whose existence will be granted. 
 549      *  @return  An {@link OCFile} instance corresponding to the folder where the file will be uploaded. 
 551     private RemoteOperationResult 
grantFolderExistence(String pathToGrant
) { 
 552         RemoteOperation operation 
= new ExistenceCheckRemoteOperation(pathToGrant
, this, false
); 
 553         RemoteOperationResult result 
= operation
.execute(mUploadClient
); 
 554         if (!result
.isSuccess() && result
.getCode() == ResultCode
.FILE_NOT_FOUND 
&& mCurrentUpload
.isRemoteFolderToBeCreated()) { 
 555             SyncOperation syncOp 
= new CreateFolderOperation( pathToGrant
, true
); 
 556             result 
= syncOp
.execute(mUploadClient
, mStorageManager
); 
 558         if (result
.isSuccess()) { 
 559             OCFile parentDir 
= mStorageManager
.getFileByPath(pathToGrant
); 
 560             if (parentDir 
== null
) { 
 561                 parentDir 
= createLocalFolder(pathToGrant
); 
 563             if (parentDir 
!= null
) { 
 564                 result 
= new RemoteOperationResult(ResultCode
.OK
); 
 566                 result 
= new RemoteOperationResult(ResultCode
.UNKNOWN_ERROR
); 
 573     private OCFile 
createLocalFolder(String remotePath
) { 
 574         String parentPath 
= new File(remotePath
).getParent(); 
 575         parentPath 
= parentPath
.endsWith(OCFile
.PATH_SEPARATOR
) ? parentPath 
: parentPath 
+ OCFile
.PATH_SEPARATOR
; 
 576         OCFile parent 
= mStorageManager
.getFileByPath(parentPath
); 
 577         if (parent 
== null
) { 
 578             parent 
= createLocalFolder(parentPath
); 
 580         if (parent 
!= null
) { 
 581             OCFile createdFolder 
= new OCFile(remotePath
); 
 582             createdFolder
.setMimetype("DIR"); 
 583             createdFolder
.setParentId(parent
.getFileId()); 
 584             mStorageManager
.saveFile(createdFolder
); 
 585             return createdFolder
; 
 592      * Saves a OC File after a successful upload. 
 594      * A PROPFIND is necessary to keep the props in the local database 
 595      * synchronized with the server, specially the modification time and Etag 
 598      * TODO refactor this ugly thing 
 600     private void saveUploadedFile() { 
 601         OCFile file 
= mCurrentUpload
.getFile(); 
 602         if (file
.fileExists()) { 
 603             file 
= mStorageManager
.getFileById(file
.getFileId()); 
 605         long syncDate 
= System
.currentTimeMillis(); 
 606         file
.setLastSyncDateForData(syncDate
); 
 608         // new PROPFIND to keep data consistent with server  
 609         // in theory, should return the same we already have 
 610         ReadRemoteFileOperation operation 
= new ReadRemoteFileOperation(mCurrentUpload
.getRemotePath()); 
 611         RemoteOperationResult result 
= operation
.execute(mUploadClient
); 
 612         if (result
.isSuccess()) { 
 613             updateOCFile(file
, (RemoteFile
) result
.getData().get(0)); 
 614             file
.setLastSyncDateForProperties(syncDate
); 
 617         // / maybe this would be better as part of UploadFileOperation... or 
 618         // maybe all this method 
 619         if (mCurrentUpload
.wasRenamed()) { 
 620             OCFile oldFile 
= mCurrentUpload
.getOldFile(); 
 621             if (oldFile
.fileExists()) { 
 622                 oldFile
.setStoragePath(null
); 
 623                 mStorageManager
.saveFile(oldFile
); 
 625             } // else: it was just an automatic renaming due to a name 
 626               // coincidence; nothing else is needed, the storagePath is right 
 627               // in the instance returned by mCurrentUpload.getFile() 
 630         mStorageManager
.saveFile(file
); 
 633     private void updateOCFile(OCFile file
, RemoteFile remoteFile
) { 
 634         file
.setCreationTimestamp(remoteFile
.getCreationTimestamp()); 
 635         file
.setFileLength(remoteFile
.getLength()); 
 636         file
.setMimetype(remoteFile
.getMimeType()); 
 637         file
.setModificationTimestamp(remoteFile
.getModifiedTimestamp()); 
 638         file
.setModificationTimestampAtLastSyncForData(remoteFile
.getModifiedTimestamp()); 
 639         // file.setEtag(remoteFile.getEtag());    // TODO Etag, where available 
 642     private OCFile 
obtainNewOCFileToUpload(String remotePath
, String localPath
, String mimeType
, 
 643             FileDataStorageManager storageManager
) { 
 644         OCFile newFile 
= new OCFile(remotePath
); 
 645         newFile
.setStoragePath(localPath
); 
 646         newFile
.setLastSyncDateForProperties(0); 
 647         newFile
.setLastSyncDateForData(0); 
 650         if (localPath 
!= null 
&& localPath
.length() > 0) { 
 651             File localFile 
= new File(localPath
); 
 652             newFile
.setFileLength(localFile
.length()); 
 653             newFile
.setLastSyncDateForData(localFile
.lastModified()); 
 654         } // don't worry about not assigning size, the problems with localPath 
 655           // are checked when the UploadFileOperation instance is created 
 658         if (mimeType 
== null 
|| mimeType
.length() <= 0) { 
 660                 mimeType 
= MimeTypeMap
.getSingleton().getMimeTypeFromExtension( 
 661                         remotePath
.substring(remotePath
.lastIndexOf('.') + 1)); 
 662             } catch (IndexOutOfBoundsException e
) { 
 663                 Log_OC
.e(TAG
, "Trying to find out MIME type of a file without extension: " + remotePath
); 
 666         if (mimeType 
== null
) { 
 667             mimeType 
= "application/octet-stream"; 
 669         newFile
.setMimetype(mimeType
); 
 675      * Creates a status notification to show the upload progress 
 677      * @param upload Upload operation starting. 
 679     private void notifyUploadStart(UploadFileOperation upload
) { 
 680         // / create status notification with a progress bar 
 682         mNotificationBuilder 
=  
 683                 NotificationBuilderWithProgressBar
.newNotificationBuilderWithProgressBar(this); 
 686                 .setSmallIcon(R
.drawable
.notification_icon
) 
 687                 .setTicker(getString(R
.string
.uploader_upload_in_progress_ticker
)) 
 688                 .setContentTitle(getString(R
.string
.uploader_upload_in_progress_ticker
)) 
 689                 .setProgress(100, 0, false
) 
 691                         String
.format(getString(R
.string
.uploader_upload_in_progress_content
), 0, upload
.getFileName())); 
 693         /// includes a pending intent in the notification showing the details view of the file 
 694         Intent showDetailsIntent 
= new Intent(this, FileDisplayActivity
.class); 
 695         showDetailsIntent
.putExtra(FileActivity
.EXTRA_FILE
, upload
.getFile()); 
 696         showDetailsIntent
.putExtra(FileActivity
.EXTRA_ACCOUNT
, upload
.getAccount()); 
 697         showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 698         mNotificationBuilder
.setContentIntent(PendingIntent
.getActivity( 
 699             this, (int) System
.currentTimeMillis(), showDetailsIntent
, 0 
 702         mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotificationBuilder
.build()); 
 706      * Callback method to update the progress bar in the status notification 
 709     public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, String filePath
) { 
 710         int percent 
= (int) (100.0 * ((double) totalTransferredSoFar
) / ((double) totalToTransfer
)); 
 711         if (percent 
!= mLastPercent
) { 
 712             mNotificationBuilder
.setProgress(100, percent
, false
); 
 713             String fileName 
= filePath
.substring(filePath
.lastIndexOf(FileUtils
.PATH_SEPARATOR
) + 1); 
 714             String text 
= String
.format(getString(R
.string
.uploader_upload_in_progress_content
), percent
, fileName
); 
 715             mNotificationBuilder
.setContentText(text
); 
 716             mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotificationBuilder
.build()); 
 718         mLastPercent 
= percent
; 
 722      * Updates the status notification with the result of an upload operation. 
 724      * @param uploadResult Result of the upload operation. 
 725      * @param upload Finished upload operation 
 727     private void notifyUploadResult( 
 728             RemoteOperationResult uploadResult
, UploadFileOperation upload
) { 
 729         Log_OC
.d(TAG
, "NotifyUploadResult with resultCode: " + uploadResult
.getCode()); 
 730         // / cancelled operation or success -> silent removal of progress notification 
 731         mNotificationManager
.cancel(R
.string
.uploader_upload_in_progress_ticker
); 
 733         // Show the result: success or fail notification 
 734         if (!uploadResult
.isCancelled()) { 
 735             int tickerId 
= (uploadResult
.isSuccess()) ? R
.string
.uploader_upload_succeeded_ticker 
:  
 736                 R
.string
.uploader_upload_failed_ticker
; 
 738             String content 
= null
; 
 740             // check credentials error 
 741             boolean needsToUpdateCredentials 
= ( 
 742                     uploadResult
.getCode() == ResultCode
.UNAUTHORIZED 
||  
 743                     uploadResult
.isIdPRedirection() 
 745             tickerId 
= (needsToUpdateCredentials
) ? 
 
 746                     R
.string
.uploader_upload_failed_credentials_error 
: tickerId
; 
 749             .setTicker(getString(tickerId
)) 
 750             .setContentTitle(getString(tickerId
)) 
 753             .setProgress(0, 0, false
); 
 755             content 
=  ErrorMessageAdapter
.getErrorCauseMessage( 
 756                     uploadResult
, upload
, getResources() 
 759             if (needsToUpdateCredentials
) { 
 760                 // let the user update credentials with one click 
 761                 Intent updateAccountCredentials 
= new Intent(this, AuthenticatorActivity
.class); 
 762                 updateAccountCredentials
.putExtra( 
 763                         AuthenticatorActivity
.EXTRA_ACCOUNT
, upload
.getAccount() 
 765                 updateAccountCredentials
.putExtra( 
 766                         AuthenticatorActivity
.EXTRA_ACTION
,  
 767                         AuthenticatorActivity
.ACTION_UPDATE_EXPIRED_TOKEN
 
 769                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
); 
 770                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
); 
 771                 updateAccountCredentials
.addFlags(Intent
.FLAG_FROM_BACKGROUND
); 
 772                 mNotificationBuilder
.setContentIntent(PendingIntent
.getActivity( 
 774                     (int) System
.currentTimeMillis(),  
 775                     updateAccountCredentials
,  
 776                     PendingIntent
.FLAG_ONE_SHOT
 
 779                 mUploadClient 
= null
;    
 780                     // grant that future retries on the same account will get the fresh credentials 
 782                 mNotificationBuilder
.setContentText(content
); 
 784                 if (upload
.isInstant()) { 
 787                         db 
= new DbHandler(this.getBaseContext()); 
 788                         String message 
= uploadResult
.getLogMessage() + " errorCode: " + 
 789                                 uploadResult
.getCode(); 
 790                         Log_OC
.e(TAG
, message 
+ " Http-Code: " + uploadResult
.getHttpCode()); 
 791                         if (uploadResult
.getCode() == ResultCode
.QUOTA_EXCEEDED
) { 
 792                             //message = getString(R.string.failed_upload_quota_exceeded_text); 
 793                             if (db
.updateFileState( 
 794                                     upload
.getOriginalStoragePath(),  
 795                                     DbHandler
.UPLOAD_STATUS_UPLOAD_FAILED
, 
 798                                         upload
.getOriginalStoragePath(),  
 799                                         upload
.getAccount().name
,  
 812             mNotificationBuilder
.setContentText(content
); 
 813             mNotificationManager
.notify(tickerId
, mNotificationBuilder
.build()); 
 815             if (uploadResult
.isSuccess()) { 
 817                 DbHandler db 
= new DbHandler(this.getBaseContext()); 
 818                 db
.removeIUPendingFile(mCurrentUpload
.getOriginalStoragePath()); 
 821                 // remove success notification, with a delay of 2 seconds 
 822                 NotificationDelayer
.cancelWithDelay( 
 823                         mNotificationManager
,  
 824                         R
.string
.uploader_upload_succeeded_ticker
,  
 832      * Sends a broadcast in order to the interested activities can update their 
 835      * @param upload Finished upload operation 
 836      * @param uploadResult Result of the upload operation 
 838     private void sendFinalBroadcast(UploadFileOperation upload
, RemoteOperationResult uploadResult
) { 
 839         Intent end 
= new Intent(getUploadFinishMessage()); 
 840         end
.putExtra(EXTRA_REMOTE_PATH
, upload
.getRemotePath()); // real remote 
 845         if (upload
.wasRenamed()) { 
 846             end
.putExtra(EXTRA_OLD_REMOTE_PATH
, upload
.getOldFile().getRemotePath()); 
 848         end
.putExtra(EXTRA_OLD_FILE_PATH
, upload
.getOriginalStoragePath()); 
 849         end
.putExtra(ACCOUNT_NAME
, upload
.getAccount().name
); 
 850         end
.putExtra(EXTRA_UPLOAD_RESULT
, uploadResult
.isSuccess()); 
 851         sendStickyBroadcast(end
);