1 /* ownCloud Android client application 
   2  *   Copyright (C) 2011  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 as published by 
   7  *   the Free Software Foundation, either version 2 of the License, or 
   8  *   (at your option) any later version. 
  10  *   This program is distributed in the hope that it will be useful, 
  11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
  12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  13  *   GNU General Public License for more details. 
  15  *   You should have received a copy of the GNU General Public License 
  16  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. 
  20 package com
.owncloud
.android
.syncadapter
; 
  22 import java
.io
.IOException
; 
  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
.AuthenticatorActivity
; 
  39 import com
.owncloud
.android
.ui
.activity
.ErrorsWhileCopyingHandlerActivity
; 
  41 import android
.accounts
.Account
; 
  42 import android
.accounts
.AccountsException
; 
  43 import android
.app
.Notification
; 
  44 import android
.app
.NotificationManager
; 
  45 import android
.app
.PendingIntent
; 
  46 import android
.content
.ContentProviderClient
; 
  47 import android
.content
.ContentResolver
; 
  48 import android
.content
.Context
; 
  49 import android
.content
.Intent
; 
  50 import android
.content
.SyncResult
; 
  51 import android
.os
.Bundle
; 
  52 import android
.util
.Log
; 
  55  * SyncAdapter implementation for syncing sample SyncAdapter contacts to the 
  56  * platform ContactOperations provider. 
  58  * @author Bartek Przybylski 
  60 public class FileSyncAdapter 
extends AbstractOwnCloudSyncAdapter 
{ 
  62     private final static String TAG 
= "FileSyncAdapter"; 
  65      * Maximum number of failed folder synchronizations that are supported before finishing the synchronization operation 
  67     private static final int MAX_FAILED_RESULTS 
= 3;  
  69     private long mCurrentSyncTime
; 
  70     private boolean mCancellation
; 
  71     private boolean mIsManualSync
; 
  72     private int mFailedResultsCounter
;     
  73     private RemoteOperationResult mLastFailedResult
; 
  74     private SyncResult mSyncResult
; 
  75     private int mConflictsFound
; 
  76     private int mFailsInFavouritesFound
; 
  77     private Map
<String
, String
> mForgottenLocalFiles
; 
  80     public FileSyncAdapter(Context context
, boolean autoInitialize
) { 
  81         super(context
, autoInitialize
); 
  88     public synchronized void onPerformSync(Account account
, Bundle extras
, 
  89             String authority
, ContentProviderClient provider
, 
  90             SyncResult syncResult
) { 
  92         mCancellation 
= false
; 
  93         mIsManualSync 
= extras
.getBoolean(ContentResolver
.SYNC_EXTRAS_MANUAL
, false
); 
  94         mFailedResultsCounter 
= 0; 
  95         mLastFailedResult 
= null
; 
  97         mFailsInFavouritesFound 
= 0; 
  98         mForgottenLocalFiles 
= new HashMap
<String
, String
>(); 
  99         mSyncResult 
= syncResult
; 
 100         mSyncResult
.fullSyncRequested 
= false
; 
 101         mSyncResult
.delayUntil 
= 60*60*24; // sync after 24h 
 103         this.setAccount(account
); 
 104         this.setContentProvider(provider
); 
 105         this.setStorageManager(new FileDataStorageManager(account
, getContentProvider())); 
 107             this.initClientForCurrentAccount(); 
 108         } catch (IOException e
) { 
 109             /// the account is unknown for the Synchronization Manager, or unreachable for this context; don't try this again 
 110             mSyncResult
.tooManyRetries 
= true
; 
 111             notifyFailedSynchronization(); 
 113         } catch (AccountsException e
) { 
 114             /// the account is unknown for the Synchronization Manager, or unreachable for this context; don't try this again 
 115             mSyncResult
.tooManyRetries 
= true
; 
 116             notifyFailedSynchronization(); 
 120         Log
.d(TAG
, "Synchronization of ownCloud account " + account
.name 
+ " starting"); 
 121         sendStickyBroadcast(true
, null
, null
);  // message to signal the start of the synchronization to the UI 
 125             mCurrentSyncTime 
= System
.currentTimeMillis(); 
 126             if (!mCancellation
) { 
 127                 fetchData(OCFile
.PATH_SEPARATOR
, DataStorageManager
.ROOT_PARENT_ID
); 
 130                 Log
.d(TAG
, "Leaving synchronization before any remote request due to cancellation was requested"); 
 135             // it's important making this although very unexpected errors occur; that's the reason for the finally 
 137             if (mFailedResultsCounter 
> 0 && mIsManualSync
) { 
 138                 /// don't let the system synchronization manager retries MANUAL synchronizations 
 139                 //      (be careful: "MANUAL" currently includes the synchronization requested when a new account is created and when the user changes the current account) 
 140                 mSyncResult
.tooManyRetries 
= true
; 
 142                 /// notify the user about the failure of MANUAL synchronization 
 143                 notifyFailedSynchronization(); 
 146             if (mConflictsFound 
> 0 || mFailsInFavouritesFound 
> 0) { 
 147                 notifyFailsInFavourites(); 
 149             if (mForgottenLocalFiles
.size() > 0) { 
 150                 notifyForgottenLocalFiles(); 
 153             sendStickyBroadcast(false
, null
, mLastFailedResult
);        // message to signal the end to the UI 
 160      * Called by system SyncManager when a synchronization is required to be cancelled. 
 162      * Sets the mCancellation flag to 'true'. THe synchronization will be stopped when before a new folder is fetched. Data of the last folder 
 163      * fetched will be still saved in the database. See onPerformSync implementation. 
 166     public void onSyncCanceled() { 
 167         Log
.d(TAG
, "Synchronization of " + getAccount().name 
+ " has been requested to cancel"); 
 168         mCancellation 
= true
; 
 169         super.onSyncCanceled(); 
 174      * Updates the locally stored version value of the ownCloud server 
 176     private void updateOCVersion() { 
 177         UpdateOCVersionOperation update 
= new UpdateOCVersionOperation(getAccount(), getContext()); 
 178         RemoteOperationResult result 
= update
.execute(getClient()); 
 179         if (!result
.isSuccess()) { 
 180             mLastFailedResult 
= result
;  
 187      * Synchronize the properties of files and folders contained in a remote folder given by remotePath. 
 189      * @param remotePath        Remote path to the folder to synchronize. 
 190      * @param parentId          Database Id of the folder to synchronize. 
 192     private void fetchData(String remotePath
, long parentId
) { 
 194         if (mFailedResultsCounter 
> MAX_FAILED_RESULTS 
|| isFinisher(mLastFailedResult
)) 
 197         // perform folder synchronization 
 198         SynchronizeFolderOperation synchFolderOp 
= new SynchronizeFolderOperation(  remotePath
,  
 205         RemoteOperationResult result 
= synchFolderOp
.execute(getClient()); 
 208         // synchronized folder -> notice to UI - ALWAYS, although !result.isSuccess 
 209         sendStickyBroadcast(true
, remotePath
, null
); 
 211         if (result
.isSuccess() || result
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 213             if (result
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 214                 mConflictsFound 
+= synchFolderOp
.getConflictsFound(); 
 215                 mFailsInFavouritesFound 
+= synchFolderOp
.getFailsInFavouritesFound(); 
 217             if (synchFolderOp
.getForgottenLocalFiles().size() > 0) { 
 218                 mForgottenLocalFiles
.putAll(synchFolderOp
.getForgottenLocalFiles()); 
 220             // synchronize children folders  
 221             List
<OCFile
> children 
= synchFolderOp
.getChildren(); 
 222             fetchChildren(children
);    // beware of the 'hidden' recursion here! 
 225             if (result
.getCode() == RemoteOperationResult
.ResultCode
.UNAUTHORIZED
) { 
 226                 mSyncResult
.stats
.numAuthExceptions
++; 
 228             } else if (result
.getException() instanceof DavException
) { 
 229                 mSyncResult
.stats
.numParseExceptions
++; 
 231             } else if (result
.getException() instanceof IOException
) {  
 232                 mSyncResult
.stats
.numIoExceptions
++; 
 234             mFailedResultsCounter
++; 
 235             mLastFailedResult 
= result
; 
 241      * Checks if a failed result should terminate the synchronization process immediately, according to 
 244      * @param   failedResult        Remote operation result to check. 
 245      * @return                      'True' if the result should immediately finish the synchronization 
 247     private boolean isFinisher(RemoteOperationResult failedResult
) { 
 248         if  (failedResult 
!= null
) { 
 249             RemoteOperationResult
.ResultCode code 
= failedResult
.getCode(); 
 250             return (code
.equals(RemoteOperationResult
.ResultCode
.SSL_ERROR
) || 
 251                     code
.equals(RemoteOperationResult
.ResultCode
.SSL_RECOVERABLE_PEER_UNVERIFIED
) || 
 252                     code
.equals(RemoteOperationResult
.ResultCode
.UNAUTHORIZED
) || 
 253                     code
.equals(RemoteOperationResult
.ResultCode
.BAD_OC_VERSION
) || 
 254                     code
.equals(RemoteOperationResult
.ResultCode
.INSTANCE_NOT_CONFIGURED
)); 
 260      * Synchronize data of folders in the list of received files 
 262      * @param files         Files to recursively fetch  
 264     private void fetchChildren(List
<OCFile
> files
) { 
 266         for (i
=0; i 
< files
.size() && !mCancellation
; i
++) { 
 267             OCFile newFile 
= files
.get(i
); 
 268             if (newFile
.isDirectory()) { 
 269                 fetchData(newFile
.getRemotePath(), newFile
.getFileId()); 
 272         if (mCancellation 
&& i 
<files
.size()) Log
.d(TAG
, "Leaving synchronization before synchronizing " + files
.get(i
).getRemotePath() + " because cancelation request"); 
 277      * Sends a message to any application component interested in the progress of the synchronization. 
 279      * @param inProgress        'True' when the synchronization progress is not finished. 
 280      * @param dirRemotePath     Remote path of a folder that was just synchronized (with or without success) 
 282     private void sendStickyBroadcast(boolean inProgress
, String dirRemotePath
, RemoteOperationResult result
) { 
 283         Intent i 
= new Intent(FileSyncService
.SYNC_MESSAGE
); 
 284         i
.putExtra(FileSyncService
.IN_PROGRESS
, inProgress
); 
 285         i
.putExtra(FileSyncService
.ACCOUNT_NAME
, getAccount().name
); 
 286         if (dirRemotePath 
!= null
) { 
 287             i
.putExtra(FileSyncService
.SYNC_FOLDER_REMOTE_PATH
, dirRemotePath
); 
 289         if (result 
!= null
) { 
 290             i
.putExtra(FileSyncService
.SYNC_RESULT
, result
); 
 292         getContext().sendStickyBroadcast(i
); 
 298      * Notifies the user about a failed synchronization through the status notification bar  
 300     private void notifyFailedSynchronization() { 
 301         Notification notification 
= new Notification(R
.drawable
.icon
, getContext().getString(R
.string
.sync_fail_ticker
), System
.currentTimeMillis()); 
 302         notification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 303         boolean needsToUpdateCredentials 
= (mLastFailedResult 
!= null 
&& mLastFailedResult
.getCode() == ResultCode
.UNAUTHORIZED
); 
 304         // TODO put something smart in the contentIntent below for all the possible errors 
 305         notification
.contentIntent 
= PendingIntent
.getActivity(getContext().getApplicationContext(), (int)System
.currentTimeMillis(), new Intent(), 0); 
 306         if (needsToUpdateCredentials
) { 
 307             // let the user update credentials with one click 
 308             Intent updateAccountCredentials 
= new Intent(getContext(), AuthenticatorActivity
.class); 
 309             updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACCOUNT
, getAccount()); 
 310             updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACTION
, AuthenticatorActivity
.ACTION_UPDATE_TOKEN
); 
 311             updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
); 
 312             updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
); 
 313             updateAccountCredentials
.addFlags(Intent
.FLAG_FROM_BACKGROUND
); 
 314             notification
.contentIntent 
= PendingIntent
.getActivity(getContext(), (int)System
.currentTimeMillis(), updateAccountCredentials
, PendingIntent
.FLAG_ONE_SHOT
); 
 315             notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 316                     getContext().getString(R
.string
.sync_fail_ticker
),  
 317                     String
.format(getContext().getString(R
.string
.sync_fail_content_unauthorized
), getAccount().name
),  
 318                     notification
.contentIntent
); 
 319             Log
.e(TAG
, "NEEDS TO UPDATE CREDENTIALS"); 
 321             notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 322                     getContext().getString(R
.string
.sync_fail_ticker
),  
 323                     String
.format(getContext().getString(R
.string
.sync_fail_content
), getAccount().name
),  
 324                     notification
.contentIntent
); 
 326         ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)).notify(R
.string
.sync_fail_ticker
, notification
); 
 331      * Notifies the user about conflicts and strange fails when trying to synchronize the contents of kept-in-sync files. 
 333      * By now, we won't consider a failed synchronization. 
 335     private void notifyFailsInFavourites() { 
 336         if (mFailedResultsCounter 
> 0) { 
 337             Notification notification 
= new Notification(R
.drawable
.icon
, getContext().getString(R
.string
.sync_fail_in_favourites_ticker
), System
.currentTimeMillis()); 
 338             notification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 339             // TODO put something smart in the contentIntent below 
 340             notification
.contentIntent 
= PendingIntent
.getActivity(getContext().getApplicationContext(), (int)System
.currentTimeMillis(), new Intent(), 0); 
 341             notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 342                                             getContext().getString(R
.string
.sync_fail_in_favourites_ticker
),  
 343                                             String
.format(getContext().getString(R
.string
.sync_fail_in_favourites_content
), mFailedResultsCounter 
+ mConflictsFound
, mConflictsFound
),  
 344                                             notification
.contentIntent
); 
 345             ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)).notify(R
.string
.sync_fail_in_favourites_ticker
, notification
); 
 348             Notification notification 
= new Notification(R
.drawable
.icon
, getContext().getString(R
.string
.sync_conflicts_in_favourites_ticker
), System
.currentTimeMillis()); 
 349             notification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 350             // TODO put something smart in the contentIntent below 
 351             notification
.contentIntent 
= PendingIntent
.getActivity(getContext().getApplicationContext(), (int)System
.currentTimeMillis(), new Intent(), 0); 
 352             notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 353                                             getContext().getString(R
.string
.sync_conflicts_in_favourites_ticker
),  
 354                                             String
.format(getContext().getString(R
.string
.sync_conflicts_in_favourites_content
), mConflictsFound
),  
 355                                             notification
.contentIntent
); 
 356             ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)).notify(R
.string
.sync_conflicts_in_favourites_ticker
, notification
); 
 362      * Notifies the user about local copies of files out of the ownCloud local directory that were 'forgotten' because  
 363      * copying them inside the ownCloud local directory was not possible. 
 365      * We don't want links to files out of the ownCloud local directory (foreign files) anymore. It's easy to have  
 366      * synchronization problems if a local file is linked to more than one remote file. 
 368      * We won't consider a synchronization as failed when foreign files can not be copied to the ownCloud local directory. 
 370     private void notifyForgottenLocalFiles() { 
 371         Notification notification 
= new Notification(R
.drawable
.icon
, getContext().getString(R
.string
.sync_foreign_files_forgotten_ticker
), System
.currentTimeMillis()); 
 372         notification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 374         /// includes a pending intent in the notification showing a more detailed explanation 
 375         Intent explanationIntent 
= new Intent(getContext(), ErrorsWhileCopyingHandlerActivity
.class); 
 376         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_ACCOUNT
, getAccount()); 
 377         ArrayList
<String
> remotePaths 
= new ArrayList
<String
>(); 
 378         ArrayList
<String
> localPaths 
= new ArrayList
<String
>(); 
 379         remotePaths
.addAll(mForgottenLocalFiles
.keySet()); 
 380         localPaths
.addAll(mForgottenLocalFiles
.values()); 
 381         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_LOCAL_PATHS
, localPaths
); 
 382         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_REMOTE_PATHS
, remotePaths
);   
 383         explanationIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 385         notification
.contentIntent 
= PendingIntent
.getActivity(getContext().getApplicationContext(), (int)System
.currentTimeMillis(), explanationIntent
, 0); 
 386         notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 387                                         getContext().getString(R
.string
.sync_foreign_files_forgotten_ticker
),  
 388                                         String
.format(getContext().getString(R
.string
.sync_foreign_files_forgotten_content
), mForgottenLocalFiles
.size(), getContext().getString(R
.string
.app_name
)),  
 389                                         notification
.contentIntent
); 
 390         ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)).notify(R
.string
.sync_foreign_files_forgotten_ticker
, notification
);