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 de
.mobilcom
.debitel
.cloud
.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 de
.mobilcom
.debitel
.cloud
.android
.Log_OC
; 
  30 import de
.mobilcom
.debitel
.cloud
.android
.MainApp
; 
  31 import de
.mobilcom
.debitel
.cloud
.android
.R
; 
  32 import de
.mobilcom
.debitel
.cloud
.android
.authentication
.AuthenticatorActivity
; 
  33 import de
.mobilcom
.debitel
.cloud
.android
.datamodel
.DataStorageManager
; 
  34 import de
.mobilcom
.debitel
.cloud
.android
.datamodel
.FileDataStorageManager
; 
  35 import de
.mobilcom
.debitel
.cloud
.android
.datamodel
.OCFile
; 
  36 import de
.mobilcom
.debitel
.cloud
.android
.operations
.RemoteOperationResult
; 
  37 import de
.mobilcom
.debitel
.cloud
.android
.operations
.SynchronizeFolderOperation
; 
  38 import de
.mobilcom
.debitel
.cloud
.android
.operations
.UpdateOCVersionOperation
; 
  39 import de
.mobilcom
.debitel
.cloud
.android
.operations
.RemoteOperationResult
.ResultCode
; 
  40 import de
.mobilcom
.debitel
.cloud
.android
.ui
.activity
.ErrorsWhileCopyingHandlerActivity
; 
  42 import android
.accounts
.Account
; 
  43 import android
.accounts
.AccountsException
; 
  44 import android
.app
.Notification
; 
  45 import android
.app
.NotificationManager
; 
  46 import android
.app
.PendingIntent
; 
  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
; 
  55  * SyncAdapter implementation for syncing sample SyncAdapter contacts to the 
  56  * platform ContactOperations provider. 
  58  * @author Bartek Przybylski 
  59  * @author David A. Velasco 
  61 public class FileSyncAdapter 
