1 /* ownCloud Android client application 
   2  *   Copyright (C) 2012-2014 ownCloud Inc. 
   4  *   This program is free software: you can redistribute it and/or modify 
   5  *   it under the terms of the GNU General Public License version 2, 
   6  *   as published by the Free Software Foundation. 
   8  *   This program is distributed in the hope that it will be useful, 
   9  *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
  10  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  11  *   GNU General Public License for more details. 
  13  *   You should have received a copy of the GNU General Public License 
  14  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. 
  18 package com
.owncloud
.android
.services
; 
  21 import java
.io
.IOException
; 
  22 import java
.util
.ArrayList
; 
  23 import java
.util
.Iterator
; 
  24 import java
.util
.concurrent
.ConcurrentHashMap
; 
  25 import java
.util
.concurrent
.ConcurrentLinkedQueue
; 
  26 import java
.util
.concurrent
.ConcurrentMap
; 
  28 import com
.owncloud
.android
.MainApp
; 
  29 import com
.owncloud
.android
.R
; 
  30 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  31 import com
.owncloud
.android
.datamodel
.OCFile
; 
  32 import com
.owncloud
.android
.files
.services
.FileDownloader
; 
  33 import com
.owncloud
.android
.lib
.common
.OwnCloudAccount
; 
  34 import com
.owncloud
.android
.lib
.common
.OwnCloudClient
; 
  35 import com
.owncloud
.android
.lib
.common
.OwnCloudClientManagerFactory
; 
  36 import com
.owncloud
.android
.lib
.common
.OwnCloudCredentials
; 
  37 import com
.owncloud
.android
.lib
.common
.OwnCloudCredentialsFactory
; 
  38 import com
.owncloud
.android
.lib
.common
.accounts
.AccountUtils
.AccountNotFoundException
; 
  39 import com
.owncloud
.android
.lib
.common
.operations
.OnRemoteOperationListener
; 
  40 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperation
; 
  41 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
; 
  42 import com
.owncloud
.android
.lib
.common
.utils
.Log_OC
; 
  43 import com
.owncloud
.android
.lib
.resources
.files
.ExistenceCheckRemoteOperation
; 
  44 import com
.owncloud
.android
.lib
.resources
.shares
.ShareType
; 
  45 import com
.owncloud
.android
.lib
.resources
.users
.GetRemoteUserNameOperation
; 
  46 import com
.owncloud
.android
.operations
.common
.SyncOperation
; 
  47 import com
.owncloud
.android
.operations
.CreateFolderOperation
; 
  48 import com
.owncloud
.android
.operations
.CreateShareOperation
; 
  49 import com
.owncloud
.android
.operations
.GetServerInfoOperation
; 
  50 import com
.owncloud
.android
.operations
.MoveFileOperation
; 
  51 import com
.owncloud
.android
.operations
.OAuth2GetAccessToken
; 
  52 import com
.owncloud
.android
.operations
.RemoveFileOperation
; 
  53 import com
.owncloud
.android
.operations
.RenameFileOperation
; 
  54 import com
.owncloud
.android
.operations
.SynchronizeFileOperation
; 
  55 import com
.owncloud
.android
.operations
.SynchronizeFolderOperation
; 
  56 import com
.owncloud
.android
.operations
.UnshareLinkOperation
; 
  57 import com
.owncloud
.android
.utils
.FileStorageUtils
; 
  59 import android
.accounts
.Account
; 
  60 import android
.accounts
.AccountsException
; 
  61 import android
.accounts
.AuthenticatorException
; 
  62 import android
.accounts
.OperationCanceledException
; 
  63 import android
.app
.Service
; 
  64 import android
.content
.Intent
; 
  65 import android
.net
.Uri
; 
  66 import android
.os
.Binder
; 
  67 import android
.os
.Handler
; 
  68 import android
.os
.HandlerThread
; 
  69 import android
.os
.IBinder
; 
  70 import android
.os
.Looper
; 
  71 import android
.os
.Message
; 
  72 import android
.os
.Process
; 
  73 import android
.util
.Pair
; 
  75 public class OperationsService 
extends Service 
{ 
  77     private static final String TAG 
= OperationsService
.class.getSimpleName(); 
  79     public static final String EXTRA_ACCOUNT 
= "ACCOUNT"; 
  80     public static final String EXTRA_SERVER_URL 
= "SERVER_URL"; 
  81     public static final String EXTRA_OAUTH2_QUERY_PARAMETERS 
= "OAUTH2_QUERY_PARAMETERS"; 
  82     public static final String EXTRA_REMOTE_PATH 
= "REMOTE_PATH"; 
  83     public static final String EXTRA_SEND_INTENT 
= "SEND_INTENT"; 
  84     public static final String EXTRA_NEWNAME 
= "NEWNAME"; 
  85     public static final String EXTRA_REMOVE_ONLY_LOCAL 
= "REMOVE_LOCAL_COPY"; 
  86     public static final String EXTRA_CREATE_FULL_PATH 
= "CREATE_FULL_PATH"; 
  87     public static final String EXTRA_SYNC_FILE_CONTENTS 
= "SYNC_FILE_CONTENTS"; 
  88     public static final String EXTRA_RESULT 
= "RESULT"; 
  89     public static final String EXTRA_NEW_PARENT_PATH 
= "NEW_PARENT_PATH"; 
  90     public static final String EXTRA_FILE 
= "FILE"; 
  92     // TODO review if ALL OF THEM are necessary 
  93     public static final String EXTRA_SUCCESS_IF_ABSENT 
= "SUCCESS_IF_ABSENT"; 
  94     public static final String EXTRA_USERNAME 
= "USERNAME"; 
  95     public static final String EXTRA_PASSWORD 
= "PASSWORD"; 
  96     public static final String EXTRA_AUTH_TOKEN 
= "AUTH_TOKEN"; 
  97     public static final String EXTRA_COOKIE 
= "COOKIE"; 
  99     public static final String ACTION_CREATE_SHARE 
= "CREATE_SHARE"; 
 100     public static final String ACTION_UNSHARE 
= "UNSHARE"; 
 101     public static final String ACTION_GET_SERVER_INFO 
= "GET_SERVER_INFO"; 
 102     public static final String ACTION_OAUTH2_GET_ACCESS_TOKEN 
= "OAUTH2_GET_ACCESS_TOKEN"; 
 103     public static final String ACTION_EXISTENCE_CHECK 
= "EXISTENCE_CHECK"; 
 104     public static final String ACTION_GET_USER_NAME 
= "GET_USER_NAME"; 
 105     public static final String ACTION_RENAME 
= "RENAME"; 
 106     public static final String ACTION_REMOVE 
= "REMOVE"; 
 107     public static final String ACTION_CREATE_FOLDER 
= "CREATE_FOLDER"; 
 108     public static final String ACTION_SYNC_FILE 
= "SYNC_FILE"; 
 109     public static final String ACTION_SYNC_FOLDER 
= "SYNC_FOLDER";  // for the moment, just to download 
 110     public static final String ACTION_CANCEL_SYNC_FOLDER 
= "CANCEL_SYNC_FOLDER";  // for the moment, just to download 
 111     public static final String ACTION_MOVE_FILE 
= "MOVE_FILE"; 
 113     public static final String ACTION_OPERATION_ADDED 
= OperationsService
.class.getName() + ".OPERATION_ADDED"; 
 114     public static final String ACTION_OPERATION_FINISHED 
= OperationsService
.class.getName() + ".OPERATION_FINISHED"; 
 117     private ConcurrentMap
<Integer
, Pair
<RemoteOperation
, RemoteOperationResult
>>  
 118         mUndispatchedFinishedOperations 
= 
 119             new ConcurrentHashMap
<Integer
, Pair
<RemoteOperation
, RemoteOperationResult
>>(); 
 121     private static class Target 
{ 
 122         public Uri mServerUrl 
= null
; 
 123         public Account mAccount 
= null
; 
 124         public String mUsername 
= null
; 
 125         public String mPassword 
= null
; 
 126         public String mAuthToken 
= null
; 
 127         public String mCookie 
= null
; 
 129         public Target(Account account
, Uri serverUrl
, String username
, String password
, String authToken
, 
 132             mServerUrl 
= serverUrl
; 
 133             mUsername 
= username
; 
 134             mPassword 
= password
; 
 135             mAuthToken 
= authToken
; 
 140     private ServiceHandler mOperationsHandler
; 
 141     private OperationsServiceBinder mOperationsBinder
; 
 143     private SyncFolderHandler mSyncFolderHandler
; 
 146      * Service initialization 
 149     public void onCreate() { 
 151         /// First worker thread for most of operations  
 152         HandlerThread thread 
= new HandlerThread("Operations thread", Process
.THREAD_PRIORITY_BACKGROUND
); 
 154         mOperationsHandler 
= new ServiceHandler(thread
.getLooper(), this); 
 155         mOperationsBinder 
= new OperationsServiceBinder(mOperationsHandler
); 
 157         /// Separated worker thread for download of folders (WIP) 
 158         thread 
= new HandlerThread("Syncfolder thread", Process
.THREAD_PRIORITY_BACKGROUND
); 
 160         mSyncFolderHandler 
= new SyncFolderHandler(thread
.getLooper(), this); 
 165      * Entry point to add a new operation to the queue of operations. 
 167      * New operations are added calling to startService(), resulting in a call to this method.  
 168      * This ensures the service will keep on working although the caller activity goes away. 
 171     public int onStartCommand(Intent intent
, int flags
, int startId
) { 
 172         // WIP: for the moment, only SYNC_FOLDER and CANCEL_SYNC_FOLDER is expected here; 
 173         // the rest of the operations are requested through the Binder 
 174         if (ACTION_SYNC_FOLDER
.equals(intent
.getAction())) { 
 175             if (!intent
.hasExtra(EXTRA_ACCOUNT
) || !intent
.hasExtra(EXTRA_REMOTE_PATH
)) { 
 176                 Log_OC
.e(TAG
, "Not enough information provided in intent"); 
 177                 return START_NOT_STICKY
; 
 179             Account account 
= intent
.getParcelableExtra(EXTRA_ACCOUNT
); 
 180             String remotePath 
= intent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 182             Pair
<Account
, String
> itemSyncKey 
=  new Pair
<Account 
, String
>(account
, remotePath
); 
 184             Pair
<Target
, RemoteOperation
> itemToQueue 
= newOperation(intent
); 
 185             if (itemToQueue 
!= null
) { 
 186                 mSyncFolderHandler
.add(account
, remotePath
, (SynchronizeFolderOperation
)itemToQueue
.second
); 
 187                 mSyncFolderHandler
.sendBroadcastNewSyncFolder(account
, remotePath
); 
 188                 Message msg 
= mSyncFolderHandler
.obtainMessage(); 
 190                 msg
.obj 
= itemSyncKey
; 
 191                 mSyncFolderHandler
.sendMessage(msg
); 
 193         } else if (ACTION_CANCEL_SYNC_FOLDER
.equals(intent
.getAction())) { 
 194             if (!intent
.hasExtra(EXTRA_ACCOUNT
) || !intent
.hasExtra(EXTRA_FILE
)) { 
 195                 Log_OC
.e(TAG
, "Not enough information provided in intent"); 
 196                 return START_NOT_STICKY
; 
 198             final Account account 
= intent
.getParcelableExtra(EXTRA_ACCOUNT
); 
 199             final OCFile file 
= intent
.getParcelableExtra(EXTRA_FILE
); 
 201             new Thread(new Runnable() { 
 203                     // Cancel the download 
 204                     mSyncFolderHandler
.cancel(account
,file
); 
 209             Message msg 
= mOperationsHandler
.obtainMessage(); 
 211             mOperationsHandler
.sendMessage(msg
); 
 214         return START_NOT_STICKY
; 
 218     public void onDestroy() { 
 219         //Log_OC.wtf(TAG, "onDestroy init" ); 
 222             OwnCloudClientManagerFactory
.getDefaultSingleton(). 
 223                 saveAllClients(this, MainApp
.getAccountType()); 
 225             // TODO - get rid of these exceptions 
 226         } catch (AccountNotFoundException e
) { 
 228         } catch (AuthenticatorException e
) { 
 230         } catch (OperationCanceledException e
) { 
 232         } catch (IOException e
) { 
 236         //Log_OC.wtf(TAG, "Clear mUndispatchedFinisiedOperations" ); 
 237         mUndispatchedFinishedOperations
.clear(); 
 239         //Log_OC.wtf(TAG, "onDestroy end" ); 
 244      * Provides a binder object that clients can use to perform actions on the queue of operations,  
 245      * except the addition of new operations.  
 248     public IBinder 
onBind(Intent intent
) { 
 249         //Log_OC.wtf(TAG, "onBind" ); 
 250         return mOperationsBinder
; 
 255      * Called when ALL the bound clients were unbound. 
 258     public boolean onUnbind(Intent intent
) { 
 259         ((OperationsServiceBinder
)mOperationsBinder
).clearListeners(); 
 260         return false
;   // not accepting rebinding (default behaviour) 
 265      *  Binder to let client components to perform actions on the queue of operations. 
 267      *  It provides by itself the available operations. 
 269     public class OperationsServiceBinder 
extends Binder 
/* implements OnRemoteOperationListener */ { 
 272          * Map of listeners that will be reported about the end of operations from a {@link OperationsServiceBinder} instance  
 274         private ConcurrentMap
<OnRemoteOperationListener
, Handler
> mBoundListeners 
=  
 275                 new ConcurrentHashMap
<OnRemoteOperationListener
, Handler
>(); 
 277         private ServiceHandler mServiceHandler 
= null
;    
 280         public OperationsServiceBinder(ServiceHandler serviceHandler
) { 
 281             mServiceHandler 
= serviceHandler
; 
 286          * Cancels an operation 
 290         public void cancel() { 
 295         public void clearListeners() { 
 297             mBoundListeners
.clear(); 
 302          * Adds a listener interested in being reported about the end of operations. 
 304          * @param listener          Object to notify about the end of operations.     
 305          * @param callbackHandler   {@link Handler} to access the listener without breaking Android threading protection. 
 307         public void addOperationListener (OnRemoteOperationListener listener
, Handler callbackHandler
) { 
 308             synchronized (mBoundListeners
) { 
 309                 mBoundListeners
.put(listener
, callbackHandler
); 
 315          * Removes a listener from the list of objects interested in the being reported about the end of operations. 
 317          * @param listener      Object to notify about progress of transfer.     
 319         public void removeOperationListener (OnRemoteOperationListener listener
) { 
 320             synchronized (mBoundListeners
) { 
 321                 mBoundListeners
.remove(listener
); 
 327          * TODO - IMPORTANT: update implementation when more operations are moved into the service  
 329          * @return  'True' when an operation that enforces the user to wait for completion is in process. 
 331         public boolean isPerformingBlockingOperation() { 
 332             return (!mServiceHandler
.mPendingOperations
.isEmpty()); 
 337          * Creates and adds to the queue a new operation, as described by operationIntent. 
 339          * Calls startService to make the operation is processed by the ServiceHandler. 
 341          * @param operationIntent       Intent describing a new operation to queue and execute. 
 342          * @return                      Identifier of the operation created, or null if failed. 
 344         public long queueNewOperation(Intent operationIntent
) { 
 345             Pair
<Target
, RemoteOperation
> itemToQueue 
= newOperation(operationIntent
); 
 346             if (itemToQueue 
!= null
) { 
 347                 mServiceHandler
.mPendingOperations
.add(itemToQueue
); 
 348                 startService(new Intent(OperationsService
.this, OperationsService
.class)); 
 349                 return itemToQueue
.second
.hashCode(); 
 352                 return Long
.MAX_VALUE
; 
 357         public boolean dispatchResultIfFinished(int operationId
, OnRemoteOperationListener listener
) { 
 358             Pair
<RemoteOperation
, RemoteOperationResult
> undispatched 
=  
 359                     mUndispatchedFinishedOperations
.remove(operationId
); 
 360             if (undispatched 
!= null
) { 
 361                 listener
.onRemoteOperationFinish(undispatched
.first
, undispatched
.second
); 
 363                 //Log_OC.wtf(TAG, "Sending callback later"); 
 365                 if (!mServiceHandler
.mPendingOperations
.isEmpty()) { 
 370                 //Log_OC.wtf(TAG, "Not finished yet"); 
 376          * Returns True when the file described by 'file' in the ownCloud account 'account' is downloading or waiting to download. 
 378          * If 'file' is a directory, returns 'true' if some of its descendant files is downloading or waiting to download.  
 380          * @param account       ownCloud account where the remote file is stored. 
 381          * @param file          A file that could be affected  
 384         public boolean isSynchronizing(Account account, String remotePath) { 
 385             return mSyncFolderHandler.isSynchronizing(account, remotePath); 
 393      * SyncFolder worker. Performs the pending operations in the order they were requested. 
 395      * Created with the Looper of a new thread, started in {@link OperationsService#onCreate()}. 
 397     private static class SyncFolderHandler 
extends Handler 
{ 
 399         // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak 
 401         OperationsService mService
; 
 403         private ConcurrentMap
<String
,SynchronizeFolderOperation
> mPendingOperations 
= 
 404                 new ConcurrentHashMap
<String
,SynchronizeFolderOperation
>(); 
 405         private OwnCloudClient mOwnCloudClient 
= null
; 
 406         private FileDataStorageManager mStorageManager
; 
 407         private SynchronizeFolderOperation mCurrentSyncOperation
; 
 410         public SyncFolderHandler(Looper looper
, OperationsService service
) { 
 412             if (service 
== null
) { 
 413                 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); 
 419         public boolean isSynchronizing(Account account
, String remotePath
) { 
 420             if (account 
== null 
|| remotePath 
== null
) return false
; 
 421             String targetKey 
= buildRemoteName(account
, remotePath
); 
 422             synchronized (mPendingOperations
) { 
 423                 // TODO - this can be slow when synchronizing a big tree - need a better data structure 
 424                 Iterator
<String
> it 
= mPendingOperations
.keySet().iterator(); 
 425                 boolean found 
= false
; 
 426                 while (it
.hasNext() && !found
) { 
 427                     found 
= it
.next().startsWith(targetKey
); 
 435         public void handleMessage(Message msg
) { 
 436             Pair
<Account
, String
> itemSyncKey 
= (Pair
<Account
, String
>) msg
.obj
; 
 437             doOperation(itemSyncKey
.first
, itemSyncKey
.second
); 
 438             mService
.stopSelf(msg
.arg1
); 
 443          * Performs the next operation in the queue 
 445         private void doOperation(Account account
, String remotePath
) { 
 447             String syncKey 
= buildRemoteName(account
,remotePath
); 
 449             synchronized(mPendingOperations
) { 
 450                 mCurrentSyncOperation 
= mPendingOperations
.get(syncKey
); 
 453             if (mCurrentSyncOperation 
!= null
) { 
 454                 RemoteOperationResult result 
= null
; 
 458                     OwnCloudAccount ocAccount 
= new OwnCloudAccount(account
, mService
); 
 459                     mOwnCloudClient 
= OwnCloudClientManagerFactory
.getDefaultSingleton(). 
 460                             getClientFor(ocAccount
, mService
); 
 461                     mStorageManager 
= new FileDataStorageManager( 
 463                             mService
.getContentResolver() 
 466                     result 
= mCurrentSyncOperation
.execute(mOwnCloudClient
, mStorageManager
); 
 468                 } catch (AccountsException e
) { 
 469                     Log_OC
.e(TAG
, "Error while trying to get autorization", e
); 
 470                 } catch (IOException e
) { 
 471                     Log_OC
.e(TAG
, "Error while trying to get autorization", e
); 
 473                     synchronized (mPendingOperations
) { 
 474                         mPendingOperations
.remove(syncKey
); 
 476                         SynchronizeFolderOperation checkedOp = mCurrentSyncOperation; 
 477                         String checkedKey = syncKey; 
 478                         while (checkedOp.getPendingChildrenCount() <= 0) { 
 479                         // while (!checkedOp.hasChildren()) { 
 480                             mPendingOperations.remove(checkedKey); 
 481                             String parentKey = buildRemoteName(account, (new File(checkedOp.getFolderPath())).getParent()); 
 482                             // String parentKey = buildRemoteName(account, checkedOp.getParentPath()); 
 483                             SynchronizeFolderOperation parentOp = mPendingOperations.get(parentKey); 
 484                             if (parentOp != null) { 
 485                                 parentOp.decreasePendingChildrenCount(); 
 491                     mService
.dispatchResultToOperationListeners(null
, mCurrentSyncOperation
, result
); 
 493                     sendBroadcastFinishedSyncFolder(account
, remotePath
, result
.isSuccess()); 
 498         public void add(Account account
, String remotePath
, SynchronizeFolderOperation syncFolderOperation
){ 
 499             String syncKey 
= buildRemoteName(account
,remotePath
); 
 500             mPendingOperations
.putIfAbsent(syncKey
,syncFolderOperation
); 
 504          * Cancels sync operations. 
 505          * @param account       Owncloud account where the remote file is stored. 
 506          * @param file          File OCFile 
 508         public void cancel(Account account
, OCFile file
){ 
 509             SynchronizeFolderOperation syncOperation 
= null
; 
 510             String targetKey 
= buildRemoteName(account
, file
.getRemotePath()); 
 511             ArrayList
<String
> keyItems 
= new ArrayList
<String
>(); 
 512             synchronized (mPendingOperations
) { 
 513                 if (file
.isFolder()) { 
 514                     Log_OC
.d(TAG
, "Canceling pending sync operations"); 
 515                     Iterator
<String
> it 
= mPendingOperations
.keySet().iterator(); 
 516                     boolean found 
= false
; 
 517                     while (it
.hasNext()) { 
 518                         String keySyncOperation 
= it
.next(); 
 519                         found 
= keySyncOperation
.startsWith(targetKey
); 
 521                             keyItems
.add(keySyncOperation
); 
 526                     // this is not really expected... 
 527                     Log_OC
.d(TAG
, "Canceling sync operation"); 
 528                     keyItems
.add(buildRemoteName(account
, file
.getRemotePath())); 
 530                 for (String item
: keyItems
) { 
 531                     syncOperation 
= mPendingOperations
.remove(item
); 
 532                     if (syncOperation 
!= null
) { 
 533                         syncOperation
.cancel(); 
 538             //sendBroadcastFinishedSyncFolder(account, file.getRemotePath()); 
 540             /// cancellation of download needs to be done separately in any case; a SynchronizeFolderOperation 
 541             //  may finish much sooner than the real download of the files in the folder 
 542             Intent intent 
= new Intent(mService
, FileDownloader
.class); 
 543             intent
.setAction(FileDownloader
.ACTION_CANCEL_FILE_DOWNLOAD
); 
 544             intent
.putExtra(FileDownloader
.EXTRA_ACCOUNT
, account
); 
 545             intent
.putExtra(FileDownloader
.EXTRA_FILE
, file
); 
 546             mService
.startService(intent
); 
 550          * Builds a key from the account and file to download 
 552          * @param account   Account where the file to download is stored 
 553          * @param path      File path 
 555         private String 
buildRemoteName(Account account
, String path
) { 
 556             return account
.name 
+ path
; 
 561          * TODO review this method when "folder synchronization" replaces "folder download"; this is a fast and ugly  
 564         private void sendBroadcastNewSyncFolder(Account account
, String remotePath
) { 
 565             Intent added 
= new Intent(FileDownloader
.getDownloadAddedMessage()); 
 566             added
.putExtra(FileDownloader
.ACCOUNT_NAME
, account
.name
); 
 567             added
.putExtra(FileDownloader
.EXTRA_REMOTE_PATH
, remotePath
); 
 568             added
.putExtra(FileDownloader
.EXTRA_FILE_PATH
, FileStorageUtils
.getSavePath(account
.name
) + remotePath
); 
 569             mService
.sendStickyBroadcast(added
); 
 573          * TODO review this method when "folder synchronization" replaces "folder download"; this is a fast and ugly  
 576         private void sendBroadcastFinishedSyncFolder(Account account
, String remotePath
, boolean success
) { 
 577             Intent finished 
= new Intent(FileDownloader
.getDownloadFinishMessage()); 
 578             finished
.putExtra(FileDownloader
.ACCOUNT_NAME
, account
.name
); 
 579             finished
.putExtra(FileDownloader
.EXTRA_REMOTE_PATH
, remotePath
); 
 580             finished
.putExtra(FileDownloader
.EXTRA_FILE_PATH
, FileStorageUtils
.getSavePath(account
.name
) + remotePath
); 
 581             finished
.putExtra(FileDownloader
.EXTRA_DOWNLOAD_RESULT
, success
); 
 582             mService
.sendStickyBroadcast(finished
); 
 590      * Operations worker. Performs the pending operations in the order they were requested.  
 592      * Created with the Looper of a new thread, started in {@link OperationsService#onCreate()}.  
 594     private static class ServiceHandler 
extends Handler 
{ 
 595         // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak 
 598         OperationsService mService
; 
 601         private ConcurrentLinkedQueue
<Pair
<Target
, RemoteOperation
>> mPendingOperations 
= 
 602                 new ConcurrentLinkedQueue
<Pair
<Target
, RemoteOperation
>>(); 
 603         private RemoteOperation mCurrentOperation 
= null
; 
 604         private Target mLastTarget 
= null
; 
 605         private OwnCloudClient mOwnCloudClient 
= null
; 
 606         private FileDataStorageManager mStorageManager
; 
 609         public ServiceHandler(Looper looper
, OperationsService service
) { 
 611             if (service 
== null
) { 
 612                 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); 
 618         public void handleMessage(Message msg
) { 
 620             mService
.stopSelf(msg
.arg1
); 
 625          * Performs the next operation in the queue 
 627         private void nextOperation() { 
 629             //Log_OC.wtf(TAG, "nextOperation init" ); 
 631             Pair
<Target
, RemoteOperation
> next 
= null
; 
 632             synchronized(mPendingOperations
) { 
 633                 next 
= mPendingOperations
.peek(); 
 638                 mCurrentOperation 
= next
.second
; 
 639                 RemoteOperationResult result 
= null
; 
 641                     /// prepare client object to send the request to the ownCloud server 
 642                     if (mLastTarget 
== null 
|| !mLastTarget
.equals(next
.first
)) { 
 643                         mLastTarget 
= next
.first
; 
 644                         if (mLastTarget
.mAccount 
!= null
) { 
 645                             OwnCloudAccount ocAccount 
= new OwnCloudAccount(mLastTarget
.mAccount
, mService
); 
 646                             mOwnCloudClient 
= OwnCloudClientManagerFactory
.getDefaultSingleton(). 
 647                                     getClientFor(ocAccount
, mService
); 
 648                             mStorageManager 
= new FileDataStorageManager( 
 649                                     mLastTarget
.mAccount
,  
 650                                     mService
.getContentResolver() 
 653                             OwnCloudCredentials credentials 
= null
; 
 654                             if (mLastTarget
.mUsername 
!= null 
&&  
 655                                     mLastTarget
.mUsername
.length() > 0) { 
 656                                 credentials 
= OwnCloudCredentialsFactory
.newBasicCredentials( 
 657                                         mLastTarget
.mUsername
,  
 658                                         mLastTarget
.mPassword
);  // basic 
 660                             } else if (mLastTarget
.mAuthToken 
!= null 
&&  
 661                                     mLastTarget
.mAuthToken
.length() > 0) { 
 662                                 credentials 
= OwnCloudCredentialsFactory
.newBearerCredentials( 
 663                                         mLastTarget
.mAuthToken
);  // bearer token 
 665                             } else if (mLastTarget
.mCookie 
!= null 
&& 
 666                                     mLastTarget
.mCookie
.length() > 0) { 
 667                                 credentials 
= OwnCloudCredentialsFactory
.newSamlSsoCredentials( 
 668                                         mLastTarget
.mCookie
); // SAML SSO 
 670                             OwnCloudAccount ocAccount 
= new OwnCloudAccount( 
 671                                     mLastTarget
.mServerUrl
, credentials
); 
 672                             mOwnCloudClient 
= OwnCloudClientManagerFactory
.getDefaultSingleton(). 
 673                                     getClientFor(ocAccount
, mService
); 
 674                             mStorageManager 
= null
; 
 678                     /// perform the operation 
 679                     if (mCurrentOperation 
instanceof SyncOperation
) { 
 680                         result 
= ((SyncOperation
)mCurrentOperation
).execute(mOwnCloudClient
, mStorageManager
); 
 682                         result 
= mCurrentOperation
.execute(mOwnCloudClient
); 
 685                 } catch (AccountsException e
) { 
 686                     if (mLastTarget
.mAccount 
== null
) { 
 687                         Log_OC
.e(TAG
, "Error while trying to get authorization for a NULL account", e
); 
 689                         Log_OC
.e(TAG
, "Error while trying to get authorization for " + mLastTarget
.mAccount
.name
, e
); 
 691                     result 
= new RemoteOperationResult(e
); 
 693                 } catch (IOException e
) { 
 694                     if (mLastTarget
.mAccount 
== null
) { 
 695                         Log_OC
.e(TAG
, "Error while trying to get authorization for a NULL account", e
); 
 697                         Log_OC
.e(TAG
, "Error while trying to get authorization for " + mLastTarget
.mAccount
.name
, e
); 
 699                     result 
= new RemoteOperationResult(e
); 
 700                 } catch (Exception e
) { 
 701                     if (mLastTarget
.mAccount 
== null
) { 
 702                         Log_OC
.e(TAG
, "Unexpected error for a NULL account", e
); 
 704                         Log_OC
.e(TAG
, "Unexpected error for " + mLastTarget
.mAccount
.name
, e
); 
 706                     result 
= new RemoteOperationResult(e
); 
 709                     synchronized(mPendingOperations
) { 
 710                         mPendingOperations
.poll(); 
 714                 //sendBroadcastOperationFinished(mLastTarget, mCurrentOperation, result); 
 715                 mService
.dispatchResultToOperationListeners(mLastTarget
, mCurrentOperation
, result
); 
 725      * Creates a new operation, as described by operationIntent. 
 727      * TODO - move to ServiceHandler (probably) 
 729      * @param operationIntent       Intent describing a new operation to queue and execute. 
 730      * @return                      Pair with the new operation object and the information about its target server. 
 732     private Pair
<Target 
, RemoteOperation
> newOperation(Intent operationIntent
) { 
 733         RemoteOperation operation 
= null
; 
 734         Target target 
= null
; 
 736             if (!operationIntent
.hasExtra(EXTRA_ACCOUNT
) &&  
 737                     !operationIntent
.hasExtra(EXTRA_SERVER_URL
)) { 
 738                 Log_OC
.e(TAG
, "Not enough information provided in intent"); 
 741                 Account account 
= operationIntent
.getParcelableExtra(EXTRA_ACCOUNT
); 
 742                 String serverUrl 
= operationIntent
.getStringExtra(EXTRA_SERVER_URL
); 
 743                 String username 
= operationIntent
.getStringExtra(EXTRA_USERNAME
); 
 744                 String password 
= operationIntent
.getStringExtra(EXTRA_PASSWORD
); 
 745                 String authToken 
= operationIntent
.getStringExtra(EXTRA_AUTH_TOKEN
); 
 746                 String cookie 
= operationIntent
.getStringExtra(EXTRA_COOKIE
); 
 749                         (serverUrl 
== null
) ? null 
: Uri
.parse(serverUrl
), 
 756                 String action 
= operationIntent
.getAction(); 
 757                 if (action
.equals(ACTION_CREATE_SHARE
)) {  // Create Share 
 758                     String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 759                     Intent sendIntent 
= operationIntent
.getParcelableExtra(EXTRA_SEND_INTENT
); 
 760                     if (remotePath
.length() > 0) { 
 761                         operation 
= new CreateShareOperation(OperationsService
.this, remotePath
, ShareType
.PUBLIC_LINK
, 
 762                                 "", false
, "", 1, sendIntent
); 
 765                 } else if (action
.equals(ACTION_UNSHARE
)) {  // Unshare file 
 766                     String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 767                     if (remotePath
.length() > 0) { 
 768                         operation 
= new UnshareLinkOperation( 
 770                                 OperationsService
.this); 
 773                 } else if (action
.equals(ACTION_GET_SERVER_INFO
)) {  
 774                     // check OC server and get basic information from it 
 775                     operation 
= new GetServerInfoOperation(serverUrl
, OperationsService
.this); 
 777                 } else if (action
.equals(ACTION_OAUTH2_GET_ACCESS_TOKEN
)) { 
 778                     /// GET ACCESS TOKEN to the OAuth server 
 779                     String oauth2QueryParameters 
= 
 780                             operationIntent
.getStringExtra(EXTRA_OAUTH2_QUERY_PARAMETERS
); 
 781                     operation 
= new OAuth2GetAccessToken( 
 782                             getString(R
.string
.oauth2_client_id
),  
 783                             getString(R
.string
.oauth2_redirect_uri
),        
 784                             getString(R
.string
.oauth2_grant_type
), 
 785                             oauth2QueryParameters
); 
 787                 } else if (action
.equals(ACTION_EXISTENCE_CHECK
)) { 
 789                     String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 790                     boolean successIfAbsent 
= operationIntent
.getBooleanExtra(EXTRA_SUCCESS_IF_ABSENT
, false
); 
 791                     operation 
= new ExistenceCheckRemoteOperation(remotePath
, OperationsService
.this, successIfAbsent
); 
 793                 } else if (action
.equals(ACTION_GET_USER_NAME
)) { 
 795                     operation 
= new GetRemoteUserNameOperation(); 
 797                 } else if (action
.equals(ACTION_RENAME
)) { 
 798                     // Rename file or folder 
 799                     String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 800                     String newName 
= operationIntent
.getStringExtra(EXTRA_NEWNAME
); 
 801                     operation 
= new RenameFileOperation(remotePath
, newName
); 
 803                 } else if (action
.equals(ACTION_REMOVE
)) { 
 804                     // Remove file or folder 
 805                     String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 806                     boolean onlyLocalCopy 
= operationIntent
.getBooleanExtra(EXTRA_REMOVE_ONLY_LOCAL
, false
); 
 807                     operation 
= new RemoveFileOperation(remotePath
, onlyLocalCopy
); 
 809                 } else if (action
.equals(ACTION_CREATE_FOLDER
)) { 
 811                     String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 812                     boolean createFullPath 
= operationIntent
.getBooleanExtra(EXTRA_CREATE_FULL_PATH
, true
); 
 813                     operation 
= new CreateFolderOperation(remotePath
, createFullPath
); 
 815                 } else if (action
.equals(ACTION_SYNC_FILE
)) { 
 817                     String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 818                     boolean syncFileContents 
= operationIntent
.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS
, true
); 
 819                     operation 
= new SynchronizeFileOperation( 
 820                             remotePath
, account
, syncFileContents
, getApplicationContext() 
 823                 } else if (action
.equals(ACTION_SYNC_FOLDER
)) { 
 825                     String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 826                     operation 
= new SynchronizeFolderOperation( 
 827                             this,                       // TODO remove this dependency from construction time  
 830                             System
.currentTimeMillis()  // TODO remove this dependency from construction time 
 833                 } else if (action
.equals(ACTION_MOVE_FILE
)) { 
 835                     String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 836                     String newParentPath 
= operationIntent
.getStringExtra(EXTRA_NEW_PARENT_PATH
); 
 837                     operation 
= new MoveFileOperation(remotePath
,newParentPath
,account
); 
 842         } catch (IllegalArgumentException e
) { 
 843             Log_OC
.e(TAG
, "Bad information provided in intent: " + e
.getMessage()); 
 847         if (operation 
!= null
) { 
 848             return new Pair
<Target 
, RemoteOperation
>(target
, operation
);   
 856      * Sends a broadcast when a new operation is added to the queue. 
 858      * Local broadcasts are only delivered to activities in the same process, but can't be done sticky :\ 
 860      * @param target            Account or URL pointing to an OC server. 
 861      * @param operation         Added operation. 
 863     private void sendBroadcastNewOperation(Target target
, RemoteOperation operation
) { 
 864         Intent intent 
= new Intent(ACTION_OPERATION_ADDED
); 
 865         if (target
.mAccount 
!= null
) { 
 866             intent
.putExtra(EXTRA_ACCOUNT
, target
.mAccount
);     
 868             intent
.putExtra(EXTRA_SERVER_URL
, target
.mServerUrl
);     
 870         //LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); 
 871         //lbm.sendBroadcast(intent); 
 872         sendStickyBroadcast(intent
); 
 876     // TODO - maybe add a notification for real start of operations 
 879      * Sends a LOCAL broadcast when an operations finishes in order to the interested activities can update their view 
 881      * Local broadcasts are only delivered to activities in the same process. 
 883      * @param target            Account or URL pointing to an OC server. 
 884      * @param operation         Finished operation. 
 885      * @param result            Result of the operation. 
 887     private void sendBroadcastOperationFinished(Target target
, RemoteOperation operation
, RemoteOperationResult result
) { 
 888         Intent intent 
= new Intent(ACTION_OPERATION_FINISHED
); 
 889         intent
.putExtra(EXTRA_RESULT
, result
); 
 890         if (target
.mAccount 
!= null
) { 
 891             intent
.putExtra(EXTRA_ACCOUNT
, target
.mAccount
);     
 893             intent
.putExtra(EXTRA_SERVER_URL
, target
.mServerUrl
);     
 895         //LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); 
 896         //lbm.sendBroadcast(intent); 
 897         sendStickyBroadcast(intent
); 
 902      * Notifies the currently subscribed listeners about the end of an operation. 
 904      * @param target            Account or URL pointing to an OC server. 
 905      * @param operation         Finished operation. 
 906      * @param result            Result of the operation. 
 908     private void dispatchResultToOperationListeners( 
 909             Target target
, final RemoteOperation operation
, final RemoteOperationResult result
) { 
 911         Iterator
<OnRemoteOperationListener
> listeners 
= mOperationsBinder
.mBoundListeners
.keySet().iterator(); 
 912         while (listeners
.hasNext()) { 
 913             final OnRemoteOperationListener listener 
= listeners
.next(); 
 914             final Handler handler 
= mOperationsBinder
.mBoundListeners
.get(listener
); 
 915             if (handler 
!= null
) {  
 916                 handler
.post(new Runnable() { 
 919                         listener
.onRemoteOperationFinish(operation
, result
); 
 926             //mOperationResults.put(operation.hashCode(), result); 
 927             Pair
<RemoteOperation
, RemoteOperationResult
> undispatched 
=  
 928                     new Pair
<RemoteOperation
, RemoteOperationResult
>(operation
, result
); 
 929             mUndispatchedFinishedOperations
.put(operation
.hashCode(), undispatched
); 
 931         Log_OC
.d(TAG
, "Called " + count 
+ " listeners");