1 /* ownCloud Android client application 
   2  *   Copyright (C) 2012-2014 ownCloud Inc. 
   4  *   This program is free software: you can redistribute it and/or modify 
   5  *   it under the terms of the GNU General Public License version 2, 
   6  *   as published by the Free Software Foundation. 
   8  *   This program is distributed in the hope that it will be useful, 
   9  *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
  10  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  11  *   GNU General Public License for more details. 
  13  *   You should have received a copy of the GNU General Public License 
  14  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. 
  18 package com
.owncloud
.android
.operations
; 
  20 import android
.accounts
.Account
; 
  21 import android
.content
.Context
; 
  22 import android
.content
.Intent
; 
  23 import android
.util
.Log
; 
  25 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  26 import com
.owncloud
.android
.datamodel
.OCFile
; 
  27 import com
.owncloud
.android
.files
.services
.FileDownloader
; 
  28 import com
.owncloud
.android
.lib
.common
.OwnCloudClient
; 
  29 import com
.owncloud
.android
.lib
.common
.operations
.OperationCancelledException
; 
  30 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
; 
  31 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
.ResultCode
; 
  32 import com
.owncloud
.android
.lib
.common
.utils
.Log_OC
; 
  33 import com
.owncloud
.android
.lib
.resources
.files
.ReadRemoteFileOperation
; 
  34 import com
.owncloud
.android
.lib
.resources
.files
.ReadRemoteFolderOperation
; 
  35 import com
.owncloud
.android
.lib
.resources
.files
.RemoteFile
; 
  36 import com
.owncloud
.android
.operations
.common
.SyncOperation
; 
  37 import com
.owncloud
.android
.utils
.FileStorageUtils
; 
  40 import java
.util
.ArrayList
; 
  41 import java
.util
.HashMap
; 
  42 import java
.util
.List
; 
  44 import java
.util
.Vector
; 
  45 import java
.util
.concurrent
.atomic
.AtomicBoolean
; 
  47 //import android.support.v4.content.LocalBroadcastManager; 
  51  *  Remote operation performing the synchronization of the list of files contained  
  52  *  in a folder identified with its remote path. 
  54  *  Fetches the list and properties of the files contained in the given folder, including their  
  55  *  properties, and updates the local database with them. 
  57  *  Does NOT enter in the child folders to synchronize their contents also. 
  59  *  @author David A. Velasco 
  61 public class SynchronizeFolderOperation 
extends SyncOperation 
{ 
  63     private static final String TAG 
= SynchronizeFolderOperation
.class.getSimpleName(); 
  65     /** Time stamp for the synchronization process in progress */ 
  66     private long mCurrentSyncTime
; 
  68     /** Remote path of the folder to synchronize */ 
  69     private String mRemotePath
; 
  71     /** Account where the file to synchronize belongs */ 
  72     private Account mAccount
; 
  74     /** Android context; necessary to send requests to the download service */ 
  75     private Context mContext
; 
  77     /** Locally cached information about folder to synchronize */ 
  78     private OCFile mLocalFolder
; 
  80     /** Files and folders contained in the synchronized folder after a successful operation */ 
  81     //private List<OCFile> mChildren; 
  83     /** Counter of conflicts found between local and remote files */ 
  84     private int mConflictsFound
; 
  86     /** Counter of failed operations in synchronization of kept-in-sync files */ 
  87     private int mFailsInFileSyncsFound
; 
  89     /** 'True' means that the remote folder changed and should be fetched */ 
  90     private boolean mRemoteFolderChanged
; 
  92     private List
<OCFile
> mFilesForDirectDownload
; 
  93         // to avoid extra PROPFINDs when there was no change in the folder 
  95     private List
<SyncOperation
> mFilesToSyncContentsWithoutUpload
; 
  96         // this will go out when 'folder synchronization' replaces 'folder download'; step by step   
  98     private List
<SyncOperation
> mFavouriteFilesToSyncContents
; 
  99         // this will be used for every file when 'folder synchronization' replaces 'folder download'  
 101     private List
<SyncOperation
> mFoldersToWalkDown
; 
 103     private final AtomicBoolean mCancellationRequested
