2  *   ownCloud Android client application 
   4  *   @author David A. Velasco 
   5  *   Copyright (C) 2015 ownCloud Inc. 
   7  *   This program is free software: you can redistribute it and/or modify 
   8  *   it under the terms of the GNU General Public License version 2, 
   9  *   as published by the Free Software Foundation. 
  11  *   This program is distributed in the hope that it will be useful, 
  12  *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
  13  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  14  *   GNU General Public License for more details. 
  16  *   You should have received a copy of the GNU General Public License 
  17  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. 
  21 package com
.owncloud
.android
.operations
; 
  24 import java
.io
.FileInputStream
; 
  25 import java
.io
.FileOutputStream
; 
  26 import java
.io
.IOException
; 
  27 import java
.io
.InputStream
; 
  28 import java
.io
.OutputStream
; 
  29 import java
.util
.ArrayList
; 
  30 import java
.util
.HashMap
; 
  31 import java
.util
.List
; 
  33 import java
.util
.Vector
; 
  35 import org
.apache
.http
.HttpStatus
; 
  36 import android
.accounts
.Account
; 
  37 import android
.content
.Context
; 
  38 import android
.content
.Intent
; 
  39 import android
.util
.Log
; 
  40 //import android.support.v4.content.LocalBroadcastManager; 
  42 import com
.owncloud
.android
.MainApp
; 
  43 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  44 import com
.owncloud
.android
.datamodel
.OCFile
; 
  46 import com
.owncloud
.android
.lib
.common
.OwnCloudClient
; 
  47 import com
.owncloud
.android
.lib
.resources
.shares
.OCShare
; 
  48 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperation
; 
  49 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
; 
  50 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
.ResultCode
; 
  51 import com
.owncloud
.android
.lib
.common
.utils
.Log_OC
; 
  52 import com
.owncloud
.android
.lib
.resources
.shares
.GetRemoteSharesForFileOperation
; 
  53 import com
.owncloud
.android
.lib
.resources
.files
.FileUtils
; 
  54 import com
.owncloud
.android
.lib
.resources
.files
.ReadRemoteFileOperation
; 
  55 import com
.owncloud
.android
.lib
.resources
.files
.ReadRemoteFolderOperation
; 
  56 import com
.owncloud
.android
.lib
.resources
.files
.RemoteFile
; 
  58 import com
.owncloud
.android
.syncadapter
.FileSyncAdapter
; 
  59 import com
.owncloud
.android
.utils
.FileStorageUtils
; 
  64  *  Remote operation performing the synchronization of the list of files contained  
  65  *  in a folder identified with its remote path. 
  67  *  Fetches the list and properties of the files contained in the given folder, including their  
  68  *  properties, and updates the local database with them. 
  70  *  Does NOT enter in the child folders to synchronize their contents also. 
  72 public class RefreshFolderOperation 
