2  *   ownCloud Android client application 
   4  *   @author Bartek Przybylski 
   5  *   @author David A. Velasco 
   6  *   Copyright (C) 2011  Bartek Przybylski 
   7  *   Copyright (C) 2015 ownCloud Inc. 
   9  *   This program is free software: you can redistribute it and/or modify 
  10  *   it under the terms of the GNU General Public License version 2, 
  11  *   as published by the Free Software Foundation. 
  13  *   This program is distributed in the hope that it will be useful, 
  14  *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
  15  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  16  *   GNU General Public License for more details. 
  18  *   You should have received a copy of the GNU General Public License 
  19  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. 
  23 package com
.owncloud
.android
.syncadapter
; 
  25 import java
.io
.IOException
; 
  26 import java
.util
.ArrayList
; 
  27 import java
.util
.HashMap
; 
  28 import java
.util
.List
; 
  31 import org
.apache
.jackrabbit
.webdav
.DavException
; 
  33 import com
.owncloud
.android
.MainApp
; 
  34 import com
.owncloud
.android
.R
; 
  35 import com
.owncloud
.android
.authentication
.AuthenticatorActivity
; 
  36 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  37 import com
.owncloud
.android
.datamodel
.OCFile
; 
  38 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
; 
  39 import com
.owncloud
.android
.operations
.RefreshFolderOperation
; 
  40 import com
.owncloud
.android
.operations
.UpdateOCVersionOperation
; 
  41 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
.ResultCode
; 
  42 import com
.owncloud
.android
.lib
.common
.utils
.Log_OC
; 
  43 import com
.owncloud
.android
.ui
.activity
.ErrorsWhileCopyingHandlerActivity
; 
  45 import android
.accounts
.Account
; 
  46 import android
.accounts
.AccountsException
; 
  47 import android
.app
.NotificationManager
; 
  48 import android
.app
.PendingIntent
; 
  49 import android
.content
.AbstractThreadedSyncAdapter
; 
  50 import android
.content
.ContentProviderClient
; 
  51 import android
.content
.ContentResolver
; 
  52 import android
.content
.Context
; 
  53 import android
.content
.Intent
; 
  54 import android
.content
.SyncResult
; 
  55 import android
.os
.Bundle
; 
  56 import android
.support
.v4
.app
.NotificationCompat
; 
  59  * Implementation of {@link AbstractThreadedSyncAdapter} responsible for synchronizing  
  62  * Performs a full synchronization of the account recieved in {@link #onPerformSync(Account, Bundle, 
  63  * String, ContentProviderClient, SyncResult)}. 
  65 public class FileSyncAdapter 
