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 version 2, 
   7  *   as published by the Free Software Foundation. 
   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
.util
.ArrayList
; 
  23 import java
.util
.HashMap
; 
  24 import java
.util
.List
; 
  27 import org
.apache
.jackrabbit
.webdav
.DavException
; 
  29 import com
.owncloud
.android
.R
; 
  30 import com
.owncloud
.android
.authentication
.AuthenticatorActivity
; 
  31 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  32 import com
.owncloud
.android
.datamodel
.OCFile
; 
  33 import com
.owncloud
.android
.lib
.common
.accounts
.AccountUtils
.Constants
; 
  34 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
; 
  35 import com
.owncloud
.android
.operations
.SynchronizeFolderOperation
; 
  36 import com
.owncloud
.android
.operations
.UpdateOCVersionOperation
; 
  37 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
.ResultCode
; 
  38 import com
.owncloud
.android
.ui
.activity
.ErrorsWhileCopyingHandlerActivity
; 
  39 import com
.owncloud
.android
.utils
.Log_OC
; 
  41 import android
.accounts
.Account
; 
  42 import android
.accounts
.AccountManager
; 
  43 import android
.accounts
.AccountsException
; 
  44 import android
.app
.NotificationManager
; 
  45 import android
.app
.PendingIntent
; 
  46 import android
.content
.AbstractThreadedSyncAdapter
; 
  47 import android
.content
.ContentProviderClient
; 
  48 import android
.content
.ContentResolver
; 
  49 import android
.content
.Context
; 
  50 import android
.content
.Intent
; 
  51 import android
.content
.SyncResult
; 
  52 import android
.os
.Bundle
; 
  53 import android