extends RemoteOperation 
{ 
  74     private static final String TAG 
= RefreshFolderOperation
.class.getSimpleName(); 
  76     public static final String EVENT_SINGLE_FOLDER_CONTENTS_SYNCED  
=  
  77             RefreshFolderOperation
.class.getName() + ".EVENT_SINGLE_FOLDER_CONTENTS_SYNCED"; 
  78     public static final String EVENT_SINGLE_FOLDER_SHARES_SYNCED    
=  
  79             RefreshFolderOperation
.class.getName() + ".EVENT_SINGLE_FOLDER_SHARES_SYNCED"; 
  81     /** Time stamp for the synchronization process in progress */ 
  82     private long mCurrentSyncTime
; 
  84     /** Remote folder to synchronize */ 
  85     private OCFile mLocalFolder
; 
  87     /** Access to the local database */ 
  88     private FileDataStorageManager mStorageManager
; 
  90     /** Account where the file to synchronize belongs */ 
  91     private Account mAccount
; 
  93     /** Android context; necessary to send requests to the download service */ 
  94     private Context mContext
; 
  96     /** Files and folders contained in the synchronized folder after a successful operation */ 
  97     private List
<OCFile
> mChildren
; 
  99     /** Counter of conflicts found between local and remote files */ 
 100     private int mConflictsFound
; 
 102     /** Counter of failed operations in synchronization of kept-in-sync files */ 
 103     private int mFailsInFavouritesFound
; 
 106      * Map of remote and local paths to files that where locally stored in a location  
 107      * out of the ownCloud folder and couldn't be copied automatically into it  
 109     private Map
<String
, String
> mForgottenLocalFiles
; 
 111     /** 'True' means that this operation is part of a full account synchronization */  
 112     private boolean mSyncFullAccount
; 
 114     /** 'True' means that Share resources bound to the files into should be refreshed also */ 
 115     private boolean mIsShareSupported
; 
 117     /** 'True' means that the remote folder changed and should be fetched */ 
 118     private boolean mRemoteFolderChanged
; 
 120     /** 'True' means that Etag will be ignored */ 
 121     private boolean mIgnoreETag
; 
 125      * Creates a new instance of {@link RefreshFolderOperation}. 
 127      * @param   folder                  Folder to synchronize. 
 128      * @param   currentSyncTime         Time stamp for the synchronization process in progress. 
 129      * @param   syncFullAccount         'True' means that this operation is part of a full account  
 131      * @param   isShareSupported        'True' means that the server supports the sharing API.            
 132      * @param   ignoreETag              'True' means that the content of the remote folder should 
 133      *                                  be fetched and updated even though the 'eTag' did not  
 135      * @param   dataStorageManager      Interface with the local database. 
 136      * @param   account                 ownCloud account where the folder is located.  
 137      * @param   context                 Application context. 
 139     public RefreshFolderOperation(OCFile folder
, 
 140                                   long currentSyncTime
, 
 141                                   boolean syncFullAccount
, 
 142                                   boolean isShareSupported
, 
 144                                   FileDataStorageManager dataStorageManager
, 
 147         mLocalFolder 
= folder
; 
 148         mCurrentSyncTime 
= currentSyncTime
; 
 149         mSyncFullAccount 
= syncFullAccount
; 
 150         mIsShareSupported 
= isShareSupported
; 
 151         mStorageManager 
= dataStorageManager
; 
 154         mForgottenLocalFiles 
= new HashMap
<String
, String
>(); 
 155         mRemoteFolderChanged 
= false
; 
 156         mIgnoreETag 
= ignoreETag
; 
 160     public int getConflictsFound() { 
 161         return mConflictsFound
; 
 164     public int getFailsInFavouritesFound() { 
 165         return mFailsInFavouritesFound
; 
 168     public Map
<String
, String
> getForgottenLocalFiles() { 
 169         return mForgottenLocalFiles
; 
 173      * Returns the list of files and folders contained in the synchronized folder,  
 174      * if called after synchronization is complete. 
 176      * @return  List of files and folders contained in the synchronized folder. 
 178     public List
<OCFile
> getChildren() { 
 183      * Performs the synchronization. 
 188     protected RemoteOperationResult 
run(OwnCloudClient client
) { 
 189         RemoteOperationResult result 
= null
; 
 190         mFailsInFavouritesFound 
= 0; 
 192         mForgottenLocalFiles
.clear(); 
 194         if (FileUtils
.PATH_SEPARATOR
.equals(mLocalFolder
.getRemotePath()) && !mSyncFullAccount
) { 
 195             updateOCVersion(client
); 
 198         result 
= checkForChanges(client
); 
 200         if (result
.isSuccess()) { 
 201             if (mRemoteFolderChanged
) { 
 202                 result 
= fetchAndSyncRemoteFolder(client
); 
 204                 mChildren 
= mStorageManager
.getFolderContent(mLocalFolder
); 
 208         if (!mSyncFullAccount
) {             
 210                     EVENT_SINGLE_FOLDER_CONTENTS_SYNCED
, mLocalFolder
.getRemotePath(), result
 
 214         if (result
.isSuccess() && mIsShareSupported 
&& !mSyncFullAccount
) { 
 215             refreshSharesForFolder(client
); // share result is ignored  
 218         if (!mSyncFullAccount
) {             
 220                     EVENT_SINGLE_FOLDER_SHARES_SYNCED
, mLocalFolder
.getRemotePath(), result
 
 229     private void updateOCVersion(OwnCloudClient client
) { 
 230         UpdateOCVersionOperation update 
= new UpdateOCVersionOperation(mAccount
, mContext
); 
 231         RemoteOperationResult result 
= update
.execute(client
, MainApp
.getUserAgent()); 
 232         if (result
.isSuccess()) { 
 233             mIsShareSupported 
= update
.getOCVersion().isSharedSupported(); 
 238     private RemoteOperationResult 
checkForChanges(OwnCloudClient client
) { 
 239         mRemoteFolderChanged 
= true
; 
 240         RemoteOperationResult result 
= null
; 
 241         String remotePath 
= null
; 
 243         remotePath 
= mLocalFolder
.getRemotePath(); 
 244         Log_OC
.d(TAG
, "Checking changes in " + mAccount
.name 
+ remotePath
); 
 247         ReadRemoteFileOperation operation 
= new ReadRemoteFileOperation(remotePath
); 
 248         result 
= operation
.execute(client
, MainApp
.getUserAgent()); 
 249         if (result
.isSuccess()){ 
 250             OCFile remoteFolder 
= FileStorageUtils
.fillOCFile((RemoteFile
) result
.getData().get(0)); 
 253                 // check if remote and local folder are different 
 254                 mRemoteFolderChanged 
=  
 255                         !(remoteFolder
.getEtag().equalsIgnoreCase(mLocalFolder
.getEtag())); 
 258             result 
= new RemoteOperationResult(ResultCode
.OK
); 
 260             Log_OC
.i(TAG
, "Checked " + mAccount
.name 
+ remotePath 
+ " : " +  
 261                     (mRemoteFolderChanged ? 
"changed" : "not changed")); 
 265             if (result
.getCode() == ResultCode
.FILE_NOT_FOUND
) { 
 268             if (result
.isException()) { 
 269                 Log_OC
.e(TAG
, "Checked " + mAccount
.name 
+ remotePath  
+ " : " +  
 270                         result
.getLogMessage(), result
.getException()); 
 272                 Log_OC
.e(TAG
, "Checked " + mAccount
.name 
+ remotePath 
+ " : " +  
 273                         result
.getLogMessage()); 
 281     private RemoteOperationResult 
fetchAndSyncRemoteFolder(OwnCloudClient client
) { 
 282         String remotePath 
= mLocalFolder
.getRemotePath(); 
 283         ReadRemoteFolderOperation operation 
= new ReadRemoteFolderOperation(remotePath
); 
 284         RemoteOperationResult result 
= operation
.execute(client
, MainApp
.getUserAgent()); 
 285         Log_OC
.d(TAG
, "Synchronizing " + mAccount
.name 
+ remotePath
); 
 287         if (result
.isSuccess()) { 
 288             synchronizeData(result
.getData(), client
); 
 289             if (mConflictsFound 
> 0  || mFailsInFavouritesFound 
> 0) {  
 290                 result 
= new RemoteOperationResult(ResultCode
.SYNC_CONFLICT
);    
 291                     // should be a different result code, but will do the job 
 294             if (result
.getCode() == ResultCode
.FILE_NOT_FOUND
) 
 302     private void removeLocalFolder() { 
 303         if (mStorageManager
.fileExists(mLocalFolder
.getFileId())) { 
 304             String currentSavePath 
= FileStorageUtils
.getSavePath(mAccount
.name
); 
 305             mStorageManager
.removeFolder( 
 308                     (   mLocalFolder
.isDown() &&  
 309                             mLocalFolder
.getStoragePath().startsWith(currentSavePath
) 
 317      *  Synchronizes the data retrieved from the server about the contents of the target folder  
 318      *  with the current data in the local database. 
 320      *  Grants that mChildren is updated with fresh data after execution. 
 322      *  @param folderAndFiles   Remote folder and children files in Folder  
 324      *  @param client           Client instance to the remote server where the data were  
 326      *  @return                 'True' when any change was made in the local data, 'false' otherwise 
 328     private void synchronizeData(ArrayList
<Object
> folderAndFiles
, OwnCloudClient client
) { 
 329         // get 'fresh data' from the database 
 330         mLocalFolder 
= mStorageManager
.getFileByPath(mLocalFolder
.getRemotePath()); 
 332         // parse data from remote folder  
 333         OCFile remoteFolder 
= fillOCFile((RemoteFile
)folderAndFiles
.get(0)); 
 334         remoteFolder
.setParentId(mLocalFolder
.getParentId()); 
 335         remoteFolder
.setFileId(mLocalFolder
.getFileId()); 
 337         Log_OC
.d(TAG
, "Remote folder " + mLocalFolder
.getRemotePath()  
 338                 + " changed - starting update of local data "); 
 340         List
<OCFile
> updatedFiles 
= new Vector
<OCFile
>(folderAndFiles
.size() - 1); 
 341         List
<SynchronizeFileOperation
> filesToSyncContents 
= new Vector
<SynchronizeFileOperation
>(); 
 343         // get current data about local contents of the folder to synchronize 
 344         List
<OCFile
> localFiles 
= mStorageManager
.getFolderContent(mLocalFolder
); 
 345         Map
<String
, OCFile
> localFilesMap 
= new HashMap
<String
, OCFile
>(localFiles
.size()); 
 346         for (OCFile file 
: localFiles
) { 
 347             localFilesMap
.put(file
.getRemotePath(), file
); 
 350         // loop to update every child 
 351         OCFile remoteFile 
= null
, localFile 
= null
; 
 352         for (int i
=1; i
<folderAndFiles
.size(); i
++) { 
 353             /// new OCFile instance with the data from the server 
 354             remoteFile 
= fillOCFile((RemoteFile
)folderAndFiles
.get(i
)); 
 355             remoteFile
.setParentId(mLocalFolder
.getFileId()); 
 357             /// retrieve local data for the read file  
 358             //  localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath()); 
 359             localFile 
= localFilesMap
.remove(remoteFile
.getRemotePath()); 
 361             /// add to the remoteFile (the new one) data about LOCAL STATE (not existing in server) 
 362             remoteFile
.setLastSyncDateForProperties(mCurrentSyncTime
); 
 363             if (localFile 
!= null
) { 
 364                 // some properties of local state are kept unmodified 
 365                 remoteFile
.setFileId(localFile
.getFileId()); 
 366                 remoteFile
.setKeepInSync(localFile
.keepInSync()); 
 367                 remoteFile
.setLastSyncDateForData(localFile
.getLastSyncDateForData()); 
 368                 remoteFile
.setModificationTimestampAtLastSyncForData( 
 369                         localFile
.getModificationTimestampAtLastSyncForData() 
 371                 remoteFile
.setStoragePath(localFile
.getStoragePath()); 
 372                 // eTag will not be updated unless contents are synchronized  
 373                 //  (Synchronize[File|Folder]Operation with remoteFile as parameter) 
 374                 remoteFile
.setEtag(localFile
.getEtag());     
 375                 if (remoteFile
.isFolder()) { 
 376                     remoteFile
.setFileLength(localFile
.getFileLength());  
 377                         // TODO move operations about size of folders to FileContentProvider 
 378                 } else if (mRemoteFolderChanged 
&& remoteFile
.isImage() && 
 379                         remoteFile
.getModificationTimestamp() != 
 380                                 localFile
.getModificationTimestamp()) { 
 381                     remoteFile
.setNeedsUpdateThumbnail(true
); 
 382                     Log
.d(TAG
, "Image " + remoteFile
.getFileName() + " updated on the server"); 
 384                 remoteFile
.setPublicLink(localFile
.getPublicLink()); 
 385                 remoteFile
.setShareByLink(localFile
.isShareByLink()); 
 387                 // remote eTag will not be updated unless contents are synchronized  
 388                 //  (Synchronize[File|Folder]Operation with remoteFile as parameter) 
 389                 remoteFile
.setEtag("");  
 392             /// check and fix, if needed, local storage path 
 393             checkAndFixForeignStoragePath(remoteFile
);      // policy - local files are COPIED  
 394                                                             // into the ownCloud local folder; 
 395             searchForLocalFileInDefaultPath(remoteFile
);    // legacy    
 397             /// prepare content synchronization for kept-in-sync files 
 398             if (remoteFile
.keepInSync()) { 
 399                 SynchronizeFileOperation operation 
= new SynchronizeFileOperation(  localFile
,         
 406                 filesToSyncContents
.add(operation
); 
 409             updatedFiles
.add(remoteFile
); 
 412         // save updated contents in local database 
 413         mStorageManager
.saveFolder(remoteFolder
, updatedFiles
, localFilesMap
.values()); 
 415         // request for the synchronization of file contents AFTER saving current remote properties 
 416         startContentSynchronizations(filesToSyncContents
, client
); 
 418         mChildren 
= updatedFiles
; 
 422      * Performs a list of synchronization operations, determining if a download or upload is needed 
 423      * or if exists conflict due to changes both in local and remote contents of the each file. 
 425      * If download or upload is needed, request the operation to the corresponding service and goes  
 428      * @param filesToSyncContents       Synchronization operations to execute. 
 429      * @param client                    Interface to the remote ownCloud server. 
 431     private void startContentSynchronizations( 
 432             List
<SynchronizeFileOperation
> filesToSyncContents
, OwnCloudClient client
 
 434         RemoteOperationResult contentsResult 
= null
; 
 435         for (SynchronizeFileOperation op
: filesToSyncContents
) { 
 436             contentsResult 
= op
.execute(mStorageManager
, mContext
);   // async 
 437             if (!contentsResult
.isSuccess()) { 
 438                 if (contentsResult
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 441                     mFailsInFavouritesFound
++; 
 442                     if (contentsResult
.getException() != null
) { 
 443                         Log_OC
.e(TAG
, "Error while synchronizing favourites : "  
 444                                 +  contentsResult
.getLogMessage(), contentsResult
.getException()); 
 446                         Log_OC
.e(TAG
, "Error while synchronizing favourites : "  
 447                                 + contentsResult
.getLogMessage()); 
 450             }   // won't let these fails break the synchronization process 
 455     public boolean isMultiStatus(int status
) { 
 456         return (status 
== HttpStatus
.SC_MULTI_STATUS
);  
 460      * Creates and populates a new {@link OCFile} object with the data read from the server. 
 462      * @param remote    remote file read from the server (remote file or folder). 
 463      * @return          New OCFile instance representing the remote resource described by we. 
 465     private OCFile 
fillOCFile(RemoteFile remote
) { 
 466         OCFile file 
= new OCFile(remote
.getRemotePath()); 
 467         file
.setCreationTimestamp(remote
.getCreationTimestamp()); 
 468         file
.setFileLength(remote
.getLength()); 
 469         file
.setMimetype(remote
.getMimeType()); 
 470         file
.setModificationTimestamp(remote
.getModifiedTimestamp()); 
 471         file
.setEtag(remote
.getEtag()); 
 472         file
.setPermissions(remote
.getPermissions()); 
 473         file
.setRemoteId(remote
.getRemoteId()); 
 479      * Checks the storage path of the OCFile received as parameter.  
 480      * If it's out of the local ownCloud folder, tries to copy the file inside it.  
 482      * If the copy fails, the link to the local file is nullified. The account of forgotten  
 483      * files is kept in {@link #mForgottenLocalFiles} 
 485      * @param file      File to check and fix. 
 487     private void checkAndFixForeignStoragePath(OCFile file
) { 
 488         String storagePath 
= file
.getStoragePath(); 
 489         String expectedPath 
= FileStorageUtils
.getDefaultSavePathFor(mAccount
.name
, file
); 
 490         if (storagePath 
!= null 
&& !storagePath
.equals(expectedPath
)) { 
 491             /// fix storagePaths out of the local ownCloud folder 
 492             File originalFile 
= new File(storagePath
); 
 493             if (FileStorageUtils
.getUsableSpace(mAccount
.name
) < originalFile
.length()) { 
 494                 mForgottenLocalFiles
.put(file
.getRemotePath(), storagePath
); 
 495                 file
.setStoragePath(null
); 
 498                 InputStream 
in = null
; 
 499                 OutputStream out 
= null
; 
 501                     File expectedFile 
= new File(expectedPath
); 
 502                     File expectedParent 
= expectedFile
.getParentFile(); 
 503                     expectedParent
.mkdirs(); 
 504                     if (!expectedParent
.isDirectory()) { 
 505                         throw new IOException( 
 506                                 "Unexpected error: parent directory could not be created" 
 509                     expectedFile
.createNewFile(); 
 510                     if (!expectedFile
.isFile()) { 
 511                         throw new IOException("Unexpected error: target file could not be created"); 
 513                     in = new FileInputStream(originalFile
); 
 514                     out 
= new FileOutputStream(expectedFile
); 
 515                     byte[] buf 
= new byte[1024]; 
 517                     while ((len 
= in.read(buf
)) > 0){ 
 518                         out
.write(buf
, 0, len
); 
 520                     file
.setStoragePath(expectedPath
); 
 522                 } catch (Exception e
) { 
 523                     Log_OC
.e(TAG
, "Exception while copying foreign file " + expectedPath
, e
); 
 524                     mForgottenLocalFiles
.put(file
.getRemotePath(), storagePath
); 
 525                     file
.setStoragePath(null
); 
 529                         if (in != null
) in.close(); 
 530                     } catch (Exception e
) { 
 531                         Log_OC
.d(TAG
, "Weird exception while closing input stream for "  
 532                                 + storagePath 
+ " (ignoring)", e
); 
 535                         if (out 
!= null
) out
.close(); 
 536                     } catch (Exception e
) { 
 537                         Log_OC
.d(TAG
, "Weird exception while closing output stream for "  
 538                                 + expectedPath 
+ " (ignoring)", e
); 
 546     private RemoteOperationResult 
refreshSharesForFolder(OwnCloudClient client
) { 
 547         RemoteOperationResult result 
= null
; 
 550         GetRemoteSharesForFileOperation operation 
=  
 551                 new GetRemoteSharesForFileOperation(mLocalFolder
.getRemotePath(), false
, true
); 
 552         result 
= operation
.execute(client
, MainApp
.getUserAgent()); 
 554         if (result
.isSuccess()) { 
 555             // update local database 
 556             ArrayList
<OCShare
> shares 
= new ArrayList
<OCShare
>(); 
 557             for(Object obj
: result
.getData()) { 
 558                 shares
.add((OCShare
) obj
); 
 560             mStorageManager
.saveSharesInFolder(shares
, mLocalFolder
); 
 568      * Scans the default location for saving local copies of files searching for 
 569      * a 'lost' file with the same full name as the {@link OCFile} received as  
 572      * @param file      File to associate a possible 'lost' local file. 
 574     private void searchForLocalFileInDefaultPath(OCFile file
) { 
 575         if (file
.getStoragePath() == null 
&& !file
.isFolder()) { 
 576             File f 
= new File(FileStorageUtils
.getDefaultSavePathFor(mAccount
.name
, file
)); 
 578                 file
.setStoragePath(f
.getAbsolutePath()); 
 579                 file
.setLastSyncDateForData(f
.lastModified()); 
 586      * Sends a message to any application component interested in the progress  
 587      * of the synchronization. 
 590      * @param dirRemotePath     Remote path of a folder that was just synchronized  
 591      *                          (with or without success) 
 594     private void sendLocalBroadcast( 
 595             String event
, String dirRemotePath
, RemoteOperationResult result
 
 597         Log_OC
.d(TAG
, "Send broadcast " + event
); 
 598         Intent intent 
= new Intent(event
); 
 599         intent
.putExtra(FileSyncAdapter
.EXTRA_ACCOUNT_NAME
, mAccount
.name
); 
 600         if (dirRemotePath 
!= null
) { 
 601             intent
.putExtra(FileSyncAdapter
.EXTRA_FOLDER_PATH
, dirRemotePath
); 
 603         intent
.putExtra(FileSyncAdapter
.EXTRA_RESULT
, result
); 
 604         mContext
.sendStickyBroadcast(intent
); 
 605         //LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent); 
 609     public boolean getRemoteFolderChanged() { 
 610         return mRemoteFolderChanged
;