extends AbstractOwnCloudSyncAdapter 
{ 
  63     private final static String TAG 
= "FileSyncAdapter"; 
  66      * Maximum number of failed folder synchronizations that are supported before finishing the synchronization operation 
  68     private static final int MAX_FAILED_RESULTS 
= 3;  
  70     private long mCurrentSyncTime
; 
  71     private boolean mCancellation
; 
  72     private boolean mIsManualSync
; 
  73     private int mFailedResultsCounter
;     
  74     private RemoteOperationResult mLastFailedResult
; 
  75     private SyncResult mSyncResult
; 
  76     private int mConflictsFound
; 
  77     private int mFailsInFavouritesFound
; 
  78     private Map
<String
, String
> mForgottenLocalFiles
; 
  81     public FileSyncAdapter(Context context
, boolean autoInitialize
) { 
  82         super(context
, autoInitialize
); 
  89     public synchronized void onPerformSync(Account account
, Bundle extras
, 
  90             String authority
, ContentProviderClient provider
, 
  91             SyncResult syncResult
) { 
  93         mCancellation 
= false
; 
  94         mIsManualSync 
= extras
.getBoolean(ContentResolver
.SYNC_EXTRAS_MANUAL
, false
); 
  95         mFailedResultsCounter 
= 0; 
  96         mLastFailedResult 
= null
; 
  98         mFailsInFavouritesFound 
= 0; 
  99         mForgottenLocalFiles 
= new HashMap
<String
, String
>(); 
 100         mSyncResult 
= syncResult
; 
 101         mSyncResult
.fullSyncRequested 
= false
; 
 102         mSyncResult
.delayUntil 
= 60*60*24; // sync after 24h 
 104         this.setAccount(account
); 
 105         this.setContentProvider(provider
); 
 106         this.setStorageManager(new FileDataStorageManager(account
, getContentProvider())); 
 108             this.initClientForCurrentAccount(); 
 109         } catch (IOException e
) { 
 110             /// the account is unknown for the Synchronization Manager, or unreachable for this context; don't try this again 
 111             mSyncResult
.tooManyRetries 
= true
; 
 112             notifyFailedSynchronization(); 
 114         } catch (AccountsException e
) { 
 115             /// the account is unknown for the Synchronization Manager, or unreachable for this context; don't try this again 
 116             mSyncResult
.tooManyRetries 
= true
; 
 117             notifyFailedSynchronization(); 
 121         Log_OC
.d(TAG
, "Synchronization of ownCloud account " + account
.name 
+ " starting"); 
 122         sendStickyBroadcast(true
, null
, null
);  // message to signal the start of the synchronization to the UI 
 126             mCurrentSyncTime 
= System
.currentTimeMillis(); 
 127             if (!mCancellation
) { 
 128                 fetchData(OCFile
.PATH_SEPARATOR
, DataStorageManager
.ROOT_PARENT_ID
); 
 131                 Log_OC
.d(TAG
, "Leaving synchronization before any remote request due to cancellation was requested"); 
 136             // it's important making this although very unexpected errors occur; that's the reason for the finally 
 138             if (mFailedResultsCounter 
> 0 && mIsManualSync
) { 
 139                 /// don't let the system synchronization manager retries MANUAL synchronizations 
 140                 //      (be careful: "MANUAL" currently includes the synchronization requested when a new account is created and when the user changes the current account) 
 141                 mSyncResult
.tooManyRetries 
= true
; 
 143                 /// notify the user about the failure of MANUAL synchronization 
 144                 notifyFailedSynchronization(); 
 147             if (mConflictsFound 
> 0 || mFailsInFavouritesFound 
> 0) { 
 148                 notifyFailsInFavourites(); 
 150             if (mForgottenLocalFiles
.size() > 0) { 
 151                 notifyForgottenLocalFiles(); 
 154             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_OC
.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
;  
 186      * Synchronize the properties of files and folders contained in a remote folder given by remotePath. 
 188      * @param remotePath        Remote path to the folder to synchronize. 
 189      * @param parentId          Database Id of the folder to synchronize. 
 191     private void fetchData(String remotePath
, long parentId
) { 
 193         if (mFailedResultsCounter 
> MAX_FAILED_RESULTS 
|| isFinisher(mLastFailedResult
)) 
 196         // perform folder synchronization 
 197         SynchronizeFolderOperation synchFolderOp 
= new SynchronizeFolderOperation(  remotePath
,  
 204         RemoteOperationResult result 
= synchFolderOp
.execute(getClient()); 
 207         // synchronized folder -> notice to UI - ALWAYS, although !result.isSuccess 
 208         sendStickyBroadcast(true
, remotePath
, null
); 
 210         if (result
.isSuccess() || result
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 212             if (result
.getCode() == ResultCode
.SYNC_CONFLICT
) { 
 213                 mConflictsFound 
+= synchFolderOp
.getConflictsFound(); 
 214                 mFailsInFavouritesFound 
+= synchFolderOp
.getFailsInFavouritesFound(); 
 216             if (synchFolderOp
.getForgottenLocalFiles().size() > 0) { 
 217                 mForgottenLocalFiles
.putAll(synchFolderOp
.getForgottenLocalFiles()); 
 219             // synchronize children folders  
 220             List
<OCFile
> children 
= synchFolderOp
.getChildren(); 
 221             fetchChildren(children
);    // beware of the 'hidden' recursion here! 
 223             sendStickyBroadcast(true
, remotePath
, null
); 
 226             if (result
.getCode() == RemoteOperationResult
.ResultCode
.UNAUTHORIZED 
|| 
 227                    // (result.isTemporalRedirection() && result.isIdPRedirection() && 
 228                     ( result
.isIdPRedirection() &&  
 229                             MainApp
.getAuthTokenTypeSamlSessionCookie().equals(getClient().getAuthTokenType()))) { 
 230                 mSyncResult
.stats
.numAuthExceptions
++; 
 232             } else if (result
.getException() instanceof DavException
) { 
 233                 mSyncResult
.stats
.numParseExceptions
++; 
 235             } else if (result
.getException() instanceof IOException
) {  
 236                 mSyncResult
.stats
.numIoExceptions
++; 
 238             mFailedResultsCounter
++; 
 239             mLastFailedResult 
= result
; 
 245      * Checks if a failed result should terminate the synchronization process immediately, according to 
 248      * @param   failedResult        Remote operation result to check. 
 249      * @return                      'True' if the result should immediately finish the synchronization 
 251     private boolean isFinisher(RemoteOperationResult failedResult
) { 
 252         if  (failedResult 
!= null
) { 
 253             RemoteOperationResult
.ResultCode code 
= failedResult
.getCode(); 
 254             return (code
.equals(RemoteOperationResult
.ResultCode
.SSL_ERROR
) || 
 255                     code
.equals(RemoteOperationResult
.ResultCode
.SSL_RECOVERABLE_PEER_UNVERIFIED
) || 
 256                     code
.equals(RemoteOperationResult
.ResultCode
.BAD_OC_VERSION
) || 
 257                     code
.equals(RemoteOperationResult
.ResultCode
.INSTANCE_NOT_CONFIGURED
)); 
 263      * Synchronize data of folders in the list of received files 
 265      * @param files         Files to recursively fetch  
 267     private void fetchChildren(List
<OCFile
> files
) { 
 269         for (i
=0; i 
< files
.size() && !mCancellation
; i
++) { 
 270             OCFile newFile 
= files
.get(i
); 
 271             if (newFile
.isDirectory()) { 
 272                 fetchData(newFile
.getRemotePath(), newFile
.getFileId()); 
 274                 // Update folder size on DB 
 275                 getStorageManager().calculateFolderSize(newFile
.getFileId());                    
 279         if (mCancellation 
&& i 
<files
.size()) Log_OC
.d(TAG
, "Leaving synchronization before synchronizing " + files
.get(i
).getRemotePath() + " because cancelation request"); 
 284      * Sends a message to any application component interested in the progress of the synchronization. 
 286      * @param inProgress        'True' when the synchronization progress is not finished. 
 287      * @param dirRemotePath     Remote path of a folder that was just synchronized (with or without success) 
 289     private void sendStickyBroadcast(boolean inProgress
, String dirRemotePath
, RemoteOperationResult result
) { 
 290         FileSyncService fileSyncService 
= new FileSyncService(); 
 292         Intent i 
= new Intent(fileSyncService
.getSyncMessage()); 
 293         i
.putExtra(FileSyncService
.IN_PROGRESS
, inProgress
); 
 294         i
.putExtra(FileSyncService
.ACCOUNT_NAME
, getAccount().name
); 
 295         if (dirRemotePath 
!= null
) { 
 296             i
.putExtra(FileSyncService
.SYNC_FOLDER_REMOTE_PATH
, dirRemotePath
); 
 298         if (result 
!= null
) { 
 299             i
.putExtra(FileSyncService
.SYNC_RESULT
, result
); 
 301         getContext().sendStickyBroadcast(i
); 
 307      * Notifies the user about a failed synchronization through the status notification bar  
 309     private void notifyFailedSynchronization() { 
 310         Notification notification 
= new Notification(R
.drawable
.icon
, getContext().getString(R
.string
.sync_fail_ticker
), System
.currentTimeMillis()); 
 311         notification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 312         boolean needsToUpdateCredentials 
= (mLastFailedResult 
!= null 
&&  
 313                                              (  mLastFailedResult
.getCode() == ResultCode
.UNAUTHORIZED 
|| 
 314                                                 // (mLastFailedResult.isTemporalRedirection() && mLastFailedResult.isIdPRedirection() &&  
 315                                                 ( mLastFailedResult
.isIdPRedirection() &&  
 316                                                  MainApp
.getAuthTokenTypeSamlSessionCookie().equals(getClient().getAuthTokenType())) 
 319         // TODO put something smart in the contentIntent below for all the possible errors 
 320         notification
.contentIntent 
= PendingIntent
.getActivity(getContext().getApplicationContext(), (int)System
.currentTimeMillis(), new Intent(), 0); 
 321         if (needsToUpdateCredentials
) { 
 322             // let the user update credentials with one click 
 323             Intent updateAccountCredentials 
= new Intent(getContext(), AuthenticatorActivity
.class); 
 324             updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACCOUNT
, getAccount()); 
 325             updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ENFORCED_UPDATE
, true
); 
 326             updateAccountCredentials
.putExtra(AuthenticatorActivity
.EXTRA_ACTION
, AuthenticatorActivity
.ACTION_UPDATE_TOKEN
); 
 327             updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
); 
 328             updateAccountCredentials
.addFlags(Intent
.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
); 
 329             updateAccountCredentials
.addFlags(Intent
.FLAG_FROM_BACKGROUND
); 
 330             notification
.contentIntent 
= PendingIntent
.getActivity(getContext(), (int)System
.currentTimeMillis(), updateAccountCredentials
, PendingIntent
.FLAG_ONE_SHOT
); 
 331             notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 332                     getContext().getString(R
.string
.sync_fail_ticker
),  
 333                     String
.format(getContext().getString(R
.string
.sync_fail_content_unauthorized
), getAccount().name
),  
 334                     notification
.contentIntent
); 
 336             notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 337                                             getContext().getString(R
.string
.sync_fail_ticker
),  
 338                                             String
.format(getContext().getString(R
.string
.sync_fail_content
), getAccount().name
),  
 339                                             notification
.contentIntent
); 
 341         ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)).notify(R
.string
.sync_fail_ticker
, notification
); 
 346      * Notifies the user about conflicts and strange fails when trying to synchronize the contents of kept-in-sync files. 
 348      * By now, we won't consider a failed synchronization. 
 350     private void notifyFailsInFavourites() { 
 351         if (mFailedResultsCounter 
> 0) { 
 352             Notification notification 
= new Notification(R
.drawable
.icon
, getContext().getString(R
.string
.sync_fail_in_favourites_ticker
), System
.currentTimeMillis()); 
 353             notification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 354             // TODO put something smart in the contentIntent below 
 355             notification
.contentIntent 
= PendingIntent
.getActivity(getContext().getApplicationContext(), (int)System
.currentTimeMillis(), new Intent(), 0); 
 356             notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 357                                             getContext().getString(R
.string
.sync_fail_in_favourites_ticker
),  
 358                                             String
.format(getContext().getString(R
.string
.sync_fail_in_favourites_content
), mFailedResultsCounter 
+ mConflictsFound
, mConflictsFound
),  
 359                                             notification
.contentIntent
); 
 360             ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)).notify(R
.string
.sync_fail_in_favourites_ticker
, notification
); 
 363             Notification notification 
= new Notification(R
.drawable
.icon
, getContext().getString(R
.string
.sync_conflicts_in_favourites_ticker
), System
.currentTimeMillis()); 
 364             notification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 365             // TODO put something smart in the contentIntent below 
 366             notification
.contentIntent 
= PendingIntent
.getActivity(getContext().getApplicationContext(), (int)System
.currentTimeMillis(), new Intent(), 0); 
 367             notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 368                                             getContext().getString(R
.string
.sync_conflicts_in_favourites_ticker
),  
 369                                             String
.format(getContext().getString(R
.string
.sync_conflicts_in_favourites_content
), mConflictsFound
),  
 370                                             notification
.contentIntent
); 
 371             ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)).notify(R
.string
.sync_conflicts_in_favourites_ticker
, notification
); 
 376      * Notifies the user about local copies of files out of the ownCloud local directory that were 'forgotten' because  
 377      * copying them inside the ownCloud local directory was not possible. 
 379      * We don't want links to files out of the ownCloud local directory (foreign files) anymore. It's easy to have  
 380      * synchronization problems if a local file is linked to more than one remote file. 
 382      * We won't consider a synchronization as failed when foreign files can not be copied to the ownCloud local directory. 
 384     private void notifyForgottenLocalFiles() { 
 385         Notification notification 
= new Notification(R
.drawable
.icon
, getContext().getString(R
.string
.sync_foreign_files_forgotten_ticker
), System
.currentTimeMillis()); 
 386         notification
.flags 
|= Notification
.FLAG_AUTO_CANCEL
; 
 388         /// includes a pending intent in the notification showing a more detailed explanation 
 389         Intent explanationIntent 
= new Intent(getContext(), ErrorsWhileCopyingHandlerActivity
.class); 
 390         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_ACCOUNT
, getAccount()); 
 391         ArrayList
<String
> remotePaths 
= new ArrayList
<String
>(); 
 392         ArrayList
<String
> localPaths 
= new ArrayList
<String
>(); 
 393         remotePaths
.addAll(mForgottenLocalFiles
.keySet()); 
 394         localPaths
.addAll(mForgottenLocalFiles
.values()); 
 395         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_LOCAL_PATHS
, localPaths
); 
 396         explanationIntent
.putExtra(ErrorsWhileCopyingHandlerActivity
.EXTRA_REMOTE_PATHS
, remotePaths
);   
 397         explanationIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
); 
 399         notification
.contentIntent 
= PendingIntent
.getActivity(getContext().getApplicationContext(), (int)System
.currentTimeMillis(), explanationIntent
, 0); 
 400         notification
.setLatestEventInfo(getContext().getApplicationContext(),  
 401                                         getContext().getString(R
.string
.sync_foreign_files_forgotten_ticker
),  
 402                                         String
.format(getContext().getString(R
.string
.sync_foreign_files_forgotten_content
), mForgottenLocalFiles
.size(), getContext().getString(R
.string
.app_name
)),  
 403                                         notification
.contentIntent
); 
 404         ((NotificationManager
) getContext().getSystemService(Context
.NOTIFICATION_SERVICE
)).notify(R
.string
.sync_foreign_files_forgotten_ticker
, notification
);