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
; 
  21 import java
.io
.FileInputStream
; 
  22 import java
.io
.FileOutputStream
; 
  23 import java
.io
.IOException
; 
  24 import java
.io
.InputStream
; 
  25 import java
.io
.OutputStream
; 
  26 import java
.util
.ArrayList
; 
  27 import java
.util
.HashMap
; 
  28 import java
.util
.List
; 
  30 import java
.util
.Vector
; 
  32 import org
.apache
.http
.HttpStatus
; 
  33 import android
.accounts
.Account
; 
  34 import android
.content
.Context
; 
  35 import android
.content
.Intent
; 
  36 import android
.util
.Log
; 
  37 //import android.support.v4.content.LocalBroadcastManager; 
  39 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  40 import com
.owncloud
.android
.datamodel
.OCFile
; 
  42 import com
.owncloud
.android
.lib
.common
.OwnCloudClient
; 
  43 import com
.owncloud
.android
.lib
.resources
.shares
.OCShare
; 
  44 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperation
; 
  45 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
; 
  46 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
.ResultCode
; 
  47 import com
.owncloud
.android
.lib
.common
.utils
.Log_OC
; 
  48 import com
.owncloud
.android
.lib
.resources
.shares
.GetRemoteSharesForFileOperation
; 
  49 import com
.owncloud
.android
.lib
.resources
.files
.FileUtils
; 
  50 import com
.owncloud
.android
.lib
.resources
.files
.ReadRemoteFileOperation
; 
  51 import com
.owncloud
.android
.lib
.resources
.files
.ReadRemoteFolderOperation
; 
  52 import com
.owncloud
.android
.lib
.resources
.files
.RemoteFile
; 
  54 import com
.owncloud
.android
.syncadapter
.FileSyncAdapter
; 
  55 import com
.owncloud
.android
.utils
.FileStorageUtils
; 
  60  *  Remote operation performing the synchronization of the list of files contained  
  61  *  in a folder identified with its remote path. 
  63  *  Fetches the list and properties of the files contained in the given folder, including their  
  64  *  properties, and updates the local database with them. 
  66  *  Does NOT enter in the child folders to synchronize their contents also. 
  68  *  @author David A. Velasco 
  70 public class SynchronizeFolderOperation 