extends AbstractOwnCloudSyncAdapter 
{ 
  67     private final static String TAG 
= FileSyncAdapter
.class.getSimpleName(); 
  69     /** Maximum number of failed folder synchronizations that are supported before finishing 
  70      * the synchronization operation */ 
  71     private static final int MAX_FAILED_RESULTS 
= 3;  
  74     public static final String EVENT_FULL_SYNC_START 
= FileSyncAdapter
.class.getName() + 
  75             ".EVENT_FULL_SYNC_START"; 
  76     public static final String EVENT_FULL_SYNC_END 
= FileSyncAdapter
.class.getName() + 
  77             ".EVENT_FULL_SYNC_END"; 
  78     public static final String EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED 
= 
  79             FileSyncAdapter
.class.getName() + ".EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED"; 
  80     //public static final String EVENT_FULL_SYNC_FOLDER_SIZE_SYNCED = 
  81     // FileSyncAdapter.class.getName() + ".EVENT_FULL_SYNC_FOLDER_SIZE_SYNCED"; 
  83     public static final String EXTRA_ACCOUNT_NAME 
= FileSyncAdapter
.class.getName() + 
  84             ".EXTRA_ACCOUNT_NAME"; 
  85     public static final String EXTRA_FOLDER_PATH 
= FileSyncAdapter
.class.getName() + 
  87     public static final String EXTRA_RESULT 
= FileSyncAdapter
.class.getName() + ".EXTRA_RESULT"; 
  90     /** Time stamp for the current synchronization process, used to distinguish fresh data */ 
  91     private long mCurrentSyncTime
; 
  93     /** Flag made 'true' when a request to cancel the synchronization is received */ 
  94     private boolean mCancellation
; 
  96     /** When 'true' the process was requested by the user through the user interface; 
  97      *  when 'false', it was requested automatically by the system */ 
  98     private boolean mIsManualSync
; 
 100     /** Counter for failed operations in the synchronization process */ 
 101     private int mFailedResultsCounter
; 
 103     /** Result of the last failed operation */ 
 104     private RemoteOperationResult mLastFailedResult
; 
 106     /** Counter of conflicts found between local and remote files */ 
 107     private int mConflictsFound
; 
 109     /** Counter of failed operations in synchronization of kept-in-sync files */ 
 110     private int mFailsInFavouritesFound
; 
 112     /** Map of remote and local paths to files that where locally stored in a location out 
 113      * of the ownCloud folder and couldn't be copied automatically into it */ 
 114     private Map
<String
, String
> mForgottenLocalFiles
; 
 116     /** {@link SyncResult} instance to return to the system when the synchronization finish */ 
 117     private SyncResult mSyncResult
; 
 119     /** 'True' means that the server supports the share API */ 
 120     private boolean mIsShareSupported
; 
 124      * Creates a {@link FileSyncAdapter} 
 128     public FileSyncAdapter(Context context
, boolean autoInitialize
) { 
 129         super(context
, autoInitialize
); 
 134      * Creates a {@link FileSyncAdapter} 
 138     public FileSyncAdapter(Context context
, boolean autoInitialize
, boolean allowParallelSyncs
) { 
 139         super(context
, autoInitialize
, allowParallelSyncs
); 
 147     public synchronized void onPerformSync(Account account
, Bundle extras
, 
 148             String authority
, ContentProviderClient providerClient
, 
 149             SyncResult syncResult
) { 
 151         mCancellation 
= false
; 
 152         mIsManualSync 
= extras
.getBoolean(ContentResolver
.SYNC_EXTRAS_MANUAL
, false
); 
 153         mFailedResultsCounter 
= 0; 
 154         mLastFailedResult 
= null
; 
 156         mFailsInFavouritesFound 
= 0; 
 157         mForgottenLocalFiles 
= new HashMap
<String
, String
>(); 
 158         mSyncResult 
= syncResult
; 
 159         mSyncResult
.fullSyncRequested 
= false
; 
 160         mSyncResult
.delayUntil 
= 60*60*24; // avoid too many automatic synchronizations 
 162         this.setAccount(account
); 
 163         this.setContentProviderClient(providerClient
); 
 164         this.setStorageManager(new FileDataStorageManager(account
, providerClient
)); 
 167             this.initClientForCurrentAccount(); 
 168         } catch (IOException e
) { 
 169             /// the account is unknown for the Synchronization Manager, unreachable this context, 
 170             // or can not be authenticated; don't try this again 
 171             mSyncResult
.tooManyRetries 
= true
; 
 172             notifyFailedSynchronization(); 
 174         } catch (AccountsException e
) { 
 175             /// the account is unknown for the Synchronization Manager, unreachable this context, 
 176             // or can not be authenticated; don't try this again 
 177             mSyncResult
.tooManyRetries 
= true
; 
 178             notifyFailedSynchronization(); 
 182         Log_OC
.d(TAG
, "Synchronization of ownCloud account " + account
.name 
+ " starting"); 
 183         sendLocalBroadcast(EVENT_FULL_SYNC_START
, null
, null
);  // message to signal the start 
 184                                                                 // of the synchronization to the UI 
 188             mCurrentSyncTime 
= System
.currentTimeMillis(); 
 189             if (!mCancellation
) { 
 190                 synchronizeFolder(getStorageManager().getFileByPath(OCFile
.ROOT_PATH
)); 
 193                 Log_OC
.d(TAG
, "Leaving synchronization before synchronizing the root folder " + 
 194                         "because cancelation request"); 
 199             // it's important making this although very unexpected errors occur; 
 200             // that's the reason for the finally 
 202             if (mFailedResultsCounter 
> 0 && mIsManualSync
) { 
 203                 /// don't let the system synchronization manager retries MANUAL synchronizations 
 204                 //      (be careful: "MANUAL" currently includes the synchronization requested when 
 205                 //      a new account is created and when the user changes the current account) 
 206                 mSyncResult
.tooManyRetries 
= true
; 
 208                 /// notify the user about the failure of MANUAL synchronization 
 209                 notifyFailedSynchronization(); 
 211             if (mConflictsFound 
> 0 || mFailsInFavouritesFound 
> 0) { 
 212                 notifyFailsInFavourites(); 
 214             if (mForgottenLocalFiles
.size() > 0) { 
 215                 notifyForgottenLocalFiles(); 
 217             sendLocalBroadcast(EVENT_FULL_SYNC_END
, null
, mLastFailedResult
);   // message to signal 
 224      * Called by system SyncManager when a synchronization is required to be cancelled. 
 226      * Sets the mCancellation flag to 'true'. THe synchronization will be stopped later,  
 227      * before a new folder is fetched. Data of the last folder synchronized will be still  
 230      * See {@link #onPerformSync(Account, Bundle, String, ContentProviderClient, SyncResult)} 
 231      * and {@link #synchronizeFolder(String, long)}. 
 234     public void onSyncCanceled() { 
 235         Log_OC
.d(TAG
, "Synchronization of " + getAccount().name 
+ " has been requested to cancel"); 
 236         mCancellation 
= true
; 
 237         super.onSyncCanceled(); 
 242      * Updates the locally stored version value of the ownCloud server 
 244     private void updateOCVersion() { 
 245         UpdateOCVersionOperation update 
= new UpdateOCVersionOperation(getAccount(), getContext()); 
 246         RemoteOperationResult result 
= update
.execute(getClient(), MainApp
.getUserAgent()); 
 247         if (!result
.isSuccess()) { 
 248             mLastFailedResult 
= result
;  
 250             mIsShareSupported 
= update
.getOCVersion().isSharedSupported(); 
 256      *  Synchronizes the list of files contained in a folder identified with its remote path. 
 258      *  Fetches the list and properties of the files contained in the given folder, including their  
 259      *  properties, and updates the local database with them. 
 261      *  Enters in the child folders to synchronize their contents also, following a recursive 
 262      *  depth first strategy.  
 264      *  @param folder                   Folder to synchronize. 
 266     private void synchronizeFolder(OCFile folder
) { 
 268         if (mFailedResultsCounter 
> MAX_FAILED_RESULTS 
|| isFinisher(mLastFailedResult
)) 
 273         long currentSyncTime,  
 274         boolean updateFolderProperties, 
 275         boolean syncFullAccount, 
 276         DataStorageManager dataStorageManager,  
 281         // folder synchronization 
 282         RefreshFolderOperation synchFolderOp 
= new RefreshFolderOperation( folder
, 
 291         RemoteOperationResult result 
= synchFolderOp
.execute(getClient(), MainApp
.getUserAgent()); 
 294         // synchronized folder -> notice to UI - ALWAYS, although !result.isSuccess 
 295         sendLocalBroadcast(EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED
, folder
.getRemotePath(), result
); 
 297         // check the result of synchronizing the folder 
 298         if (result
.isSuccess() || result
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 300             if (result
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 301                 mConflictsFound 
+= synchFolderOp
.getConflictsFound(); 
 302                 mFailsInFavouritesFound 
+= synchFolderOp
.getFailsInFavouritesFound(); 
 304             if (synchFolderOp
.getForgottenLocalFiles().size() > 0) { 
 305                 mForgottenLocalFiles
.putAll(synchFolderOp
.getForgottenLocalFiles()); 
 307             if (result
.isSuccess()) { 
 308                 // synchronize children folders  
 309                 List
<OCFile
> children 
= synchFolderOp
.getChildren(); 
 310                 // beware of the 'hidden' recursion here! 
 311                 fetchChildren(folder
, children
, synchFolderOp
.getRemoteFolderChanged()); 
 315             // in failures, the statistics for the global result are updated 
 316             if (    result
.getCode() == RemoteOperationResult
.ResultCode
.UNAUTHORIZED 
|| 
 317                     result
.isIdPRedirection() 
 319                 mSyncResult
.stats
.numAuthExceptions
++; 
 321             } else if (result
.getException() instanceof DavException
) { 
 322                 mSyncResult
.stats
.numParseExceptions
++; 
 324             } else if (result
.getException() instanceof IOException
) {  
 325                 mSyncResult
.stats
.numIoExceptions
++; 
 327             mFailedResultsCounter
++; 
 328             mLastFailedResult 
= result
; 
 334      * Checks if a failed result should terminate the synchronization process immediately, 
 335      * according to OUR OWN POLICY 
 337      * @param   failedResult        Remote operation result to check. 
 338      * @return                      'True' if the result should immediately finish the 
 341     private boolean isFinisher(RemoteOperationResult failedResult
) { 
 342         if  (failedResult 
!= null
) { 
 343             RemoteOperationResult
.ResultCode code 
= failedResult
.getCode(); 
 344             return (code
.equals(RemoteOperationResult
.ResultCode
.SSL_ERROR
) || 
 345                     code
.equals(RemoteOperationResult
.ResultCode
.SSL_RECOVERABLE_PEER_UNVERIFIED
) || 
 346                     code
.equals(RemoteOperationResult
.ResultCode
.BAD_OC_VERSION
) || 
 347                     code
.equals(RemoteOperationResult
.ResultCode
.INSTANCE_NOT_CONFIGURED
)); 
 353      * Triggers the synchronization of any folder contained in the list of received files. 
 355      * @param files         Files to recursively synchronize. 
 357     private void fetchChildren(OCFile parent
, List
<OCFile
> files
, boolean parentEtagChanged
) { 
 359         OCFile newFile 
= null
; 
 360         //String etag = null; 
 361         //boolean syncDown = false; 
 362         for (i
=0; i 
< files
.size() && !mCancellation
; i
++) { 
 363             newFile 
= files
.get(i
); 
 364             if (newFile
.isFolder()) { 
 366                 etag = newFile.getEtag(); 
 367                 syncDown = (parentEtagChanged || etag == null || etag.length() == 0); 
 369                     synchronizeFolder(newFile
); 
 370                     //sendLocalBroadcast(EVENT_FULL_SYNC_FOLDER_SIZE_SYNCED, parent.getRemotePath(), 
 376         if (mCancellation 
&& i 
<files
.size()) Log_OC
.d(TAG
, 
 377                 "Leaving synchronization before synchronizing " + files
.get(i
).getRemotePath() + 
 378                         " due to cancelation request"); 
 383      * Sends a message to any application component interested in the progress of the 
 386      * @param event             Event in the process of synchronization to be notified.    
 387      * @param dirRemotePath     Remote path of the folder target of the event occurred. 
 388      * @param result            Result of an individual {@ SynchronizeFolderOperation}, 
 389      *                          if completed; may be null. 
 391     private void sendLocalBroadcast(String event
, String dirRemotePath
, 
 392                                     RemoteOperationResult result
) { 
 393         Log_OC
.d(TAG
, "Send broadcast " + event
); 
 394         Intent intent 
= new Intent(event
); 
 395         intent
.putExtra(FileSyncAdapter
.EXTRA_ACCOUNT_NAME
, getAccount().name
); 
 396         if (dirRemotePath 
!= null
) { 
 397             intent
.putExtra(FileSyncAdapter
.EXTRA_FOLDER_PATH
, dirRemotePath
); 
 399         if (result 
!= null
) { 
 400             intent
.putExtra(FileSyncAdapter
.EXTRA_RESULT
, result
); 
 402         getContext().sendStickyBroadcast(intent
); 
 403         //LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); 
 409      * Notifies the user about a failed synchronization through the status notification bar  
 411     private void notifyFailedSynchronization() { 
 412         NotificationCompat
.Builder notificationBuilder 
= createNotificationBuilder(); 
 413         boolean needsToUpdateCredentials 
= ( 
 414                 mLastFailedResult 
!= null 
&& (   
 415                         mLastFailedResult
.getCode() == ResultCode
.UNAUTHORIZED 
|| 
 416                         mLastFailedResult
.isIdPRedirection() 
 419         if (needsToUpdateCredentials
) { 
 420             // let the user update credentials with one click 
 421             Intent updateAccountCredentials 
= new Intent(getContext(), AuthenticatorActivity
.class); 
 422             updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACCOUNT
, getAccount()); 
 423             updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACTION
, 
 424                     AuthenticatorActivity
.ACTION_UPDATE_EXPIRED_TOKEN
); 
 425             updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
); 
 426             updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
); 
 427             updateAccountCredentials
.addFlags(Intent
.FLAG_FROM_BACKGROUND
); 
 429                 .setTicker(i18n(R
.string
.sync_fail_ticker_unauthorized
)) 
 430                 .setContentTitle(i18n(R
.string
.sync_fail_ticker_unauthorized
)) 
 431                 .setContentIntent(PendingIntent
.getActivity( 
 432                     getContext(), (int)System
.currentTimeMillis(), updateAccountCredentials
, 
 433                         PendingIntent
.FLAG_ONE_SHOT
 
 435                 .setContentText(i18n(R
.string
.sync_fail_content_unauthorized
, getAccount().name
)); 
 438                 .setTicker(i18n(R
.string
.sync_fail_ticker
)) 
 439                 .setContentTitle(i18n(R
.string
.sync_fail_ticker
)) 
 440                 .setContentText(i18n(R
.string
.sync_fail_content
, getAccount().name
)); 
 443         showNotification(R
.string
.sync_fail_ticker
, notificationBuilder
); 
 448      * Notifies the user about conflicts and strange fails when trying to synchronize the contents 
 449      * of kept-in-sync files. 
 451      * By now, we won't consider a failed synchronization. 
 453     private void notifyFailsInFavourites() { 
 454         if (mFailedResultsCounter 
> 0) { 
 455             NotificationCompat
.Builder notificationBuilder 
= createNotificationBuilder(); 
 456             notificationBuilder
.setTicker(i18n(R
.string
.sync_fail_in_favourites_ticker
)); 
 458             // TODO put something smart in the contentIntent below 
 460                 .setContentIntent(PendingIntent
.getActivity( 
 461                     getContext(), (int) System
.currentTimeMillis(), new Intent(), 0 
 463                 .setContentTitle(i18n(R
.string
.sync_fail_in_favourites_ticker
)) 
 464                 .setContentText(i18n(R
.string
.sync_fail_in_favourites_content
, 
 465                         mFailedResultsCounter 
+ mConflictsFound
, mConflictsFound
)); 
 467             showNotification(R
.string
.sync_fail_in_favourites_ticker
, notificationBuilder
); 
 469             NotificationCompat
.Builder notificationBuilder 
= createNotificationBuilder(); 
 470             notificationBuilder
.setTicker(i18n(R
.string
.sync_conflicts_in_favourites_ticker
)); 
 472             // TODO put something smart in the contentIntent below 
 474                 .setContentIntent(PendingIntent
.getActivity( 
 475                     getContext(), (int) System
.currentTimeMillis(), new Intent(), 0 
 477                 .setContentTitle(i18n(R
.string
.sync_conflicts_in_favourites_ticker
)) 
 478                 .setContentText(i18n(R
.string
.sync_conflicts_in_favourites_ticker
, mConflictsFound
)); 
 480             showNotification(R
.string
.sync_conflicts_in_favourites_ticker
, notificationBuilder
); 
 485      * Notifies the user about local copies of files out of the ownCloud local directory that 
 486      * were 'forgotten' because copying them inside the ownCloud local directory was not possible. 
 488      * We don't want links to files out of the ownCloud local directory (foreign files) anymore. 
 489      * It's easy to have synchronization problems if a local file is linked to more than one 
 492      * We won't consider a synchronization as failed when foreign files can not be copied to 
 493      * the ownCloud local directory. 
 495     private void notifyForgottenLocalFiles() { 
 496         NotificationCompat
.Builder notificationBuilder 
= createNotificationBuilder(); 
 497         notificationBuilder
.setTicker(i18n(R
.string
.sync_foreign_files_forgotten_ticker
)); 
 499         /// includes a pending intent in the notification showing a more detailed explanation 
 500         Intent explanationIntent 
= new Intent(getContext(), ErrorsWhileCopyingHandlerActivity
.class); 
 501         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_ACCOUNT
, getAccount()); 
 502         ArrayList
<String
> remotePaths 
= new ArrayList
<String
>(); 
 503         ArrayList
<String
> localPaths 
= new ArrayList
<String
>(); 
 504         remotePaths
.addAll(mForgottenLocalFiles
.keySet()); 
 505         localPaths
.addAll(mForgottenLocalFiles
.values()); 
 506         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_LOCAL_PATHS
, localPaths
); 
 507         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_REMOTE_PATHS
, remotePaths
);   
 508         explanationIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 511             .setContentIntent(PendingIntent
.getActivity( 
 512                 getContext(), (int) System
.currentTimeMillis(), explanationIntent
, 0 
 514             .setContentTitle(i18n(R
.string
.sync_foreign_files_forgotten_ticker
)) 
 515             .setContentText(i18n(R
.string
.sync_foreign_files_forgotten_content
, 
 516                     mForgottenLocalFiles
.size(), i18n(R
.string
.app_name
))); 
 518         showNotification(R
.string
.sync_foreign_files_forgotten_ticker
, notificationBuilder
); 
 522      * Creates a notification builder with some commonly used settings 
 526     private NotificationCompat
.Builder 
createNotificationBuilder() { 
 527         NotificationCompat
.Builder notificationBuilder 
= new NotificationCompat
.Builder(getContext()); 
 528         notificationBuilder
.setSmallIcon(R
.drawable
.notification_icon
).setAutoCancel(true
); 
 529         return notificationBuilder
; 
 533      * Builds and shows the notification 
 538     private void showNotification(int id
, NotificationCompat
.Builder builder
) { 
 539         ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)) 
 540             .notify(id
, builder
.build()); 
 543      * Shorthand translation 
 549     private String 
i18n(int key
, Object
... args
) { 
 550         return getContext().getString(key
, args
);