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 org
.apache
.http
.HttpStatus
; 
  32 import org
.apache
.jackrabbit
.webdav
.DavConstants
; 
  33 import org
.apache
.jackrabbit
.webdav
.MultiStatus
; 
  34 import org
.apache
.jackrabbit
.webdav
.client
.methods
.PropFindMethod
; 
  36 import com
.owncloud
.android
.R
; 
  37 import com
.owncloud
.android
.authentication
.AuthenticatorActivity
; 
  38 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  39 import com
.owncloud
.android
.datamodel
.OCFile
; 
  40 import com
.owncloud
.android
.db
.DbHandler
; 
  41 import com
.owncloud
.android
.operations
.ChunkedUploadFileOperation
; 
  42 import com
.owncloud
.android
.operations
.CreateFolderOperation
; 
  43 import com
.owncloud
.android
.oc_framework
.operations
.RemoteOperation
; 
  44 import com
.owncloud
.android
.oc_framework
.operations
.RemoteOperationResult
; 
  45 import com
.owncloud
.android
.operations
.UploadFileOperation
; 
  46 import com
.owncloud
.android
.oc_framework
.operations
.RemoteOperationResult
.ResultCode
; 
  47 import com
.owncloud
.android
.oc_framework
.operations
.remote
.ExistenceCheckRemoteOperation
; 
  48 import com
.owncloud
.android
.oc_framework
.utils
.OwnCloudVersion
; 
  49 import com
.owncloud
.android
.oc_framework
.network
.webdav
.OnDatatransferProgressListener
; 
  50 import com
.owncloud
.android
.oc_framework
.accounts
.OwnCloudAccount
; 
  51 import com
.owncloud
.android
.oc_framework
.network
.webdav
.OwnCloudClientFactory
; 
  52 import com
.owncloud
.android
.oc_framework
.network
.webdav
.WebdavClient
; 
  53 import com
.owncloud
.android
.oc_framework
.network
.webdav
.WebdavEntry
; 
  54 import com
.owncloud
.android
.oc_framework
.network
.webdav
.WebdavUtils
; 
  55 import com
.owncloud
.android
.ui
.activity
.FailedUploadActivity
; 
  56 import com
.owncloud
.android
.ui
.activity
.FileActivity
; 
  57 import com
.owncloud
.android
.ui
.activity
.FileDisplayActivity
; 
  58 import com
.owncloud
.android
.ui
.activity
.InstantUploadActivity
; 
  59 import com
.owncloud
.android
.ui
.preview
.PreviewImageActivity
; 
  60 import com
.owncloud
.android
.ui
.preview
.PreviewImageFragment
; 
  61 import com
.owncloud
.android
.utils
.Log_OC
; 
  63 import android
.accounts
.Account
; 
  64 import android
.accounts
.AccountManager
; 
  65 import android
.accounts
.AccountsException
; 
  66 import android
.app
.Notification
; 
  67 import android
.app
.NotificationManager
; 
  68 import android
.app
.PendingIntent
; 
  69 import android
.app
.Service
; 
  70 import android
.content
.Intent
; 
  71 import android
.os
.Binder
; 
  72 import android
.os
.Handler
; 
  73 import android
.os
.HandlerThread
; 
  74 import android
.os
.IBinder
; 
  75 import android
.os
.Looper
; 
  76 import android
.os
.Message
; 
  77 import android
.os
.Process
; 
  78 import android
.webkit
.MimeTypeMap
; 
  79 import android
.widget
.RemoteViews
; 
  83 public class FileUploader 
