1 /* ownCloud Android client application 
   2  *   Copyright (C) 2011  Bartek Przybylski 
   4  *   This program is free software: you can redistribute it and/or modify 
   5  *   it under the terms of the GNU General Public License as published by 
   6  *   the Free Software Foundation, either version 3 of the License, or 
   7  *   (at your option) any later version. 
   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
.syncadapter
; 
  21 import java
.io
.IOException
; 
  22 import java
.net
.UnknownHostException
; 
  23 import java
.util
.ArrayList
; 
  24 import java
.util
.HashMap
; 
  25 import java
.util
.List
; 
  28 import org
.apache
.jackrabbit
.webdav
.DavException
; 
  30 import com
.owncloud
.android
.R
; 
  31 import com
.owncloud
.android
.datamodel
.DataStorageManager
; 
  32 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  33 import com
.owncloud
.android
.datamodel
.OCFile
; 
  34 import com
.owncloud
.android
.operations
.RemoteOperationResult
; 
  35 import com
.owncloud
.android
.operations
.SynchronizeFolderOperation
; 
  36 import com
.owncloud
.android
.operations
.UpdateOCVersionOperation
; 
  37 import com
.owncloud
.android
.operations
.RemoteOperationResult
.ResultCode
; 
  38 import com
.owncloud
.android
.ui
.activity
.ErrorsWhileCopyingHandlerActivity
; 
  39 import android
.accounts
.Account
; 
  40 import android
.app
.Notification
; 
  41 import android
.app
.NotificationManager
; 
  42 import android
.app
.PendingIntent
; 
  43 import android
.content
.ContentProviderClient
; 
  44 import android
.content
.ContentResolver
; 
  45 import android
.content
.Context
; 
  46 import android
.content
.Intent
; 
  47 import android
.content
.SyncResult
; 
  48 import android
.os
.Bundle
; 
  49 import android
.util
.Log
; 
  52  * SyncAdapter implementation for syncing sample SyncAdapter contacts to the 
  53  * platform ContactOperations provider. 
  55  * @author Bartek Przybylski 
  57 public class FileSyncAdapter 
extends AbstractOwnCloudSyncAdapter 
{ 
  59     private final static String TAG 
= "FileSyncAdapter"; 
  62      * Maximum number of failed folder synchronizations that are supported before finishing the synchronization operation 
  64     private static final int MAX_FAILED_RESULTS 
= 3;  
  66     private long mCurrentSyncTime
; 
  67     private boolean mCancellation
; 
  68     private boolean mIsManualSync
; 
  69     private int mFailedResultsCounter
;     
  70     private RemoteOperationResult mLastFailedResult
; 
  71     private SyncResult mSyncResult
; 
  72     private int mConflictsFound
; 
  73     private int mFailsInFavouritesFound
; 
  74     private Map
<String
, String
> mForgottenLocalFiles
; 
  77     public FileSyncAdapter(Context context
, boolean autoInitialize
) { 
  78         super(context
, autoInitialize
); 
  85     public synchronized void onPerformSync(Account account
, Bundle extras
, 
  86             String authority
, ContentProviderClient provider
, 
  87             SyncResult syncResult
) { 
  89         mCancellation 
= false
; 
  90         mIsManualSync 
= extras
.getBoolean(ContentResolver
.SYNC_EXTRAS_MANUAL
, false
); 
  91         mFailedResultsCounter 
= 0; 
  92         mLastFailedResult 
= null
; 
  94         mFailsInFavouritesFound 
= 0; 
  95         mForgottenLocalFiles 
= new HashMap
<String
, String
>(); 
  96         mSyncResult 
= syncResult
; 
  97         mSyncResult
.fullSyncRequested 
= false
; 
  98         mSyncResult
.delayUntil 
= 60*60*24; // sync after 24h 
 100         this.setAccount(account
); 
 101         this.setContentProvider(provider
); 
 102         this.setStorageManager(new FileDataStorageManager(account
, getContentProvider())); 
 104             this.initClientForCurrentAccount(); 
 105         } catch (UnknownHostException e
) { 
 106             /// the account is unknown for the Synchronization Manager, or unreachable for this context; don't try this again 
 107             mSyncResult
.tooManyRetries 
= true
; 
 108             notifyFailedSynchronization(); 
 112         Log
.d(TAG
, "Synchronization of ownCloud account " + account
.name 
+ " starting"); 
 113         sendStickyBroadcast(true
, null
, null
);  // message to signal the start of the synchronization to the UI 
 117             mCurrentSyncTime 
= System
.currentTimeMillis(); 
 118             if (!mCancellation
) { 
 119                 fetchData(OCFile
.PATH_SEPARATOR
, DataStorageManager
.ROOT_PARENT_ID
); 
 122                 Log
.d(TAG
, "Leaving synchronization before any remote request due to cancellation was requested"); 
 127             // it's important making this although very unexpected errors occur; that's the reason for the finally 
 129             if (mFailedResultsCounter 
> 0 && mIsManualSync
) { 
 130                 /// don't let the system synchronization manager retries MANUAL synchronizations 
 131                 //      (be careful: "MANUAL" currently includes the synchronization requested when a new account is created and when the user changes the current account) 
 132                 mSyncResult
.tooManyRetries 
= true
; 
 134                 /// notify the user about the failure of MANUAL synchronization 
 135                 notifyFailedSynchronization(); 
 138             if (mConflictsFound 
> 0 || mFailsInFavouritesFound 
> 0) { 
 139                 notifyFailsInFavourites(); 
 141             if (mForgottenLocalFiles
.size() > 0) { 
 142                 notifyForgottenLocalFiles(); 
 145             sendStickyBroadcast(false
, null
, mLastFailedResult
);        // message to signal the end to the UI 
 152      * Called by system SyncManager when a synchronization is required to be cancelled. 
 154      * Sets the mCancellation flag to 'true'. THe synchronization will be stopped when before a new folder is fetched. Data of the last folder 
 155      * fetched will be still saved in the database. See onPerformSync implementation. 
 158     public void onSyncCanceled() { 
 159         Log
.d(TAG
, "Synchronization of " + getAccount().name 
+ " has been requested to cancel"); 
 160         mCancellation 
= true
; 
 161         super.onSyncCanceled(); 
 166      * Updates the locally stored version value of the ownCloud server 
 168     private void updateOCVersion() { 
 169         UpdateOCVersionOperation update 
= new UpdateOCVersionOperation(getAccount(), getContext()); 
 170         RemoteOperationResult result 
= update
.execute(getClient()); 
 171         if (!result
.isSuccess()) { 
 172             mLastFailedResult 
= result
;  
 179      * Synchronize the properties of files and folders contained in a remote folder given by remotePath. 
 181      * @param remotePath        Remote path to the folder to synchronize. 
 182      * @param parentId          Database Id of the folder to synchronize. 
 184     private void fetchData(String remotePath
, long parentId
) { 
 186         if (mFailedResultsCounter 
> MAX_FAILED_RESULTS 
|| isFinisher(mLastFailedResult
)) 
 189         // perform folder synchronization 
 190         SynchronizeFolderOperation synchFolderOp 
= new SynchronizeFolderOperation(  remotePath
,  
 197         RemoteOperationResult result 
= synchFolderOp
.execute(getClient()); 
 200         // synchronized folder -> notice to UI - ALWAYS, although !result.isSuccess 
 201         sendStickyBroadcast(true
, remotePath
, null
); 
 203         if (result
.isSuccess() || result
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 205             if (result
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 206                 mConflictsFound 
+= synchFolderOp
.getConflictsFound(); 
 207                 mFailsInFavouritesFound 
+= synchFolderOp
.getFailsInFavouritesFound(); 
 209             if (synchFolderOp
.getForgottenLocalFiles().size() > 0) { 
 210                 mForgottenLocalFiles
.putAll(synchFolderOp
.getForgottenLocalFiles()); 
 212             // synchronize children folders  
 213             List
<OCFile
> children 
= synchFolderOp
.getChildren(); 
 214             fetchChildren(children
);    // beware of the 'hidden' recursion here! 
 217             if (result
.getCode() == RemoteOperationResult
.ResultCode
.UNAUTHORIZED
) { 
 218                 mSyncResult
.stats
.numAuthExceptions
++; 
 220             } else if (result
.getException() instanceof DavException
) { 
 221                 mSyncResult
.stats
.numParseExceptions
++; 
 223             } else if (result
.getException() instanceof IOException
) {  
 224                 mSyncResult
.stats
.numIoExceptions
++; 
 226             mFailedResultsCounter
++; 
 227             mLastFailedResult 
= result
; 
 233      * Checks if a failed result should terminate the synchronization process immediately, according to 
 236      * @param   failedResult        Remote operation result to check. 
 237      * @return                      'True' if the result should immediately finish the synchronization 
 239     private boolean isFinisher(RemoteOperationResult failedResult
) { 
 240         if  (failedResult 
!= null
) { 
 241             RemoteOperationResult
.ResultCode code 
= failedResult
.getCode(); 
 242             return (code
.equals(RemoteOperationResult
.ResultCode
.SSL_ERROR
) || 
 243                     code
.equals(RemoteOperationResult
.ResultCode
.SSL_RECOVERABLE_PEER_UNVERIFIED
) || 
 244                     code
.equals(RemoteOperationResult
.ResultCode
.BAD_OC_VERSION
) || 
 245                     code
.equals(RemoteOperationResult
.ResultCode
.INSTANCE_NOT_CONFIGURED
)); 
 251      * Synchronize data of folders in the list of received files 
 253      * @param files         Files to recursively fetch  
 255     private void fetchChildren(List
<OCFile
> files
) { 
 257         for (i
=0; i 
< files
.size() && !mCancellation
; i
++) { 
 258             OCFile newFile 
= files
.get(i
); 
 259             if (newFile
.isDirectory()) { 
 260                 fetchData(newFile
.getRemotePath(), newFile
.getFileId()); 
 263         if (mCancellation 
&& i 
<files
.size()) Log
.d(TAG
, "Leaving synchronization before synchronizing " + files
.get(i
).getRemotePath() + " because cancelation request"); 
 268      * Sends a message to any application component interested in the progress of the synchronization. 
 270      * @param inProgress        'True' when the synchronization progress is not finished. 
 271      * @param dirRemotePath     Remote path of a folder that was just synchronized (with or without success) 
 273     private void sendStickyBroadcast(boolean inProgress
, String dirRemotePath
, RemoteOperationResult result
) { 
 274         Intent i 
= new Intent(FileSyncService
.SYNC_MESSAGE
); 
 275         i
.putExtra(FileSyncService
.IN_PROGRESS
, inProgress
); 
 276         i
.putExtra(FileSyncService
.ACCOUNT_NAME
, getAccount().name
); 
 277         if (dirRemotePath 
!= null
) { 
 278             i
.putExtra(FileSyncService
.SYNC_FOLDER_REMOTE_PATH
, dirRemotePath
); 
 280         if (result 
!= null
) { 
 281             i
.putExtra(FileSyncService
.SYNC_RESULT
, result
); 
 283         getContext().sendStickyBroadcast(i
); 
 289      * Notifies the user about a failed synchronization through the status notification bar  
 291     private void notifyFailedSynchronization() { 
 292         Notification notification 
= new Notification(R
.drawable
.icon
, getContext().getString(R
.string
.sync_fail_ticker
), System
.currentTimeMillis()); 
 293         notification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 294         // TODO put something smart in the contentIntent below 
 295         notification
.contentIntent 
= PendingIntent
.getActivity(getContext().getApplicationContext(), (int)System
.currentTimeMillis(), new Intent(), 0); 
 296         notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 297                                         getContext().getString(R
.string
.sync_fail_ticker
),  
 298                                         String
.format(getContext().getString(R
.string
.sync_fail_content
), getAccount().name
),  
 299                                         notification
.contentIntent
); 
 300         ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)).notify(R
.string
.sync_fail_ticker
, notification
); 
 305      * Notifies the user about conflicts and strange fails when trying to synchronize the contents of kept-in-sync files. 
 307      * By now, we won't consider a failed synchronization. 
 309     private void notifyFailsInFavourites() { 
 310         if (mFailedResultsCounter 
> 0) { 
 311             Notification notification 
= new Notification(R
.drawable
.icon
, getContext().getString(R
.string
.sync_fail_in_favourites_ticker
), System
.currentTimeMillis()); 
 312             notification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 313             // TODO put something smart in the contentIntent below 
 314             notification
.contentIntent 
= PendingIntent
.getActivity(getContext().getApplicationContext(), (int)System
.currentTimeMillis(), new Intent(), 0); 
 315             notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 316                                             getContext().getString(R
.string
.sync_fail_in_favourites_ticker
),  
 317                                             String
.format(getContext().getString(R
.string
.sync_fail_in_favourites_content
), mFailedResultsCounter 
+ mConflictsFound
, mConflictsFound
),  
 318                                             notification
.contentIntent
); 
 319             ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)).notify(R
.string
.sync_fail_in_favourites_ticker
, notification
); 
 322             Notification notification 
= new Notification(R
.drawable
.icon
, getContext().getString(R
.string
.sync_conflicts_in_favourites_ticker
), System
.currentTimeMillis()); 
 323             notification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 324             // TODO put something smart in the contentIntent below 
 325             notification
.contentIntent 
= PendingIntent
.getActivity(getContext().getApplicationContext(), (int)System
.currentTimeMillis(), new Intent(), 0); 
 326             notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 327                                             getContext().getString(R
.string
.sync_conflicts_in_favourites_ticker
),  
 328                                             String
.format(getContext().getString(R
.string
.sync_conflicts_in_favourites_content
), mConflictsFound
),  
 329                                             notification
.contentIntent
); 
 330             ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)).notify(R
.string
.sync_conflicts_in_favourites_ticker
, notification
); 
 336      * Notifies the user about local copies of files out of the ownCloud local directory that were 'forgotten' because  
 337      * copying them inside the ownCloud local directory was not possible. 
 339      * We don't want links to files out of the ownCloud local directory (foreign files) anymore. It's easy to have  
 340      * synchronization problems if a local file is linked to more than one remote file. 
 342      * We won't consider a synchronization as failed when foreign files can not be copied to the ownCloud local directory. 
 344     private void notifyForgottenLocalFiles() { 
 345         Notification notification 
= new Notification(R
.drawable
.icon
, getContext().getString(R
.string
.sync_foreign_files_forgotten_ticker
), System
.currentTimeMillis()); 
 346         notification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 348         /// includes a pending intent in the notification showing a more detailed explanation 
 349         Intent explanationIntent 
= new Intent(getContext(), ErrorsWhileCopyingHandlerActivity
.class); 
 350         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_ACCOUNT
, getAccount()); 
 351         ArrayList
<String
> remotePaths 
= new ArrayList
<String
>(); 
 352         ArrayList
<String
> localPaths 
= new ArrayList
<String
>(); 
 353         remotePaths
.addAll(mForgottenLocalFiles
.keySet()); 
 354         localPaths
.addAll(mForgottenLocalFiles
.values()); 
 355         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_LOCAL_PATHS
, localPaths
); 
 356         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_REMOTE_PATHS
, remotePaths
);   
 357         explanationIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 359         notification
.contentIntent 
= PendingIntent
.getActivity(getContext().getApplicationContext(), (int)System
.currentTimeMillis(), explanationIntent
, 0); 
 360         notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 361                                         getContext().getString(R
.string
.sync_foreign_files_forgotten_ticker
),  
 362                                         String
.format(getContext().getString(R
.string
.sync_foreign_files_forgotten_content
), mForgottenLocalFiles
.size()),  
 363                                         notification
.contentIntent
); 
 364         ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)).notify(R
.string
.sync_foreign_files_forgotten_ticker
, notification
);