; 
 106      * Creates a new instance of {@link SynchronizeFolderOperation}. 
 108      * @param   context                 Application context. 
 109      * @param   remotePath              Path to synchronize. 
 110      * @param   account                 ownCloud account where the folder is located. 
 111      * @param   currentSyncTime         Time stamp for the synchronization process in progress. 
 113     public SynchronizeFolderOperation(Context context
, String remotePath
, Account account
, long currentSyncTime
){ 
 114         mRemotePath 
= remotePath
; 
 115         mCurrentSyncTime 
= currentSyncTime
; 
 118         mRemoteFolderChanged 
= false
; 
 119         mFilesForDirectDownload 
= new Vector
<OCFile
>(); 
 120         mFilesToSyncContentsWithoutUpload 
= new Vector
<SyncOperation
>(); 
 121         mFavouriteFilesToSyncContents 
= new Vector
<SyncOperation
>(); 
 122         mFoldersToWalkDown 
= new Vector
<SyncOperation
>(); 
 123         mCancellationRequested 
= new AtomicBoolean(false
); 
 127     public int getConflictsFound() { 
 128         return mConflictsFound
; 
 131     public int getFailsInFileSyncsFound() { 
 132         return mFailsInFileSyncsFound
; 
 136      * Performs the synchronization. 
 141     protected RemoteOperationResult 
run(OwnCloudClient client
) { 
 142         RemoteOperationResult result 
= null
; 
 143         mFailsInFileSyncsFound 
= 0; 
 147             // get locally cached information about folder  
 148             mLocalFolder 
= getStorageManager().getFileByPath(mRemotePath
);    
 150             result 
= checkForChanges(client
); 
 152             if (result
.isSuccess()) { 
 153                 if (mRemoteFolderChanged
) { 
 154                     result 
= fetchAndSyncRemoteFolder(client
); 
 157                     prepareOpsFromLocalKnowledge(); 
 160                 if (result
.isSuccess()) { 
 161                     syncContents(client
); 
 164                 if (mFilesForDirectDownload
.isEmpty()) { 
 165                     sendBroadcastForNotifyingUIUpdate(result
.isSuccess()); 
 168         } catch (OperationCancelledException e
) { 
 169             result 
= new RemoteOperationResult(e
); 
 171             // cancel 'child' synchronizations 
 172             for (SyncOperation  synchOp
: mFoldersToWalkDown
) { 
 173                 ((SynchronizeFolderOperation
) synchOp
).cancel(); 
 181     private RemoteOperationResult 
checkForChanges(OwnCloudClient client
) throws OperationCancelledException 
{ 
 182         Log_OC
.d(TAG
, "Checking changes in " + mAccount
.name 
+ mRemotePath
); 
 184         mRemoteFolderChanged 
= true
; 
 185         RemoteOperationResult result 
= null
; 
 187         if (mCancellationRequested
.get()) { 
 188             throw new OperationCancelledException(); 
 192         ReadRemoteFileOperation operation 
= new ReadRemoteFileOperation(mRemotePath
); 
 193         result 
= operation
.execute(client
); 
 194         if (result
.isSuccess()){ 
 195             OCFile remoteFolder 
= FileStorageUtils
.fillOCFile((RemoteFile
) result
.getData().get(0)); 
 197             // check if remote and local folder are different 
 198             mRemoteFolderChanged 
= 
 199                         !(remoteFolder
.getEtag().equalsIgnoreCase(mLocalFolder
.getEtag())); 
 201             result 
= new RemoteOperationResult(ResultCode
.OK
); 
 203             Log_OC
.i(TAG
, "Checked " + mAccount
.name 
+ mRemotePath 
+ " : " + 
 204                     (mRemoteFolderChanged ? 
"changed" : "not changed")); 
 208             if (result
.getCode() == ResultCode
.FILE_NOT_FOUND
) { 
 211             if (result
.isException()) { 
 212                 Log_OC
.e(TAG
, "Checked " + mAccount
.name 
+ mRemotePath  
+ " : " + 
 213                         result
.getLogMessage(), result
.getException()); 
 215                 Log_OC
.e(TAG
, "Checked " + mAccount
.name 
+ mRemotePath 
+ " : " + 
 216                         result
.getLogMessage()); 
 219             sendBroadcastForNotifyingUIUpdate(result
.isSuccess()); 
 226     private RemoteOperationResult 
fetchAndSyncRemoteFolder(OwnCloudClient client
) throws OperationCancelledException 
{ 
 227         if (mCancellationRequested
.get()) { 
 228             throw new OperationCancelledException(); 
 231         ReadRemoteFolderOperation operation 
= new ReadRemoteFolderOperation(mRemotePath
); 
 232         RemoteOperationResult result 
= operation
.execute(client
); 
 233         Log_OC
.d(TAG
, "Synchronizing " + mAccount
.name 
+ mRemotePath
); 
 235         if (result
.isSuccess()) { 
 236             synchronizeData(result
.getData(), client
); 
 237             if (mConflictsFound 
> 0  || mFailsInFileSyncsFound 
> 0) { 
 238                 result 
= new RemoteOperationResult(ResultCode
.SYNC_CONFLICT
); 
 239                     // should be a different result code, but will do the job 
 242             if (result
.getCode() == ResultCode
.FILE_NOT_FOUND
) 
 251     private void removeLocalFolder() { 
 252         FileDataStorageManager storageManager 
= getStorageManager(); 
 253         if (storageManager
.fileExists(mLocalFolder
.getFileId())) { 
 254             String currentSavePath 
= FileStorageUtils
.getSavePath(mAccount
.name
); 
 255             storageManager
.removeFolder( 
 258                     (   mLocalFolder
.isDown() &&        // TODO: debug, I think this is always false for folders 
 259                             mLocalFolder
.getStoragePath().startsWith(currentSavePath
) 
 267      *  Synchronizes the data retrieved from the server about the contents of the target folder 
 268      *  with the current data in the local database. 
 270      *  Grants that mChildren is updated with fresh data after execution. 
 272      *  @param folderAndFiles   Remote folder and children files in Folder 
 274      *  @param client           Client instance to the remote server where the data were 
 276      *  @return                 'True' when any change was made in the local data, 'false' otherwise 
 278     private void synchronizeData(ArrayList
<Object
> folderAndFiles
, OwnCloudClient client
) { 
 279         FileDataStorageManager storageManager 
= getStorageManager(); 
 281         // parse data from remote folder 
 282         OCFile remoteFolder 
= fillOCFile((RemoteFile
)folderAndFiles
.get(0)); 
 283         remoteFolder
.setParentId(mLocalFolder
.getParentId()); 
 284         remoteFolder
.setFileId(mLocalFolder
.getFileId()); 
 286         Log_OC
.d(TAG
, "Remote folder " + mLocalFolder
.getRemotePath() 
 287                 + " changed - starting update of local data "); 
 289         List
<OCFile
> updatedFiles 
= new Vector
<OCFile
>(folderAndFiles
.size() - 1); 
 290         mFilesForDirectDownload
.clear(); 
 291         mFilesToSyncContentsWithoutUpload
.clear(); 
 292         mFavouriteFilesToSyncContents
.clear(); 
 293         mFoldersToWalkDown
.clear(); 
 295         // get current data about local contents of the folder to synchronize 
 296         List
<OCFile
> localFiles 
= storageManager
.getFolderContent(mLocalFolder
); 
 297         Map
<String
, OCFile
> localFilesMap 
= new HashMap
<String
, OCFile
>(localFiles
.size()); 
 298         for (OCFile file 
: localFiles
) { 
 299             localFilesMap
.put(file
.getRemotePath(), file
); 
 302         // loop to synchronize every child 
 303         OCFile remoteFile 
= null
, localFile 
= null
; 
 304         for (int i
=1; i
<folderAndFiles
.size(); i
++) { 
 305             /// new OCFile instance with the data from the server 
 306             remoteFile 
= fillOCFile((RemoteFile
)folderAndFiles
.get(i
)); 
 307             remoteFile
.setParentId(mLocalFolder
.getFileId()); 
 309             /// retrieve local data for the read file 
 310             //  localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath()); 
 311             localFile 
= localFilesMap
.remove(remoteFile
.getRemotePath()); 
 313             /// add to the remoteFile (the new one) data about LOCAL STATE (not existing in server) 
 314             remoteFile
.setLastSyncDateForProperties(mCurrentSyncTime
); 
 315             if (localFile 
!= null
) { 
 316                 // some properties of local state are kept unmodified 
 317                 remoteFile
.setFileId(localFile
.getFileId()); 
 318                 remoteFile
.setKeepInSync(localFile
.keepInSync()); 
 319                 remoteFile
.setLastSyncDateForData(localFile
.getLastSyncDateForData()); 
 320                 remoteFile
.setModificationTimestampAtLastSyncForData( 
 321                         localFile
.getModificationTimestampAtLastSyncForData() 
 323                 remoteFile
.setStoragePath(localFile
.getStoragePath()); 
 324                 // eTag will not be updated unless contents are synchronized 
 325                 //  (Synchronize[File|Folder]Operation with remoteFile as parameter) 
 326                 remoteFile
.setEtag(localFile
.getEtag()); 
 327                 if (remoteFile
.isFolder()) { 
 328                     remoteFile
.setFileLength(localFile
.getFileLength()); 
 329                         // TODO move operations about size of folders to FileContentProvider 
 330                 } else if (mRemoteFolderChanged 
&& remoteFile
.isImage() && 
 331                         remoteFile
.getModificationTimestamp() != localFile
.getModificationTimestamp()) { 
 332                     remoteFile
.setNeedsUpdateThumbnail(true
); 
 333                     Log
.d(TAG
, "Image " + remoteFile
.getFileName() + " updated on the server"); 
 335                 remoteFile
.setPublicLink(localFile
.getPublicLink()); 
 336                 remoteFile
.setShareByLink(localFile
.isShareByLink()); 
 338                 // remote eTag will not be updated unless contents are synchronized 
 339                 //  (Synchronize[File|Folder]Operation with remoteFile as parameter) 
 340                 remoteFile
.setEtag(""); 
 343             /// check and fix, if needed, local storage path 
 344             searchForLocalFileInDefaultPath(remoteFile
); 
 346             /// classify file to sync/download contents later 
 347             if (remoteFile
.isFolder()) { 
 348                 /// to download children files recursively 
 349                 SynchronizeFolderOperation synchFolderOp 
=  new SynchronizeFolderOperation(  
 351                         remoteFile
.getRemotePath(), 
 355                 mFoldersToWalkDown
.add(synchFolderOp
); 
 357             } else if (remoteFile
.keepInSync()) { 
 358                 /// prepare content synchronization for kept-in-sync files 
 359                 SynchronizeFileOperation operation 
= new SynchronizeFileOperation( 
 366                 mFavouriteFilesToSyncContents
.add(operation
); 
 369                 /// prepare limited synchronization for regular files 
 370                 SynchronizeFileOperation operation 
= new SynchronizeFileOperation( 
 378                 mFilesToSyncContentsWithoutUpload
.add(operation
); 
 381             updatedFiles
.add(remoteFile
); 
 384         // save updated contents in local database 
 385         storageManager
.saveFolder(remoteFolder
, updatedFiles
, localFilesMap
.values()); 
 390     private void prepareOpsFromLocalKnowledge() { 
 391         List
<OCFile
> children 
= getStorageManager().getFolderContent(mLocalFolder
); 
 392         for (OCFile child 
: children
) { 
 393             /// classify file to sync/download contents later 
 394             if (child
.isFolder()) { 
 395                 /// to download children files recursively 
 396                 SynchronizeFolderOperation synchFolderOp 
=  new SynchronizeFolderOperation(  
 398                         child
.getRemotePath(), 
 402                 mFoldersToWalkDown
.add(synchFolderOp
); 
 405                 /// prepare limited synchronization for regular files 
 406                 if (!child
.isDown()) { 
 407                     mFilesForDirectDownload
.add(child
); 
 414     private void syncContents(OwnCloudClient client
) throws OperationCancelledException 
{ 
 415         startDirectDownloads(); 
 416         startContentSynchronizations(mFilesToSyncContentsWithoutUpload
, client
); 
 417         startContentSynchronizations(mFavouriteFilesToSyncContents
, client
); 
 418         walkSubfolders(client
);    // this must be the last! 
 422     private void startDirectDownloads() throws OperationCancelledException 
{ 
 423         for (OCFile file 
: mFilesForDirectDownload
) { 
 424             if (mCancellationRequested
.get()) { 
 425                 throw new OperationCancelledException(); 
 427             Intent i 
= new Intent(mContext
, FileDownloader
.class); 
 428             i
.putExtra(FileDownloader
.EXTRA_ACCOUNT
, mAccount
); 
 429             i
.putExtra(FileDownloader
.EXTRA_FILE
, file
); 
 430             mContext
.startService(i
); 
 435      * Performs a list of synchronization operations, determining if a download or upload is needed 
 436      * or if exists conflict due to changes both in local and remote contents of the each file. 
 438      * If download or upload is needed, request the operation to the corresponding service and goes 
 441      * @param filesToSyncContents       Synchronization operations to execute. 
 442      * @param client                    Interface to the remote ownCloud server. 
 444     private void startContentSynchronizations(List
<SyncOperation
> filesToSyncContents
, OwnCloudClient client
)  
 445             throws OperationCancelledException 
{ 
 447         RemoteOperationResult contentsResult 
= null
; 
 448         for (SyncOperation op
: filesToSyncContents
) { 
 449             if (mCancellationRequested
.get()) { 
 450                 throw new OperationCancelledException(); 
 452             contentsResult 
= op
.execute(getStorageManager(), mContext
); 
 453             if (!contentsResult
.isSuccess()) { 
 454                 if (contentsResult
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 457                     mFailsInFileSyncsFound
++; 
 458                     if (contentsResult
.getException() != null
) { 
 459                         Log_OC
.e(TAG
, "Error while synchronizing file : " 
 460                                 +  contentsResult
.getLogMessage(), contentsResult
.getException()); 
 462                         Log_OC
.e(TAG
, "Error while synchronizing file : " 
 463                                 + contentsResult
.getLogMessage()); 
 466                 // TODO - use the errors count in notifications 
 467             }   // won't let these fails break the synchronization process 
 472     private void walkSubfolders(OwnCloudClient client
) throws OperationCancelledException 
{ 
 473         RemoteOperationResult contentsResult 
= null
; 
 474         for (SyncOperation op
: mFoldersToWalkDown
) { 
 475             if (mCancellationRequested
.get()) { 
 476                 throw new OperationCancelledException(); 
 478             contentsResult 
= op
.execute(client
, getStorageManager());   // to watch out: possibly deep recursion 
 479             if (!contentsResult
.isSuccess()) { 
 480                 // TODO - some kind of error count, and use it with notifications 
 481                 if (contentsResult
.getException() != null
) { 
 482                     Log_OC
.e(TAG
, "Non blocking exception : " 
 483                             +  contentsResult
.getLogMessage(), contentsResult
.getException()); 
 485                     Log_OC
.e(TAG
, "Non blocking error : " + contentsResult
.getLogMessage()); 
 487             }   // won't let these fails break the synchronization process 
 493      * Creates and populates a new {@link com.owncloud.android.datamodel.OCFile} object with the data read from the server. 
 495      * @param remote    remote file read from the server (remote file or folder). 
 496      * @return          New OCFile instance representing the remote resource described by we. 
 498     private OCFile 
fillOCFile(RemoteFile remote
) { 
 499         OCFile file 
= new OCFile(remote
.getRemotePath()); 
 500         file
.setCreationTimestamp(remote
.getCreationTimestamp()); 
 501         file
.setFileLength(remote
.getLength()); 
 502         file
.setMimetype(remote
.getMimeType()); 
 503         file
.setModificationTimestamp(remote
.getModifiedTimestamp()); 
 504         file
.setEtag(remote
.getEtag()); 
 505         file
.setPermissions(remote
.getPermissions()); 
 506         file
.setRemoteId(remote
.getRemoteId()); 
 512      * Scans the default location for saving local copies of files searching for 
 513      * a 'lost' file with the same full name as the {@link com.owncloud.android.datamodel.OCFile} received as 
 516      * @param file      File to associate a possible 'lost' local file. 
 518     private void searchForLocalFileInDefaultPath(OCFile file
) { 
 519         if (file
.getStoragePath() == null 
&& !file
.isFolder()) { 
 520             File f 
= new File(FileStorageUtils
.getDefaultSavePathFor(mAccount
.name
, file
)); 
 522                 file
.setStoragePath(f
.getAbsolutePath()); 
 523                 file
.setLastSyncDateForData(f
.lastModified()); 
 528     private void sendBroadcastForNotifyingUIUpdate(boolean result
) { 
 529         // Send a broadcast message for notifying UI update 
 530         Intent uiUpdate 
= new Intent(FileDownloader
.getDownloadFinishMessage()); 
 531         uiUpdate
.putExtra(FileDownloader
.EXTRA_DOWNLOAD_RESULT
, result
); 
 532         uiUpdate
.putExtra(FileDownloader
.ACCOUNT_NAME
, mAccount
.name
); 
 533         uiUpdate
.putExtra(FileDownloader
.EXTRA_REMOTE_PATH
, mRemotePath
); 
 534         uiUpdate
.putExtra(FileDownloader
.EXTRA_FILE_PATH
, mLocalFolder
.getRemotePath()); 
 535         mContext
.sendStickyBroadcast(uiUpdate
); 
 542     public void cancel() { 
 543         mCancellationRequested
.set(true
);