.support
.v4
.app
.NotificationCompat
; 
  56  * Implementation of {@link AbstractThreadedSyncAdapter} responsible for synchronizing  
  59  * Performs a full synchronization of the account recieved in {@link #onPerformSync(Account, Bundle, String, ContentProviderClient, SyncResult)}. 
  61  * @author Bartek Przybylski 
  62  * @author David A. Velasco 
  64 public class FileSyncAdapter 
extends AbstractOwnCloudSyncAdapter 
{ 
  66     private final static String TAG 
= FileSyncAdapter
.class.getSimpleName(); 
  68     /** Maximum number of failed folder synchronizations that are supported before finishing the synchronization operation */ 
  69     private static final int MAX_FAILED_RESULTS 
= 3;  
  72     public static final String EVENT_FULL_SYNC_START 
= FileSyncAdapter
.class.getName() + ".EVENT_FULL_SYNC_START"; 
  73     public static final String EVENT_FULL_SYNC_END 
= FileSyncAdapter
.class.getName() + ".EVENT_FULL_SYNC_END"; 
  74     public static final String EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED 
= FileSyncAdapter
.class.getName() + ".EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED"; 
  75     //public static final String EVENT_FULL_SYNC_FOLDER_SIZE_SYNCED = FileSyncAdapter.class.getName() + ".EVENT_FULL_SYNC_FOLDER_SIZE_SYNCED"; 
  77     public static final String EXTRA_ACCOUNT_NAME 
= FileSyncAdapter
.class.getName() + ".EXTRA_ACCOUNT_NAME"; 
  78     public static final String EXTRA_FOLDER_PATH 
= FileSyncAdapter
.class.getName() + ".EXTRA_FOLDER_PATH"; 
  79     public static final String EXTRA_RESULT 
= FileSyncAdapter
.class.getName() + ".EXTRA_RESULT"; 
  82     /** Time stamp for the current synchronization process, used to distinguish fresh data */ 
  83     private long mCurrentSyncTime
; 
  85     /** Flag made 'true' when a request to cancel the synchronization is received */ 
  86     private boolean mCancellation
; 
  88     /** When 'true' the process was requested by the user through the user interface; when 'false', it was requested automatically by the system */ 
  89     private boolean mIsManualSync
; 
  91     /** Counter for failed operations in the synchronization process */ 
  92     private int mFailedResultsCounter
; 
  94     /** Result of the last failed operation */ 
  95     private RemoteOperationResult mLastFailedResult
; 
  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
; 
 103     /** Map of remote and local paths to files that where locally stored in a location out of the ownCloud folder and couldn't be copied automatically into it */ 
 104     private Map
<String
, String
> mForgottenLocalFiles
; 
 106     /** {@link SyncResult} instance to return to the system when the synchronization finish */ 
 107     private SyncResult mSyncResult
; 
 109     /** 'True' means that the server supports the share API */ 
 110     private boolean mIsShareSupported
; 
 114      * Creates a {@link FileSyncAdapter} 
 118     public FileSyncAdapter(Context context
, boolean autoInitialize
) { 
 119         super(context
, autoInitialize
); 
 124      * Creates a {@link FileSyncAdapter} 
 128     public FileSyncAdapter(Context context
, boolean autoInitialize
, boolean allowParallelSyncs
) { 
 129         super(context
, autoInitialize
, allowParallelSyncs
); 
 137     public synchronized void onPerformSync(Account account
, Bundle extras
, 
 138             String authority
, ContentProviderClient providerClient
, 
 139             SyncResult syncResult
) { 
 141         mCancellation 
= false
; 
 142         mIsManualSync 
= extras
.getBoolean(ContentResolver
.SYNC_EXTRAS_MANUAL
, false
); 
 143         mFailedResultsCounter 
= 0; 
 144         mLastFailedResult 
= null
; 
 146         mFailsInFavouritesFound 
= 0; 
 147         mForgottenLocalFiles 
= new HashMap
<String
, String
>(); 
 148         mSyncResult 
= syncResult
; 
 149         mSyncResult
.fullSyncRequested 
= false
; 
 150         mSyncResult
.delayUntil 
= 60*60*24; // avoid too many automatic synchronizations 
 152         this.setAccount(account
); 
 153         this.setContentProviderClient(providerClient
); 
 154         this.setStorageManager(new FileDataStorageManager(account
, providerClient
)); 
 157             this.initClientForCurrentAccount(); 
 158         } catch (IOException e
) { 
 159             /// the account is unknown for the Synchronization Manager, unreachable this context, or can not be authenticated; don't try this again 
 160             mSyncResult
.tooManyRetries 
= true
; 
 161             notifyFailedSynchronization(); 
 163         } catch (AccountsException e
) { 
 164             /// the account is unknown for the Synchronization Manager, unreachable this context, or can not be authenticated; don't try this again 
 165             mSyncResult
.tooManyRetries 
= true
; 
 166             notifyFailedSynchronization(); 
 170         Log_OC
.d(TAG
, "Synchronization of ownCloud account " + account
.name 
+ " starting"); 
 171         sendLocalBroadcast(EVENT_FULL_SYNC_START
, null
, null
);  // message to signal the start of the synchronization to the UI 
 175             mCurrentSyncTime 
= System
.currentTimeMillis(); 
 176             if (!mCancellation
) { 
 177                 synchronizeFolder(getStorageManager().getFileByPath(OCFile
.ROOT_PATH
)); 
 180                 Log_OC
.d(TAG
, "Leaving synchronization before synchronizing the root folder because cancelation request"); 
 185             // it's important making this although very unexpected errors occur; that's the reason for the finally 
 187             if (mFailedResultsCounter 
> 0 && mIsManualSync
) { 
 188                 /// don't let the system synchronization manager retries MANUAL synchronizations 
 189                 //      (be careful: "MANUAL" currently includes the synchronization requested when a new account is created and when the user changes the current account) 
 190                 mSyncResult
.tooManyRetries 
= true
; 
 192                 /// notify the user about the failure of MANUAL synchronization 
 193                 notifyFailedSynchronization(); 
 195             if (mConflictsFound 
> 0 || mFailsInFavouritesFound 
> 0) { 
 196                 notifyFailsInFavourites(); 
 198             if (mForgottenLocalFiles
.size() > 0) { 
 199                 notifyForgottenLocalFiles(); 
 201             sendLocalBroadcast(EVENT_FULL_SYNC_END
, null
, mLastFailedResult
);   // message to signal the end to the UI 
 207      * Called by system SyncManager when a synchronization is required to be cancelled. 
 209      * Sets the mCancellation flag to 'true'. THe synchronization will be stopped later,  
 210      * before a new folder is fetched. Data of the last folder synchronized will be still  
 213      * See {@link #onPerformSync(Account, Bundle, String, ContentProviderClient, SyncResult)} 
 214      * and {@link #synchronizeFolder(String, long)}. 
 217     public void onSyncCanceled() { 
 218         Log_OC
.d(TAG
, "Synchronization of " + getAccount().name 
+ " has been requested to cancel"); 
 219         mCancellation 
= true
; 
 220         super.onSyncCanceled(); 
 225      * Updates the locally stored version value of the ownCloud server 
 227     private void updateOCVersion() { 
 228         UpdateOCVersionOperation update 
= new UpdateOCVersionOperation(getAccount(), getContext()); 
 229         RemoteOperationResult result 
= update
.execute(getClient()); 
 230         if (!result
.isSuccess()) { 
 231             mLastFailedResult 
= result
;  
 233             mIsShareSupported 
= update
.getOCVersion().isSharedSupported(); 
 239      *  Synchronizes the list of files contained in a folder identified with its remote path. 
 241      *  Fetches the list and properties of the files contained in the given folder, including their  
 242      *  properties, and updates the local database with them. 
 244      *  Enters in the child folders to synchronize their contents also, following a recursive 
 245      *  depth first strategy.  
 247      *  @param folder                   Folder to synchronize. 
 249     private void synchronizeFolder(OCFile folder
) { 
 251         if (mFailedResultsCounter 
> MAX_FAILED_RESULTS 
|| isFinisher(mLastFailedResult
)) 
 256         long currentSyncTime,  
 257         boolean updateFolderProperties, 
 258         boolean syncFullAccount, 
 259         DataStorageManager dataStorageManager,  
 264         // folder synchronization 
 265         SynchronizeFolderOperation synchFolderOp 
= new SynchronizeFolderOperation(  folder
,  
 273         RemoteOperationResult result 
= synchFolderOp
.execute(getClient()); 
 276         // synchronized folder -> notice to UI - ALWAYS, although !result.isSuccess 
 277         sendLocalBroadcast(EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED
, folder
.getRemotePath(), result
); 
 279         // check the result of synchronizing the folder 
 280         if (result
.isSuccess() || result
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 282             if (result
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 283                 mConflictsFound 
+= synchFolderOp
.getConflictsFound(); 
 284                 mFailsInFavouritesFound 
+= synchFolderOp
.getFailsInFavouritesFound(); 
 286             if (synchFolderOp
.getForgottenLocalFiles().size() > 0) { 
 287                 mForgottenLocalFiles
.putAll(synchFolderOp
.getForgottenLocalFiles()); 
 289             if (result
.isSuccess()) { 
 290                 // synchronize children folders  
 291                 List
<OCFile
> children 
= synchFolderOp
.getChildren(); 
 292                 fetchChildren(folder
, children
, synchFolderOp
.getRemoteFolderChanged());    // beware of the 'hidden' recursion here! 
 296             // in failures, the statistics for the global result are updated 
 297             if (result
.getCode() == RemoteOperationResult
.ResultCode
.UNAUTHORIZED 
|| 
 298                     ( result
.isIdPRedirection() && 
 299                             getClient().getCredentials() == null      
)) { 
 300                             //MainApp.getAuthTokenTypeSamlSessionCookie().equals(getClient().getAuthTokenType()))) { 
 301                 mSyncResult
.stats
.numAuthExceptions
++; 
 303             } else if (result
.getException() instanceof DavException
) { 
 304                 mSyncResult
.stats
.numParseExceptions
++; 
 306             } else if (result
.getException() instanceof IOException
) {  
 307                 mSyncResult
.stats
.numIoExceptions
++; 
 309             mFailedResultsCounter
++; 
 310             mLastFailedResult 
= result
; 
 316      * Checks if a failed result should terminate the synchronization process immediately, according to 
 319      * @param   failedResult        Remote operation result to check. 
 320      * @return                      'True' if the result should immediately finish the synchronization 
 322     private boolean isFinisher(RemoteOperationResult failedResult
) { 
 323         if  (failedResult 
!= null
) { 
 324             RemoteOperationResult
.ResultCode code 
= failedResult
.getCode(); 
 325             return (code
.equals(RemoteOperationResult
.ResultCode
.SSL_ERROR
) || 
 326                     code
.equals(RemoteOperationResult
.ResultCode
.SSL_RECOVERABLE_PEER_UNVERIFIED
) || 
 327                     code
.equals(RemoteOperationResult
.ResultCode
.BAD_OC_VERSION
) || 
 328                     code
.equals(RemoteOperationResult
.ResultCode
.INSTANCE_NOT_CONFIGURED
)); 
 334      * Triggers the synchronization of any folder contained in the list of received files. 
 336      * @param files         Files to recursively synchronize. 
 338     private void fetchChildren(OCFile parent
, List
<OCFile
> files
, boolean parentEtagChanged
) { 
 340         OCFile newFile 
= null
; 
 341         //String etag = null; 
 342         //boolean syncDown = false; 
 343         for (i
=0; i 
< files
.size() && !mCancellation
; i
++) { 
 344             newFile 
= files
.get(i
); 
 345             if (newFile
.isFolder()) { 
 347                 etag = newFile.getEtag(); 
 348                 syncDown = (parentEtagChanged || etag == null || etag.length() == 0); 
 350                     synchronizeFolder(newFile
); 
 351                     //sendLocalBroadcast(EVENT_FULL_SYNC_FOLDER_SIZE_SYNCED, parent.getRemotePath(), null); 
 356         if (mCancellation 
&& i 
<files
.size()) Log_OC
.d(TAG
, "Leaving synchronization before synchronizing " + files
.get(i
).getRemotePath() + " due to cancelation request"); 
 361      * Sends a message to any application component interested in the progress of the synchronization. 
 363      * @param event             Event in the process of synchronization to be notified.    
 364      * @param dirRemotePath     Remote path of the folder target of the event occurred. 
 365      * @param result            Result of an individual {@ SynchronizeFolderOperation}, if completed; may be null. 
 367     private void sendLocalBroadcast(String event
, String dirRemotePath
, RemoteOperationResult result
) { 
 368         Log_OC
.d(TAG
, "Send broadcast " + event
); 
 369         Intent intent 
= new Intent(event
); 
 370         intent
.putExtra(FileSyncAdapter
.EXTRA_ACCOUNT_NAME
, getAccount().name
); 
 371         if (dirRemotePath 
!= null
) { 
 372             intent
.putExtra(FileSyncAdapter
.EXTRA_FOLDER_PATH
, dirRemotePath
); 
 374         if (result 
!= null
) { 
 375             intent
.putExtra(FileSyncAdapter
.EXTRA_RESULT
, result
); 
 377         getContext().sendStickyBroadcast(intent
); 
 378         //LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); 
 384      * Notifies the user about a failed synchronization through the status notification bar  
 386     private void notifyFailedSynchronization() { 
 387         NotificationCompat
.Builder notificationBuilder 
= createNotificationBuilder(); 
 388         notificationBuilder
.setTicker(i18n(R
.string
.sync_fail_ticker
)); 
 389         boolean needsToUpdateCredentials 
= (mLastFailedResult 
!= null 
&&  
 390                                              (  mLastFailedResult
.getCode() == ResultCode
.UNAUTHORIZED 
|| 
 391                                                 ( mLastFailedResult
.isIdPRedirection() &&  
 392                                                   getClient().getCredentials() == null      
) 
 393                                                  //MainApp.getAuthTokenTypeSamlSessionCookie().equals(getClient().getAuthTokenType())) 
 396         // TODO put something smart in the contentIntent below for all the possible errors 
 397         notificationBuilder
.setContentTitle(i18n(R
.string
.sync_fail_ticker
)); 
 398         if (needsToUpdateCredentials
) { 
 399             // let the user update credentials with one click 
 400             Intent updateAccountCredentials 
= new Intent(getContext(), AuthenticatorActivity
.class); 
 401             updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACCOUNT
, getAccount()); 
 402             updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ENFORCED_UPDATE
, true
); 
 403             updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACTION
, AuthenticatorActivity
.ACTION_UPDATE_TOKEN
); 
 404             updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
); 
 405             updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
); 
 406             updateAccountCredentials
.addFlags(Intent
.FLAG_FROM_BACKGROUND
); 
 408                 .setContentIntent(PendingIntent
.getActivity( 
 409                     getContext(), (int)System
.currentTimeMillis(), updateAccountCredentials
, PendingIntent
.FLAG_ONE_SHOT
 
 411                 .setContentText(i18n(R
.string
.sync_fail_content_unauthorized
, getAccount().name
)); 
 414                 .setContentText(i18n(R
.string
.sync_fail_content
, getAccount().name
)); 
 417         showNotification(R
.string
.sync_fail_ticker
, notificationBuilder
); 
 422      * Notifies the user about conflicts and strange fails when trying to synchronize the contents of kept-in-sync files. 
 424      * By now, we won't consider a failed synchronization. 
 426     private void notifyFailsInFavourites() { 
 427         if (mFailedResultsCounter 
> 0) { 
 428             NotificationCompat
.Builder notificationBuilder 
= createNotificationBuilder(); 
 429             notificationBuilder
.setTicker(i18n(R
.string
.sync_fail_in_favourites_ticker
)); 
 431             // TODO put something smart in the contentIntent below 
 433                 .setContentIntent(PendingIntent
.getActivity( 
 434                     getContext(), (int) System
.currentTimeMillis(), new Intent(), 0 
 436                 .setContentTitle(i18n(R
.string
.sync_fail_in_favourites_ticker
)) 
 437                 .setContentText(i18n(R
.string
.sync_fail_in_favourites_content
, mFailedResultsCounter 
+ mConflictsFound
, mConflictsFound
)); 
 439             showNotification(R
.string
.sync_fail_in_favourites_ticker
, notificationBuilder
); 
 441             NotificationCompat
.Builder notificationBuilder 
= createNotificationBuilder(); 
 442             notificationBuilder
.setTicker(i18n(R
.string
.sync_conflicts_in_favourites_ticker
)); 
 444             // TODO put something smart in the contentIntent below 
 446                 .setContentIntent(PendingIntent
.getActivity( 
 447                     getContext(), (int) System
.currentTimeMillis(), new Intent(), 0 
 449                 .setContentTitle(i18n(R
.string
.sync_conflicts_in_favourites_ticker
)) 
 450                 .setContentText(i18n(R
.string
.sync_conflicts_in_favourites_ticker
, mConflictsFound
)); 
 452             showNotification(R
.string
.sync_conflicts_in_favourites_ticker
, notificationBuilder
); 
 457      * Notifies the user about local copies of files out of the ownCloud local directory that were 'forgotten' because  
 458      * copying them inside the ownCloud local directory was not possible. 
 460      * We don't want links to files out of the ownCloud local directory (foreign files) anymore. It's easy to have  
 461      * synchronization problems if a local file is linked to more than one remote file. 
 463      * We won't consider a synchronization as failed when foreign files can not be copied to the ownCloud local directory. 
 465     private void notifyForgottenLocalFiles() { 
 466         NotificationCompat
.Builder notificationBuilder 
= createNotificationBuilder(); 
 467         notificationBuilder
.setTicker(i18n(R
.string
.sync_foreign_files_forgotten_ticker
)); 
 469         /// includes a pending intent in the notification showing a more detailed explanation 
 470         Intent explanationIntent 
= new Intent(getContext(), ErrorsWhileCopyingHandlerActivity
.class); 
 471         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_ACCOUNT
, getAccount()); 
 472         ArrayList
<String
> remotePaths 
= new ArrayList
<String
>(); 
 473         ArrayList
<String
> localPaths 
= new ArrayList
<String
>(); 
 474         remotePaths
.addAll(mForgottenLocalFiles
.keySet()); 
 475         localPaths
.addAll(mForgottenLocalFiles
.values()); 
 476         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_LOCAL_PATHS
, localPaths
); 
 477         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_REMOTE_PATHS
, remotePaths
);   
 478         explanationIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 481             .setContentIntent(PendingIntent
.getActivity( 
 482                 getContext(), (int) System
.currentTimeMillis(), explanationIntent
, 0 
 484             .setContentTitle(i18n(R
.string
.sync_foreign_files_forgotten_ticker
)) 
 485             .setContentText(i18n(R
.string
.sync_foreign_files_forgotten_content
, mForgottenLocalFiles
.size(), i18n(R
.string
.app_name
))); 
 487         showNotification(R
.string
.sync_foreign_files_forgotten_ticker
, notificationBuilder
); 
 491      * Creates a notification builder with some commonly used settings 
 495     private NotificationCompat
.Builder 
createNotificationBuilder() { 
 496         NotificationCompat
.Builder notificationBuilder 
= new NotificationCompat
.Builder(getContext()); 
 497         notificationBuilder
.setSmallIcon(R
.drawable
.notification_icon
).setAutoCancel(true
); 
 498         return notificationBuilder
; 
 502      * Builds and shows the notification 
 507     private void showNotification(int id
, NotificationCompat
.Builder builder
) { 
 508         ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)) 
 509             .notify(id
, builder
.build()); 
 512      * Shorthand translation 
 518     private String 
i18n(int key
, Object
... args
) { 
 519         return getContext().getString(key
, args
);