extends RemoteOperation 
{ 
  72     private static final String TAG 
= SynchronizeFolderOperation
.class.getSimpleName(); 
  74     public static final String EVENT_SINGLE_FOLDER_CONTENTS_SYNCED  
=  
  75             SynchronizeFolderOperation
.class.getName() + ".EVENT_SINGLE_FOLDER_CONTENTS_SYNCED"; 
  76     public static final String EVENT_SINGLE_FOLDER_SHARES_SYNCED    
=  
  77             SynchronizeFolderOperation
.class.getName() + ".EVENT_SINGLE_FOLDER_SHARES_SYNCED"; 
  79     /** Time stamp for the synchronization process in progress */ 
  80     private long mCurrentSyncTime
; 
  82     /** Remote folder to synchronize */ 
  83     private OCFile mLocalFolder
; 
  85     /** Access to the local database */ 
  86     private FileDataStorageManager mStorageManager
; 
  88     /** Account where the file to synchronize belongs */ 
  89     private Account mAccount
; 
  91     /** Android context; necessary to send requests to the download service */ 
  92     private Context mContext
; 
  94     /** Files and folders contained in the synchronized folder after a successful operation */ 
  95     private List
<OCFile
> mChildren
; 
  97     /** Counter of conflicts found between local and remote files */ 
  98     private int mConflictsFound
; 
 100     /** Counter of failed operations in synchronization of kept-in-sync files */ 
 101     private int mFailsInFavouritesFound
; 
 104      * Map of remote and local paths to files that where locally stored in a location  
 105      * out of the ownCloud folder and couldn't be copied automatically into it  
 107     private Map
<String
, String
> mForgottenLocalFiles
; 
 109     /** 'True' means that this operation is part of a full account synchronization */  
 110     private boolean mSyncFullAccount
; 
 112     /** 'True' means that Share resources bound to the files into should be refreshed also */ 
 113     private boolean mIsShareSupported
; 
 115     /** 'True' means that the remote folder changed and should be fetched */ 
 116     private boolean mRemoteFolderChanged
; 
 118     /** 'True' means that Etag will be ignored */ 
 119     private boolean mIgnoreETag
; 
 123      * Creates a new instance of {@link SynchronizeFolderOperation}. 
 125      * @param   folder                  Folder to synchronize. 
 126      * @param   currentSyncTime         Time stamp for the synchronization process in progress. 
 127      * @param   syncFullAccount         'True' means that this operation is part of a full account  
 129      * @param   isShareSupported        'True' means that the server supports the sharing API.            
 130      * @param   ignoreEtag              'True' means that the content of the remote folder should 
 131      *                                  be fetched and updated even though the 'eTag' did not  
 133      * @param   dataStorageManager      Interface with the local database. 
 134      * @param   account                 ownCloud account where the folder is located.  
 135      * @param   context                 Application context. 
 137     public SynchronizeFolderOperation(  OCFile folder
,  
 138                                         long currentSyncTime
,  
 139                                         boolean syncFullAccount
, 
 140                                         boolean isShareSupported
, 
 142                                         FileDataStorageManager dataStorageManager
,  
 145         mLocalFolder 
= folder
; 
 146         mCurrentSyncTime 
= currentSyncTime
; 
 147         mSyncFullAccount 
= syncFullAccount
; 
 148         mIsShareSupported 
= isShareSupported
; 
 149         mStorageManager 
= dataStorageManager
; 
 152         mForgottenLocalFiles 
= new HashMap
<String
, String
>(); 
 153         mRemoteFolderChanged 
= false
; 
 154         mIgnoreETag 
= ignoreETag
; 
 158     public int getConflictsFound() { 
 159         return mConflictsFound
; 
 162     public int getFailsInFavouritesFound() { 
 163         return mFailsInFavouritesFound
; 
 166     public Map
<String
, String
> getForgottenLocalFiles() { 
 167         return mForgottenLocalFiles
; 
 171      * Returns the list of files and folders contained in the synchronized folder,  
 172      * if called after synchronization is complete. 
 174      * @return  List of files and folders contained in the synchronized folder. 
 176     public List
<OCFile
> getChildren() { 
 181      * Performs the synchronization. 
 186     protected RemoteOperationResult 
run(OwnCloudClient client
) { 
 187         RemoteOperationResult result 
= null
; 
 188         mFailsInFavouritesFound 
= 0; 
 190         mForgottenLocalFiles
.clear(); 
 192         if (FileUtils
.PATH_SEPARATOR
.equals(mLocalFolder
.getRemotePath()) && !mSyncFullAccount
) { 
 193             updateOCVersion(client
); 
 196         result 
= checkForChanges(client
); 
 198         if (result
.isSuccess()) { 
 199             if (mRemoteFolderChanged
) { 
 200                 result 
= fetchAndSyncRemoteFolder(client
); 
 202                 mChildren 
= mStorageManager
.getFolderContent(mLocalFolder
); 
 206         if (!mSyncFullAccount
) {             
 208                     EVENT_SINGLE_FOLDER_CONTENTS_SYNCED
, mLocalFolder
.getRemotePath(), result
 
 212         if (result
.isSuccess() && mIsShareSupported 
&& !mSyncFullAccount
) { 
 213             refreshSharesForFolder(client
); // share result is ignored  
 216         if (!mSyncFullAccount
) {             
 218                     EVENT_SINGLE_FOLDER_SHARES_SYNCED
, mLocalFolder
.getRemotePath(), result
 
 227     private void updateOCVersion(OwnCloudClient client
) { 
 228         UpdateOCVersionOperation update 
= new UpdateOCVersionOperation(mAccount
, mContext
); 
 229         RemoteOperationResult result 
= update
.execute(client
); 
 230         if (result
.isSuccess()) { 
 231             mIsShareSupported 
= update
.getOCVersion().isSharedSupported(); 
 236     private RemoteOperationResult 
checkForChanges(OwnCloudClient client
) { 
 237         mRemoteFolderChanged 
= true
; 
 238         RemoteOperationResult result 
= null
; 
 239         String remotePath 
= null
; 
 241         remotePath 
= mLocalFolder
.getRemotePath(); 
 242         Log_OC
.d(TAG
, "Checking changes in " + mAccount
.name 
+ remotePath
); 
 245         ReadRemoteFileOperation operation 
= new ReadRemoteFileOperation(remotePath
); 
 246         result 
= operation
.execute(client
); 
 247         if (result
.isSuccess()){ 
 248             OCFile remoteFolder 
= FileStorageUtils
.fillOCFile((RemoteFile
) result
.getData().get(0)); 
 251                 // check if remote and local folder are different 
 252                 mRemoteFolderChanged 
=  
 253                         !(remoteFolder
.getEtag().equalsIgnoreCase(mLocalFolder
.getEtag())); 
 256             result 
= new RemoteOperationResult(ResultCode
.OK
); 
 258             Log_OC
.i(TAG
, "Checked " + mAccount
.name 
+ remotePath 
+ " : " +  
 259                     (mRemoteFolderChanged ? 
"changed" : "not changed")); 
 263             if (result
.getCode() == ResultCode
.FILE_NOT_FOUND
) { 
 266             if (result
.isException()) { 
 267                 Log_OC
.e(TAG
, "Checked " + mAccount
.name 
+ remotePath  
+ " : " +  
 268                         result
.getLogMessage(), result
.getException()); 
 270                 Log_OC
.e(TAG
, "Checked " + mAccount
.name 
+ remotePath 
+ " : " +  
 271                         result
.getLogMessage()); 
 279     private RemoteOperationResult 
fetchAndSyncRemoteFolder(OwnCloudClient client
) { 
 280         String remotePath 
= mLocalFolder
.getRemotePath(); 
 281         ReadRemoteFolderOperation operation 
= new ReadRemoteFolderOperation(remotePath
); 
 282         RemoteOperationResult result 
= operation
.execute(client
); 
 283         Log_OC
.d(TAG
, "Synchronizing " + mAccount
.name 
+ remotePath
); 
 285         if (result
.isSuccess()) { 
 286             synchronizeData(result
.getData(), client
); 
 287             if (mConflictsFound 
> 0  || mFailsInFavouritesFound 
> 0) {  
 288                 result 
= new RemoteOperationResult(ResultCode
.SYNC_CONFLICT
);    
 289                     // should be a different result code, but will do the job 
 292             if (result
.getCode() == ResultCode
.FILE_NOT_FOUND
) 
 300     private void removeLocalFolder() { 
 301         if (mStorageManager
.fileExists(mLocalFolder
.getFileId())) { 
 302             String currentSavePath 
= FileStorageUtils
.getSavePath(mAccount
.name
); 
 303             mStorageManager
.removeFolder( 
 306                     (   mLocalFolder
.isDown() &&  
 307                             mLocalFolder
.getStoragePath().startsWith(currentSavePath
) 
 315      *  Synchronizes the data retrieved from the server about the contents of the target folder  
 316      *  with the current data in the local database. 
 318      *  Grants that mChildren is updated with fresh data after execution. 
 320      *  @param folderAndFiles   Remote folder and children files in Folder  
 322      *  @param client           Client instance to the remote server where the data were  
 324      *  @return                 'True' when any change was made in the local data, 'false' otherwise 
 326     private void synchronizeData(ArrayList
<Object
> folderAndFiles
, OwnCloudClient client
) { 
 327         // get 'fresh data' from the database 
 328         mLocalFolder 
= mStorageManager
.getFileByPath(mLocalFolder
.getRemotePath()); 
 330         // parse data from remote folder  
 331         OCFile remoteFolder 
= fillOCFile((RemoteFile
)folderAndFiles
.get(0)); 
 332         remoteFolder
.setParentId(mLocalFolder
.getParentId()); 
 333         remoteFolder
.setFileId(mLocalFolder
.getFileId()); 
 335         Log_OC
.d(TAG
, "Remote folder " + mLocalFolder
.getRemotePath()  
 336                 + " changed - starting update of local data "); 
 338         List
<OCFile
> updatedFiles 
= new Vector
<OCFile
>(folderAndFiles
.size() - 1); 
 339         List
<SynchronizeFileOperation
> filesToSyncContents 
= new Vector
<SynchronizeFileOperation
>(); 
 341         // get current data about local contents of the folder to synchronize 
 342         List
<OCFile
> localFiles 
= mStorageManager
.getFolderContent(mLocalFolder
); 
 343         Map
<String
, OCFile
> localFilesMap 
= new HashMap
<String
, OCFile
>(localFiles
.size()); 
 344         for (OCFile file 
: localFiles
) { 
 345             localFilesMap
.put(file
.getRemotePath(), file
); 
 348         // loop to update every child 
 349         OCFile remoteFile 
= null
, localFile 
= null
; 
 350         for (int i
=1; i
<folderAndFiles
.size(); i
++) { 
 351             /// new OCFile instance with the data from the server 
 352             remoteFile 
= fillOCFile((RemoteFile
)folderAndFiles
.get(i
)); 
 353             remoteFile
.setParentId(mLocalFolder
.getFileId()); 
 355             /// retrieve local data for the read file  
 356             //  localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath()); 
 357             localFile 
= localFilesMap
.remove(remoteFile
.getRemotePath()); 
 359             /// add to the remoteFile (the new one) data about LOCAL STATE (not existing in server) 
 360             remoteFile
.setLastSyncDateForProperties(mCurrentSyncTime
); 
 361             if (localFile 
!= null
) { 
 362                 // some properties of local state are kept unmodified 
 363                 remoteFile
.setFileId(localFile
.getFileId()); 
 364                 remoteFile
.setKeepInSync(localFile
.keepInSync()); 
 365                 remoteFile
.setLastSyncDateForData(localFile
.getLastSyncDateForData()); 
 366                 remoteFile
.setModificationTimestampAtLastSyncForData( 
 367                         localFile
.getModificationTimestampAtLastSyncForData() 
 369                 remoteFile
.setStoragePath(localFile
.getStoragePath()); 
 370                 // eTag will not be updated unless contents are synchronized  
 371                 //  (Synchronize[File|Folder]Operation with remoteFile as parameter) 
 372                 remoteFile
.setEtag(localFile
.getEtag());     
 373                 if (remoteFile
.isFolder()) { 
 374                     remoteFile
.setFileLength(localFile
.getFileLength());  
 375                         // TODO move operations about size of folders to FileContentProvider 
 376                 } else if (mRemoteFolderChanged 
&& remoteFile
.isImage() && 
 377                         remoteFile
.getModificationTimestamp() != localFile
.getModificationTimestamp()) { 
 378                     remoteFile
.setNeedsUpdateThumbnail(true
); 
 379                     Log
.d(TAG
, "Image " + remoteFile
.getFileName() + " updated on the server"); 
 381                 remoteFile
.setPublicLink(localFile
.getPublicLink()); 
 382                 remoteFile
.setShareByLink(localFile
.isShareByLink()); 
 384                 // remote eTag will not be updated unless contents are synchronized  
 385                 //  (Synchronize[File|Folder]Operation with remoteFile as parameter) 
 386                 remoteFile
.setEtag("");  
 389             /// check and fix, if needed, local storage path 
 390             checkAndFixForeignStoragePath(remoteFile
);      // policy - local files are COPIED  
 391                                                             // into the ownCloud local folder; 
 392             searchForLocalFileInDefaultPath(remoteFile
);    // legacy    
 394             /// prepare content synchronization for kept-in-sync files 
 395             if (remoteFile
.keepInSync()) { 
 396                 SynchronizeFileOperation operation 
= new SynchronizeFileOperation(  localFile
,         
 403                 filesToSyncContents
.add(operation
); 
 406             updatedFiles
.add(remoteFile
); 
 409         // save updated contents in local database 
 410         mStorageManager
.saveFolder(remoteFolder
, updatedFiles
, localFilesMap
.values()); 
 412         // request for the synchronization of file contents AFTER saving current remote properties 
 413         startContentSynchronizations(filesToSyncContents
, client
); 
 415         mChildren 
= updatedFiles
; 
 419      * Performs a list of synchronization operations, determining if a download or upload is needed 
 420      * or if exists conflict due to changes both in local and remote contents of the each file. 
 422      * If download or upload is needed, request the operation to the corresponding service and goes  
 425      * @param filesToSyncContents       Synchronization operations to execute. 
 426      * @param client                    Interface to the remote ownCloud server. 
 428     private void startContentSynchronizations( 
 429             List
<SynchronizeFileOperation
> filesToSyncContents
, OwnCloudClient client
 
 431         RemoteOperationResult contentsResult 
= null
; 
 432         for (SynchronizeFileOperation op
: filesToSyncContents
) { 
 433             contentsResult 
= op
.execute(mStorageManager
, mContext
);   // async 
 434             if (!contentsResult
.isSuccess()) { 
 435                 if (contentsResult
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 438                     mFailsInFavouritesFound
++; 
 439                     if (contentsResult
.getException() != null
) { 
 440                         Log_OC
.e(TAG
, "Error while synchronizing favourites : "  
 441                                 +  contentsResult
.getLogMessage(), contentsResult
.getException()); 
 443                         Log_OC
.e(TAG
, "Error while synchronizing favourites : "  
 444                                 + contentsResult
.getLogMessage()); 
 447             }   // won't let these fails break the synchronization process 
 452     public boolean isMultiStatus(int status
) { 
 453         return (status 
== HttpStatus
.SC_MULTI_STATUS
);  
 457      * Creates and populates a new {@link OCFile} object with the data read from the server. 
 459      * @param remote    remote file read from the server (remote file or folder). 
 460      * @return          New OCFile instance representing the remote resource described by we. 
 462     private OCFile 
fillOCFile(RemoteFile remote
) { 
 463         OCFile file 
= new OCFile(remote
.getRemotePath()); 
 464         file
.setCreationTimestamp(remote
.getCreationTimestamp()); 
 465         file
.setFileLength(remote
.getLength()); 
 466         file
.setMimetype(remote
.getMimeType()); 
 467         file
.setModificationTimestamp(remote
.getModifiedTimestamp()); 
 468         file
.setEtag(remote
.getEtag()); 
 469         file
.setPermissions(remote
.getPermissions()); 
 470         file
.setRemoteId(remote
.getRemoteId()); 
 476      * Checks the storage path of the OCFile received as parameter.  
 477      * If it's out of the local ownCloud folder, tries to copy the file inside it.  
 479      * If the copy fails, the link to the local file is nullified. The account of forgotten  
 480      * files is kept in {@link #mForgottenLocalFiles} 
 482      * @param file      File to check and fix. 
 484     private void checkAndFixForeignStoragePath(OCFile file
) { 
 485         String storagePath 
= file
.getStoragePath(); 
 486         String expectedPath 
= FileStorageUtils
.getDefaultSavePathFor(mAccount
.name
, file
); 
 487         if (storagePath 
!= null 
&& !storagePath
.equals(expectedPath
)) { 
 488             /// fix storagePaths out of the local ownCloud folder 
 489             File originalFile 
= new File(storagePath
); 
 490             if (FileStorageUtils
.getUsableSpace(mAccount
.name
) < originalFile
.length()) { 
 491                 mForgottenLocalFiles
.put(file
.getRemotePath(), storagePath
); 
 492                 file
.setStoragePath(null
); 
 495                 InputStream 
in = null
; 
 496                 OutputStream out 
= null
; 
 498                     File expectedFile 
= new File(expectedPath
); 
 499                     File expectedParent 
= expectedFile
.getParentFile(); 
 500                     expectedParent
.mkdirs(); 
 501                     if (!expectedParent
.isDirectory()) { 
 502                         throw new IOException( 
 503                                 "Unexpected error: parent directory could not be created" 
 506                     expectedFile
.createNewFile(); 
 507                     if (!expectedFile
.isFile()) { 
 508                         throw new IOException("Unexpected error: target file could not be created"); 
 510                     in = new FileInputStream(originalFile
); 
 511                     out 
= new FileOutputStream(expectedFile
); 
 512                     byte[] buf 
= new byte[1024]; 
 514                     while ((len 
= in.read(buf
)) > 0){ 
 515                         out
.write(buf
, 0, len
); 
 517                     file
.setStoragePath(expectedPath
); 
 519                 } catch (Exception e
) { 
 520                     Log_OC
.e(TAG
, "Exception while copying foreign file " + expectedPath
, e
); 
 521                     mForgottenLocalFiles
.put(file
.getRemotePath(), storagePath
); 
 522                     file
.setStoragePath(null
); 
 526                         if (in != null
) in.close(); 
 527                     } catch (Exception e
) { 
 528                         Log_OC
.d(TAG
, "Weird exception while closing input stream for "  
 529                                 + storagePath 
+ " (ignoring)", e
); 
 532                         if (out 
!= null
) out
.close(); 
 533                     } catch (Exception e
) { 
 534                         Log_OC
.d(TAG
, "Weird exception while closing output stream for "  
 535                                 + expectedPath 
+ " (ignoring)", e
); 
 543     private RemoteOperationResult 
refreshSharesForFolder(OwnCloudClient client
) { 
 544         RemoteOperationResult result 
= null
; 
 547         GetRemoteSharesForFileOperation operation 
=  
 548                 new GetRemoteSharesForFileOperation(mLocalFolder
.getRemotePath(), false
, true
); 
 549         result 
= operation
.execute(client
); 
 551         if (result
.isSuccess()) { 
 552             // update local database 
 553             ArrayList
<OCShare
> shares 
= new ArrayList
<OCShare
>(); 
 554             for(Object obj
: result
.getData()) { 
 555                 shares
.add((OCShare
) obj
); 
 557             mStorageManager
.saveSharesInFolder(shares
, mLocalFolder
); 
 565      * Scans the default location for saving local copies of files searching for 
 566      * a 'lost' file with the same full name as the {@link OCFile} received as  
 569      * @param file      File to associate a possible 'lost' local file. 
 571     private void searchForLocalFileInDefaultPath(OCFile file
) { 
 572         if (file
.getStoragePath() == null 
&& !file
.isFolder()) { 
 573             File f 
= new File(FileStorageUtils
.getDefaultSavePathFor(mAccount
.name
, file
)); 
 575                 file
.setStoragePath(f
.getAbsolutePath()); 
 576                 file
.setLastSyncDateForData(f
.lastModified()); 
 583      * Sends a message to any application component interested in the progress  
 584      * of the synchronization. 
 587      * @param dirRemotePath     Remote path of a folder that was just synchronized  
 588      *                          (with or without success) 
 591     private void sendLocalBroadcast( 
 592             String event
, String dirRemotePath
, RemoteOperationResult result
 
 594         Log_OC
.d(TAG
, "Send broadcast " + event
); 
 595         Intent intent 
= new Intent(event
); 
 596         intent
.putExtra(FileSyncAdapter
.EXTRA_ACCOUNT_NAME
, mAccount
.name
); 
 597         if (dirRemotePath 
!= null
) { 
 598             intent
.putExtra(FileSyncAdapter
.EXTRA_FOLDER_PATH
, dirRemotePath
); 
 600         intent
.putExtra(FileSyncAdapter
.EXTRA_RESULT
, result
); 
 601         mContext
.sendStickyBroadcast(intent
); 
 602         //LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent); 
 606     public boolean getRemoteFolderChanged() { 
 607         return mRemoteFolderChanged
;