extends Service 
implements OnDatatransferProgressListener 
{ 
  85     private static final String UPLOAD_FINISH_MESSAGE 
= "UPLOAD_FINISH"; 
  86     public static final String EXTRA_UPLOAD_RESULT 
= "RESULT"; 
  87     public static final String EXTRA_REMOTE_PATH 
= "REMOTE_PATH"; 
  88     public static final String EXTRA_OLD_REMOTE_PATH 
= "OLD_REMOTE_PATH"; 
  89     public static final String EXTRA_OLD_FILE_PATH 
= "OLD_FILE_PATH"; 
  90     public static final String ACCOUNT_NAME 
= "ACCOUNT_NAME"; 
  92     public static final String KEY_FILE 
= "FILE"; 
  93     public static final String KEY_LOCAL_FILE 
= "LOCAL_FILE"; 
  94     public static final String KEY_REMOTE_FILE 
= "REMOTE_FILE"; 
  95     public static final String KEY_MIME_TYPE 
= "MIME_TYPE"; 
  97     public static final String KEY_ACCOUNT 
= "ACCOUNT"; 
  99     public static final String KEY_UPLOAD_TYPE 
= "UPLOAD_TYPE"; 
 100     public static final String KEY_FORCE_OVERWRITE 
= "KEY_FORCE_OVERWRITE"; 
 101     public static final String KEY_INSTANT_UPLOAD 
= "INSTANT_UPLOAD"; 
 102     public static final String KEY_LOCAL_BEHAVIOUR 
= "BEHAVIOUR"; 
 104     public static final int LOCAL_BEHAVIOUR_COPY 
= 0; 
 105     public static final int LOCAL_BEHAVIOUR_MOVE 
= 1; 
 106     public static final int LOCAL_BEHAVIOUR_FORGET 
= 2; 
 108     public static final int UPLOAD_SINGLE_FILE 
= 0; 
 109     public static final int UPLOAD_MULTIPLE_FILES 
= 1; 
 111     private static final String TAG 
= FileUploader
.class.getSimpleName(); 
 113     private Looper mServiceLooper
; 
 114     private ServiceHandler mServiceHandler
; 
 115     private IBinder mBinder
; 
 116     private WebdavClient mUploadClient 
= null
; 
 117     private Account mLastAccount 
= null
; 
 118     private FileDataStorageManager mStorageManager
; 
 120     private ConcurrentMap
<String
, UploadFileOperation
> mPendingUploads 
= new ConcurrentHashMap
<String
, UploadFileOperation
>(); 
 121     private UploadFileOperation mCurrentUpload 
= null
; 
 123     private NotificationManager mNotificationManager
; 
 124     private Notification mNotification
; 
 125     private int mLastPercent
; 
 126     private RemoteViews mDefaultNotificationContentView
; 
 129     public static String 
getUploadFinishMessage() { 
 130         return FileUploader
.class.getName().toString() + UPLOAD_FINISH_MESSAGE
; 
 134      * Builds a key for mPendingUploads from the account and file to upload 
 136      * @param account   Account where the file to upload is stored 
 137      * @param file      File to upload 
 139     private String 
buildRemoteName(Account account
, OCFile file
) { 
 140         return account
.name 
+ file
.getRemotePath(); 
 143     private String 
buildRemoteName(Account account
, String remotePath
) { 
 144         return account
.name 
+ remotePath
; 
 148      * Checks if an ownCloud server version should support chunked uploads. 
 150      * @param version OwnCloud version instance corresponding to an ownCloud 
 152      * @return 'True' if the ownCloud server with version supports chunked 
 155     private static boolean chunkedUploadIsSupported(OwnCloudVersion version
) { 
 156         return (version 
!= null 
&& version
.compareTo(OwnCloudVersion
.owncloud_v4_5
) >= 0); 
 160      * Service initialization 
 163     public void onCreate() { 
 165         Log_OC
.i(TAG
, "mPendingUploads size:" + mPendingUploads
.size()); 
 166         mNotificationManager 
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
); 
 167         HandlerThread thread 
= new HandlerThread("FileUploaderThread", Process
.THREAD_PRIORITY_BACKGROUND
); 
 169         mServiceLooper 
= thread
.getLooper(); 
 170         mServiceHandler 
= new ServiceHandler(mServiceLooper
, this); 
 171         mBinder 
= new FileUploaderBinder(); 
 175      * Entry point to add one or several files to the queue of uploads. 
 177      * New uploads are added calling to startService(), resulting in a call to 
 178      * this method. This ensures the service will keep on working although the 
 179      * caller activity goes away. 
 182     public int onStartCommand(Intent intent
, int flags
, int startId
) { 
 183         if (!intent
.hasExtra(KEY_ACCOUNT
) || !intent
.hasExtra(KEY_UPLOAD_TYPE
) 
 184                 || !(intent
.hasExtra(KEY_LOCAL_FILE
) || intent
.hasExtra(KEY_FILE
))) { 
 185             Log_OC
.e(TAG
, "Not enough information provided in intent"); 
 186             return Service
.START_NOT_STICKY
; 
 188         int uploadType 
= intent
.getIntExtra(KEY_UPLOAD_TYPE
, -1); 
 189         if (uploadType 
== -1) { 
 190             Log_OC
.e(TAG
, "Incorrect upload type provided"); 
 191             return Service
.START_NOT_STICKY
; 
 193         Account account 
= intent
.getParcelableExtra(KEY_ACCOUNT
); 
 195         String
[] localPaths 
= null
, remotePaths 
= null
, mimeTypes 
= null
; 
 196         OCFile
[] files 
= null
; 
 197         if (uploadType 
== UPLOAD_SINGLE_FILE
) { 
 199             if (intent
.hasExtra(KEY_FILE
)) { 
 200                 files 
= new OCFile
[] { intent
.getParcelableExtra(KEY_FILE
) }; 
 203                 localPaths 
= new String
[] { intent
.getStringExtra(KEY_LOCAL_FILE
) }; 
 204                 remotePaths 
= new String
[] { intent
.getStringExtra(KEY_REMOTE_FILE
) }; 
 205                 mimeTypes 
= new String
[] { intent
.getStringExtra(KEY_MIME_TYPE
) }; 
 208         } else { // mUploadType == UPLOAD_MULTIPLE_FILES 
 210             if (intent
.hasExtra(KEY_FILE
)) { 
 211                 files 
= (OCFile
[]) intent
.getParcelableArrayExtra(KEY_FILE
); // TODO 
 219                 localPaths 
= intent
.getStringArrayExtra(KEY_LOCAL_FILE
); 
 220                 remotePaths 
= intent
.getStringArrayExtra(KEY_REMOTE_FILE
); 
 221                 mimeTypes 
= intent
.getStringArrayExtra(KEY_MIME_TYPE
); 
 225         FileDataStorageManager storageManager 
= new FileDataStorageManager(account
, getContentResolver()); 
 227         boolean forceOverwrite 
= intent
.getBooleanExtra(KEY_FORCE_OVERWRITE
, false
); 
 228         boolean isInstant 
= intent
.getBooleanExtra(KEY_INSTANT_UPLOAD
, false
); 
 229         int localAction 
= intent
.getIntExtra(KEY_LOCAL_BEHAVIOUR
, LOCAL_BEHAVIOUR_COPY
); 
 231         if (intent
.hasExtra(KEY_FILE
) && files 
== null
) { 
 232             Log_OC
.e(TAG
, "Incorrect array for OCFiles provided in upload intent"); 
 233             return Service
.START_NOT_STICKY
; 
 235         } else if (!intent
.hasExtra(KEY_FILE
)) { 
 236             if (localPaths 
== null
) { 
 237                 Log_OC
.e(TAG
, "Incorrect array for local paths provided in upload intent"); 
 238                 return Service
.START_NOT_STICKY
; 
 240             if (remotePaths 
== null
) { 
 241                 Log_OC
.e(TAG
, "Incorrect array for remote paths provided in upload intent"); 
 242                 return Service
.START_NOT_STICKY
; 
 244             if (localPaths
.length 
!= remotePaths
.length
) { 
 245                 Log_OC
.e(TAG
, "Different number of remote paths and local paths!"); 
 246                 return Service
.START_NOT_STICKY
; 
 249             files 
= new OCFile
[localPaths
.length
]; 
 250             for (int i 
= 0; i 
< localPaths
.length
; i
++) { 
 251                 files
[i
] = obtainNewOCFileToUpload(remotePaths
[i
], localPaths
[i
], ((mimeTypes 
!= null
) ? mimeTypes
[i
] 
 252                         : (String
) null
), storageManager
); 
 253                 if (files
[i
] == null
) { 
 254                     // TODO @andomaex add failure Notiification 
 255                     return Service
.START_NOT_STICKY
; 
 260         OwnCloudVersion ocv 
= new OwnCloudVersion(AccountManager
.get(this).getUserData(account
, OwnCloudAccount
.Constants
.KEY_OC_VERSION
)); 
 261         boolean chunked 
= FileUploader
.chunkedUploadIsSupported(ocv
); 
 262         AbstractList
<String
> requestedUploads 
= new Vector
<String
>(); 
 263         String uploadKey 
= null
; 
 264         UploadFileOperation newUpload 
= null
; 
 266             for (int i 
= 0; i 
< files
.length
; i
++) { 
 267                 uploadKey 
= buildRemoteName(account
, files
[i
].getRemotePath()); 
 269                     newUpload 
= new ChunkedUploadFileOperation(account
, files
[i
], isInstant
, forceOverwrite
, 
 270                             localAction
, getApplicationContext()); 
 272                     newUpload 
= new UploadFileOperation(account
, files
[i
], isInstant
, forceOverwrite
, localAction
,  
 273                             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) 
 332      * Binder to let client components to perform operations on the queue of 
 335      * It provides by itself the available operations. 
 337     public class FileUploaderBinder 
extends Binder 
implements OnDatatransferProgressListener 
{ 
 340          * Map of listeners that will be reported about progress of uploads from a {@link FileUploaderBinder} instance  
 342         private Map
<String
, OnDatatransferProgressListener
> mBoundListeners 
= new HashMap
<String
, OnDatatransferProgressListener
>(); 
 345          * Cancels a pending or current upload of a remote file. 
 347          * @param account Owncloud account where the remote file will be stored. 
 348          * @param file A file in the queue of pending uploads 
 350         public void cancel(Account account
, OCFile file
) { 
 351             UploadFileOperation upload 
= null
; 
 352             synchronized (mPendingUploads
) { 
 353                 upload 
= mPendingUploads
.remove(buildRemoteName(account
, file
)); 
 355             if (upload 
!= null
) { 
 362         public void clearListeners() { 
 363             mBoundListeners
.clear(); 
 370          * Returns True when the file described by 'file' is being uploaded to 
 371          * the ownCloud account 'account' or waiting for it 
 373          * If 'file' is a directory, returns 'true' if some of its descendant files is uploading or waiting to upload.  
 375          * @param account Owncloud account where the remote file will be stored. 
 376          * @param file A file that could be in the queue of pending uploads 
 378         public boolean isUploading(Account account
, OCFile file
) { 
 379             if (account 
== null 
|| file 
== null
) 
 381             String targetKey 
= buildRemoteName(account
, file
); 
 382             synchronized (mPendingUploads
) { 
 383                 if (file
.isFolder()) { 
 384                     // this can be slow if there are many uploads :( 
 385                     Iterator
<String
> it 
= mPendingUploads
.keySet().iterator(); 
 386                     boolean found 
= false
; 
 387                     while (it
.hasNext() && !found
) { 
 388                         found 
= it
.next().startsWith(targetKey
); 
 392                     return (mPendingUploads
.containsKey(targetKey
)); 
 399          * Adds a listener interested in the progress of the upload for a concrete file. 
 401          * @param listener      Object to notify about progress of transfer.     
 402          * @param account       ownCloud account holding the file of interest. 
 403          * @param file          {@link OCfile} of interest for listener.  
 405         public void addDatatransferProgressListener (OnDatatransferProgressListener listener
, Account account
, OCFile file
) { 
 406             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 407             String targetKey 
= buildRemoteName(account
, file
); 
 408             mBoundListeners
.put(targetKey
, listener
); 
 414          * Removes a listener interested in the progress of the upload for a concrete file. 
 416          * @param listener      Object to notify about progress of transfer.     
 417          * @param account       ownCloud account holding the file of interest. 
 418          * @param file          {@link OCfile} of interest for listener.  
 420         public void removeDatatransferProgressListener (OnDatatransferProgressListener listener
, Account account
, OCFile file
) { 
 421             if (account 
== null 
|| file 
== null 
|| listener 
== null
) return; 
 422             String targetKey 
= buildRemoteName(account
, file
); 
 423             if (mBoundListeners
.get(targetKey
) == listener
) { 
 424                 mBoundListeners
.remove(targetKey
); 
 430         public void onTransferProgress(long progressRate
) { 
 431             // old way, should not be in use any more 
 436         public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, 
 438             String key 
= buildRemoteName(mCurrentUpload
.getAccount(), mCurrentUpload
.getFile()); 
 439             OnDatatransferProgressListener boundListener 
= mBoundListeners
.get(key
); 
 440             if (boundListener 
!= null
) { 
 441                 boundListener
.onTransferProgress(progressRate
, totalTransferredSoFar
, totalToTransfer
, fileName
); 
 448      * Upload worker. Performs the pending uploads in the order they were 
 451      * Created with the Looper of a new thread, started in 
 452      * {@link FileUploader#onCreate()}. 
 454     private static class ServiceHandler 
extends Handler 
{ 
 455         // don't make it a final class, and don't remove the static ; lint will 
 456         // warn about a possible memory leak 
 457         FileUploader mService
; 
 459         public ServiceHandler(Looper looper
, FileUploader service
) { 
 462                 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); 
 467         public void handleMessage(Message msg
) { 
 468             @SuppressWarnings("unchecked") 
 469             AbstractList
<String
> requestedUploads 
= (AbstractList
<String
>) msg
.obj
; 
 470             if (msg
.obj 
!= null
) { 
 471                 Iterator
<String
> it 
= requestedUploads
.iterator(); 
 472                 while (it
.hasNext()) { 
 473                     mService
.uploadFile(it
.next()); 
 476             mService
.stopSelf(msg
.arg1
); 
 481      * Core upload method: sends the file(s) to upload 
 483      * @param uploadKey Key to access the upload to perform, contained in 
 486     public void uploadFile(String uploadKey
) { 
 488         synchronized (mPendingUploads
) { 
 489             mCurrentUpload 
= mPendingUploads
.get(uploadKey
); 
 492         if (mCurrentUpload 
!= null
) { 
 494             notifyUploadStart(mCurrentUpload
); 
 496             RemoteOperationResult uploadResult 
= null
, grantResult 
= null
; 
 499                 /// prepare client object to send requests to the ownCloud server 
 500                 if (mUploadClient 
== null 
|| !mLastAccount
.equals(mCurrentUpload
.getAccount())) { 
 501                     mLastAccount 
= mCurrentUpload
.getAccount(); 
 502                     mStorageManager 
= new FileDataStorageManager(mLastAccount
, getContentResolver()); 
 503                     mUploadClient 
= OwnCloudClientFactory
.createOwnCloudClient(mLastAccount
, getApplicationContext()); 
 506                 /// check the existence of the parent folder for the file to upload 
 507                 String remoteParentPath 
= new File(mCurrentUpload
.getRemotePath()).getParent(); 
 508                 remoteParentPath 
= remoteParentPath
.endsWith(OCFile
.PATH_SEPARATOR
) ? remoteParentPath 
: remoteParentPath 
+ OCFile
.PATH_SEPARATOR
; 
 509                 grantResult 
= grantFolderExistence(remoteParentPath
); 
 511                 /// perform the upload 
 512                 if (grantResult
.isSuccess()) { 
 513                     OCFile parent 
= mStorageManager
.getFileByPath(remoteParentPath
); 
 514                     mCurrentUpload
.getFile().setParentId(parent
.getFileId()); 
 515                     uploadResult 
= mCurrentUpload
.execute(mUploadClient
); 
 516                     if (uploadResult
.isSuccess()) { 
 520                     uploadResult 
= grantResult
; 
 523             } catch (AccountsException e
) { 
 524                 Log_OC
.e(TAG
, "Error while trying to get autorization for " + mLastAccount
.name
, e
); 
 525                 uploadResult 
= new RemoteOperationResult(e
); 
 527             } catch (IOException e
) { 
 528                 Log_OC
.e(TAG
, "Error while trying to get autorization for " + mLastAccount
.name
, e
); 
 529                 uploadResult 
= new RemoteOperationResult(e
); 
 532                 synchronized (mPendingUploads
) { 
 533                     mPendingUploads
.remove(uploadKey
); 
 534                     Log_OC
.i(TAG
, "Remove CurrentUploadItem from pending upload Item Map."); 
 536                 if (uploadResult
.isException()) { 
 537                     // enforce the creation of a new client object for next uploads; this grant that a new socket will  
 538                     // be created in the future if the current exception is due to an abrupt lose of network connection 
 539                     mUploadClient 
= null
; 
 545             notifyUploadResult(uploadResult
, mCurrentUpload
); 
 546             sendFinalBroadcast(mCurrentUpload
, uploadResult
); 
 553      * Checks the existence of the folder where the current file will be uploaded both in the remote server  
 554      * and in the local database. 
 556      * If the upload is set to enforce the creation of the folder, the method tries to create it both remote 
 559      *  @param  pathToGrant     Full remote path whose existence will be granted. 
 560      *  @return  An {@link OCFile} instance corresponding to the folder where the file will be uploaded. 
 562     private RemoteOperationResult 
grantFolderExistence(String pathToGrant
) { 
 563         RemoteOperation operation 
= new ExistenceCheckRemoteOperation(pathToGrant
, this, false
); 
 564         RemoteOperationResult result 
= operation
.execute(mUploadClient
); 
 565         if (!result
.isSuccess() && result
.getCode() == ResultCode
.FILE_NOT_FOUND 
&& mCurrentUpload
.isRemoteFolderToBeCreated()) { 
 566             operation 
= new CreateFolderOperation( pathToGrant
, 
 569             result 
= operation
.execute(mUploadClient
); 
 571         if (result
.isSuccess()) { 
 572             OCFile parentDir 
= mStorageManager
.getFileByPath(pathToGrant
); 
 573             if (parentDir 
== null
) { 
 574                 parentDir 
= createLocalFolder(pathToGrant
); 
 576             if (parentDir 
!= null
) { 
 577                 result 
= new RemoteOperationResult(ResultCode
.OK
); 
 579                 result 
= new RemoteOperationResult(ResultCode
.UNKNOWN_ERROR
); 
 586     private OCFile 
createLocalFolder(String remotePath
) { 
 587         String parentPath 
= new File(remotePath
).getParent(); 
 588         parentPath 
= parentPath
.endsWith(OCFile
.PATH_SEPARATOR
) ? parentPath 
: parentPath 
+ OCFile
.PATH_SEPARATOR
; 
 589         OCFile parent 
= mStorageManager
.getFileByPath(parentPath
); 
 590         if (parent 
== null
) { 
 591             parent 
= createLocalFolder(parentPath
); 
 593         if (parent 
!= null
) { 
 594             OCFile createdFolder 
= new OCFile(remotePath
); 
 595             createdFolder
.setMimetype("DIR"); 
 596             createdFolder
.setParentId(parent
.getFileId()); 
 597             mStorageManager
.saveFile(createdFolder
); 
 598             return createdFolder
; 
 605      * Saves a OC File after a successful upload. 
 607      * A PROPFIND is necessary to keep the props in the local database 
 608      * synchronized with the server, specially the modification time and Etag 
 611      * TODO refactor this ugly thing 
 613     private void saveUploadedFile() { 
 614         OCFile file 
= mCurrentUpload
.getFile(); 
 615         long syncDate 
= System
.currentTimeMillis(); 
 616         file
.setLastSyncDateForData(syncDate
); 
 618         // / new PROPFIND to keep data consistent with server in theory, should 
 619         // return the same we already have 
 620         PropFindMethod propfind 
= null
; 
 621         RemoteOperationResult result 
= null
; 
 623             propfind 
= new PropFindMethod(mUploadClient
.getBaseUri() + WebdavUtils
.encodePath(mCurrentUpload
.getRemotePath()), 
 624                     DavConstants
.PROPFIND_ALL_PROP
, 
 625                     DavConstants
.DEPTH_0
); 
 626             int status 
= mUploadClient
.executeMethod(propfind
); 
 627             boolean isMultiStatus 
= (status 
== HttpStatus
.SC_MULTI_STATUS
); 
 629                 MultiStatus resp 
= propfind
.getResponseBodyAsMultiStatus(); 
 630                 WebdavEntry we 
= new WebdavEntry(resp
.getResponses()[0], mUploadClient
.getBaseUri().getPath()); 
 631                 updateOCFile(file
, we
); 
 632                 file
.setLastSyncDateForProperties(syncDate
); 
 635                 mUploadClient
.exhaustResponse(propfind
.getResponseBodyAsStream()); 
 638             result 
= new RemoteOperationResult(isMultiStatus
, status
, propfind
.getResponseHeaders()); 
 639             Log_OC
.i(TAG
, "Update: synchronizing properties for uploaded " + mCurrentUpload
.getRemotePath() + ": " 
 640                     + result
.getLogMessage()); 
 642         } catch (Exception e
) { 
 643             result 
= new RemoteOperationResult(e
); 
 644             Log_OC
.e(TAG
, "Update: synchronizing properties for uploaded " + mCurrentUpload
.getRemotePath() + ": " 
 645                     + result
.getLogMessage(), e
); 
 648             if (propfind 
!= null
) 
 649                 propfind
.releaseConnection(); 
 652         // / maybe this would be better as part of UploadFileOperation... or 
 653         // maybe all this method 
 654         if (mCurrentUpload
.wasRenamed()) { 
 655             OCFile oldFile 
= mCurrentUpload
.getOldFile(); 
 656             if (oldFile
.fileExists()) { 
 657                 oldFile
.setStoragePath(null
); 
 658                 mStorageManager
.saveFile(oldFile
); 
 660             } // else: it was just an automatic renaming due to a name 
 661               // coincidence; nothing else is needed, the storagePath is right 
 662               // in the instance returned by mCurrentUpload.getFile() 
 665         mStorageManager
.saveFile(file
); 
 668     private void updateOCFile(OCFile file
, WebdavEntry we
) { 
 669         file
.setCreationTimestamp(we
.createTimestamp()); 
 670         file
.setFileLength(we
.contentLength()); 
 671         file
.setMimetype(we
.contentType()); 
 672         file
.setModificationTimestamp(we
.modifiedTimestamp()); 
 673         file
.setModificationTimestampAtLastSyncForData(we
.modifiedTimestamp()); 
 674         // file.setEtag(mCurrentUpload.getEtag());    // TODO Etag, where available 
 677     private OCFile 
obtainNewOCFileToUpload(String remotePath
, String localPath
, String mimeType
, 
 678             FileDataStorageManager storageManager
) { 
 679         OCFile newFile 
= new OCFile(remotePath
); 
 680         newFile
.setStoragePath(localPath
); 
 681         newFile
.setLastSyncDateForProperties(0); 
 682         newFile
.setLastSyncDateForData(0); 
 685         if (localPath 
!= null 
&& localPath
.length() > 0) { 
 686             File localFile 
= new File(localPath
); 
 687             newFile
.setFileLength(localFile
.length()); 
 688             newFile
.setLastSyncDateForData(localFile
.lastModified()); 
 689         } // don't worry about not assigning size, the problems with localPath 
 690           // are checked when the UploadFileOperation instance is created 
 693         if (mimeType 
== null 
|| mimeType
.length() <= 0) { 
 695                 mimeType 
= MimeTypeMap
.getSingleton().getMimeTypeFromExtension( 
 696                         remotePath
.substring(remotePath
.lastIndexOf('.') + 1)); 
 697             } catch (IndexOutOfBoundsException e
) { 
 698                 Log_OC
.e(TAG
, "Trying to find out MIME type of a file without extension: " + remotePath
); 
 701         if (mimeType 
== null
) { 
 702             mimeType 
= "application/octet-stream"; 
 704         newFile
.setMimetype(mimeType
); 
 710      * Creates a status notification to show the upload progress 
 712      * @param upload Upload operation starting. 
 714     @SuppressWarnings("deprecation") 
 715     private void notifyUploadStart(UploadFileOperation upload
) { 
 716         // / create status notification with a progress bar 
 718         mNotification 
= new Notification(R
.drawable
.icon
, getString(R
.string
.uploader_upload_in_progress_ticker
), 
 719                 System
.currentTimeMillis()); 
 720         mNotification
.flags 
|= Notification
.FLAG_ONGOING_EVENT
; 
 721         mDefaultNotificationContentView 
= mNotification
.contentView
; 
 722         mNotification
.contentView 
= new RemoteViews(getApplicationContext().getPackageName(), 
 723                 R
.layout
.progressbar_layout
); 
 724         mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, 0, false
); 
 725         mNotification
.contentView
.setTextViewText(R
.id
.status_text
, 
 726                 String
.format(getString(R
.string
.uploader_upload_in_progress_content
), 0, upload
.getFileName())); 
 727         mNotification
.contentView
.setImageViewResource(R
.id
.status_icon
, R
.drawable
.icon
); 
 729         /// includes a pending intent in the notification showing the details view of the file 
 730         Intent showDetailsIntent 
= new Intent(this, FileDisplayActivity
.class); 
 731         showDetailsIntent
.putExtra(FileActivity
.EXTRA_FILE
, upload
.getFile()); 
 732         showDetailsIntent
.putExtra(FileActivity
.EXTRA_ACCOUNT
, upload
.getAccount()); 
 733         showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 734         mNotification
.contentIntent 
= PendingIntent
.getActivity(getApplicationContext(), 
 735                 (int) System
.currentTimeMillis(), showDetailsIntent
, 0); 
 737         mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
); 
 741      * Callback method to update the progress bar in the status notification 
 744     public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, String fileName
) { 
 745         int percent 
= (int) (100.0 * ((double) totalTransferredSoFar
) / ((double) totalToTransfer
)); 
 746         if (percent 
!= mLastPercent
) { 
 747             mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, percent
, false
); 
 748             String text 
= String
.format(getString(R
.string
.uploader_upload_in_progress_content
), percent
, fileName
); 
 749             mNotification
.contentView
.setTextViewText(R
.id
.status_text
, text
); 
 750             mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
); 
 752         mLastPercent 
= percent
; 
 756      * Callback method to update the progress bar in the status notification 
 760     public void onTransferProgress(long progressRate
) { 
 761         // NOTHING TO DO HERE ANYMORE 
 765      * Updates the status notification with the result of an upload operation. 
 767      * @param uploadResult Result of the upload operation. 
 768      * @param upload Finished upload operation 
 770     private void notifyUploadResult(RemoteOperationResult uploadResult
, UploadFileOperation upload
) { 
 771         Log_OC
.d(TAG
, "NotifyUploadResult with resultCode: " + uploadResult
.getCode()); 
 772         if (uploadResult
.isCancelled()) { 
 773             // / cancelled operation -> silent removal of progress notification 
 774             mNotificationManager
.cancel(R
.string
.uploader_upload_in_progress_ticker
); 
 776         } else if (uploadResult
.isSuccess()) { 
 777             // / success -> silent update of progress notification to success 
 779             mNotification
.flags ^
= Notification
.FLAG_ONGOING_EVENT
; // remove 
 783             mNotification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 784             mNotification
.contentView 
= mDefaultNotificationContentView
; 
 786             /// includes a pending intent in the notification showing the details view of the file 
 787             Intent showDetailsIntent 
= null
; 
 788             if (PreviewImageFragment
.canBePreviewed(upload
.getFile())) { 
 789                 showDetailsIntent 
= new Intent(this, PreviewImageActivity
.class);  
 791                 showDetailsIntent 
= new Intent(this, FileDisplayActivity
.class);  
 793             showDetailsIntent
.putExtra(FileActivity
.EXTRA_FILE
, upload
.getFile()); 
 794             showDetailsIntent
.putExtra(FileActivity
.EXTRA_ACCOUNT
, upload
.getAccount()); 
 795             showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 796             mNotification
.contentIntent 
= PendingIntent
.getActivity(getApplicationContext(), 
 797                     (int) System
.currentTimeMillis(), showDetailsIntent
, 0); 
 799             mNotification
.setLatestEventInfo(getApplicationContext(), 
 800                     getString(R
.string
.uploader_upload_succeeded_ticker
), 
 801                     String
.format(getString(R
.string
.uploader_upload_succeeded_content_single
), upload
.getFileName()), 
 802                     mNotification
.contentIntent
); 
 804             mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
); // NOT 
 806             DbHandler db 
= new DbHandler(this.getBaseContext()); 
 807             db
.removeIUPendingFile(mCurrentUpload
.getOriginalStoragePath()); 
 812             // / fail -> explicit failure notification 
 813             mNotificationManager
.cancel(R
.string
.uploader_upload_in_progress_ticker
); 
 814             Notification finalNotification 
= new Notification(R
.drawable
.icon
, 
 815                     getString(R
.string
.uploader_upload_failed_ticker
), System
.currentTimeMillis()); 
 816             finalNotification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 817             String content 
= null
; 
 819             boolean needsToUpdateCredentials 
= (uploadResult
.getCode() == ResultCode
.UNAUTHORIZED 
|| 
 820                     //(uploadResult.isTemporalRedirection() && uploadResult.isIdPRedirection() &&  
 821                     (uploadResult
.isIdPRedirection() && 
 822                             mUploadClient
.getCredentials() == null
)); 
 823                             //MainApp.getAuthTokenTypeSamlSessionCookie().equals(mUploadClient.getAuthTokenType()))); 
 824             if (needsToUpdateCredentials
) { 
 825                 // let the user update credentials with one click 
 826                 Intent updateAccountCredentials 
= new Intent(this, AuthenticatorActivity
.class); 
 827                 updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACCOUNT
, upload
.getAccount()); 
 828                 updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ENFORCED_UPDATE
, true
); 
 829                 updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACTION
, AuthenticatorActivity
.ACTION_UPDATE_TOKEN
); 
 830                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
); 
 831                 updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
); 
 832                 updateAccountCredentials
.addFlags(Intent
.FLAG_FROM_BACKGROUND
); 
 833                 finalNotification
.contentIntent 
= PendingIntent
.getActivity(this, (int)System
.currentTimeMillis(), updateAccountCredentials
, PendingIntent
.FLAG_ONE_SHOT
); 
 834                 content 
=  String
.format(getString(R
.string
.uploader_upload_failed_content_single
), upload
.getFileName()); 
 835                 finalNotification
.setLatestEventInfo(getApplicationContext(), 
 836                         getString(R
.string
.uploader_upload_failed_ticker
), content
, finalNotification
.contentIntent
); 
 837                 mUploadClient 
= null
;   // grant that future retries on the same account will get the fresh credentials 
 839                 // TODO put something smart in the contentIntent below 
 840             //    finalNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), (int)System.currentTimeMillis(), new Intent(), 0); 
 843                 if (uploadResult
.getCode() == ResultCode
.LOCAL_STORAGE_FULL
 
 844                         || uploadResult
.getCode() == ResultCode
.LOCAL_STORAGE_NOT_COPIED
) { 
 845                     // TODO we need a class to provide error messages for the users 
 846                     // from a RemoteOperationResult and a RemoteOperation 
 847                     content 
= String
.format(getString(R
.string
.error__upload__local_file_not_copied
), upload
.getFileName(), 
 848                             getString(R
.string
.app_name
)); 
 849                 } else if (uploadResult
.getCode() == ResultCode
.QUOTA_EXCEEDED
) { 
 850                     content 
= getString(R
.string
.failed_upload_quota_exceeded_text
); 
 853                             .format(getString(R
.string
.uploader_upload_failed_content_single
), upload
.getFileName()); 
 856                 // we add only for instant-uploads the InstantUploadActivity and the 
 858                 Intent detailUploadIntent 
= null
; 
 859                 if (upload
.isInstant() && InstantUploadActivity
.IS_ENABLED
) { 
 860                     detailUploadIntent 
= new Intent(this, InstantUploadActivity
.class); 
 861                     detailUploadIntent
.putExtra(FileUploader
.KEY_ACCOUNT
, upload
.getAccount()); 
 863                     detailUploadIntent 
= new Intent(this, FailedUploadActivity
.class); 
 864                     detailUploadIntent
.putExtra(FailedUploadActivity
.MESSAGE
, content
); 
 866                 finalNotification
.contentIntent 
= PendingIntent
.getActivity(getApplicationContext(), 
 867                         (int) System
.currentTimeMillis(), detailUploadIntent
, PendingIntent
.FLAG_UPDATE_CURRENT
 
 868                         | PendingIntent
.FLAG_ONE_SHOT
); 
 870                 if (upload
.isInstant()) { 
 873                         db 
= new DbHandler(this.getBaseContext()); 
 874                         String message 
= uploadResult
.getLogMessage() + " errorCode: " + uploadResult
.getCode(); 
 875                         Log_OC
.e(TAG
, message 
+ " Http-Code: " + uploadResult
.getHttpCode()); 
 876                         if (uploadResult
.getCode() == ResultCode
.QUOTA_EXCEEDED
) { 
 877                             message 
= getString(R
.string
.failed_upload_quota_exceeded_text
); 
 878                             if (db
.updateFileState(upload
.getOriginalStoragePath(), DbHandler
.UPLOAD_STATUS_UPLOAD_FAILED
, 
 880                                 db
.putFileForLater(upload
.getOriginalStoragePath(), upload
.getAccount().name
, message
); 
 890             finalNotification
.setLatestEventInfo(getApplicationContext(), 
 891                     getString(R
.string
.uploader_upload_failed_ticker
), content
, finalNotification
.contentIntent
); 
 893             mNotificationManager
.notify(R
.string
.uploader_upload_failed_ticker
, finalNotification
); 
 899      * Sends a broadcast in order to the interested activities can update their 
 902      * @param upload Finished upload operation 
 903      * @param uploadResult Result of the upload operation 
 905     private void sendFinalBroadcast(UploadFileOperation upload
, RemoteOperationResult uploadResult
) { 
 906         Intent end 
= new Intent(getUploadFinishMessage()); 
 907         end
.putExtra(EXTRA_REMOTE_PATH
, upload
.getRemotePath()); // real remote 
 912         if (upload
.wasRenamed()) { 
 913             end
.putExtra(EXTRA_OLD_REMOTE_PATH
, upload
.getOldFile().getRemotePath()); 
 915         end
.putExtra(EXTRA_OLD_FILE_PATH
, upload
.getOriginalStoragePath()); 
 916         end
.putExtra(ACCOUNT_NAME
, upload
.getAccount().name
); 
 917         end
.putExtra(EXTRA_UPLOAD_RESULT
, uploadResult
.isSuccess()); 
 918         sendStickyBroadcast(end
);