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
; 
  20 import java
.io
.IOException
; 
  21 import java
.util
.Iterator
; 
  22 import java
.util
.concurrent
.ConcurrentHashMap
; 
  23 import java
.util
.concurrent
.ConcurrentLinkedQueue
; 
  24 import java
.util
.concurrent
.ConcurrentMap
; 
  26 import com
.owncloud
.android
.MainApp
; 
  27 import com
.owncloud
.android
.R
; 
  28 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  29 import com
.owncloud
.android
.lib
.common
.OwnCloudAccount
; 
  30 import com
.owncloud
.android
.lib
.common
.OwnCloudClient
; 
  31 import com
.owncloud
.android
.lib
.common
.OwnCloudClientManagerFactory
; 
  32 import com
.owncloud
.android
.lib
.common
.OwnCloudCredentials
; 
  33 import com
.owncloud
.android
.lib
.common
.OwnCloudCredentialsFactory
; 
  34 import com
.owncloud
.android
.lib
.common
.accounts
.AccountUtils
.AccountNotFoundException
; 
  35 import com
.owncloud
.android
.lib
.common
.operations
.OnRemoteOperationListener
; 
  36 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperation
; 
  37 import com
.owncloud
.android
.lib
.common
.operations
.RemoteOperationResult
; 
  38 import com
.owncloud
.android
.lib
.resources
.files
.ExistenceCheckRemoteOperation
; 
  39 import com
.owncloud
.android
.lib
.resources
.shares
.ShareType
; 
  40 import com
.owncloud
.android
.lib
.resources
.users
.GetRemoteUserNameOperation
; 
  41 import com
.owncloud
.android
.operations
.common
.SyncOperation
; 
  42 import com
.owncloud
.android
.operations
.CreateFolderOperation
; 
  43 import com
.owncloud
.android
.operations
.CreateShareOperation
; 
  44 import com
.owncloud
.android
.operations
.GetServerInfoOperation
; 
  45 import com
.owncloud
.android
.operations
.OAuth2GetAccessToken
; 
  46 import com
.owncloud
.android
.operations
.RemoveFileOperation
; 
  47 import com
.owncloud
.android
.operations
.RenameFileOperation
; 
  48 import com
.owncloud
.android
.operations
.SynchronizeFileOperation
; 
  49 import com
.owncloud
.android
.operations
.UnshareLinkOperation
; 
  50 import com
.owncloud
.android
.utils
.Log_OC
; 
  52 import android
.accounts
.Account
; 
  53 import android
.accounts
.AccountsException
; 
  54 import android
.accounts
.AuthenticatorException
; 
  55 import android
.accounts
.OperationCanceledException
; 
  56 import android
.app
.Service
; 
  57 import android
.content
.Intent
; 
  58 import android
.net
.Uri
; 
  59 import android
.os
.Binder
; 
  60 import android
.os
.Handler
; 
  61 import android
.os
.HandlerThread
; 
  62 import android
.os
.IBinder
; 
  63 import android
.os
.Looper
; 
  64 import android
.os
.Message
; 
  65 import android
.os
.Process
; 
  66 import android
.util
.Pair
; 
  68 public class OperationsService 
extends Service 
{ 
  70     private static final String TAG 
= OperationsService
.class.getSimpleName(); 
  72     public static final String EXTRA_ACCOUNT 
= "ACCOUNT"; 
  73     public static final String EXTRA_SERVER_URL 
= "SERVER_URL"; 
  74     public static final String EXTRA_OAUTH2_QUERY_PARAMETERS 
= "OAUTH2_QUERY_PARAMETERS"; 
  75     public static final String EXTRA_REMOTE_PATH 
= "REMOTE_PATH"; 
  76     public static final String EXTRA_SEND_INTENT 
= "SEND_INTENT"; 
  77     public static final String EXTRA_NEWNAME 
= "NEWNAME"; 
  78     public static final String EXTRA_REMOVE_ONLY_LOCAL 
= "REMOVE_LOCAL_COPY"; 
  79     public static final String EXTRA_CREATE_FULL_PATH 
= "CREATE_FULL_PATH"; 
  80     public static final String EXTRA_SYNC_FILE_CONTENTS 
= "SYNC_FILE_CONTENTS"; 
  81     public static final String EXTRA_RESULT 
= "RESULT"; 
  83     // TODO review if ALL OF THEM are necessary 
  84     public static final String EXTRA_SUCCESS_IF_ABSENT 
= "SUCCESS_IF_ABSENT"; 
  85     public static final String EXTRA_USERNAME 
= "USERNAME"; 
  86     public static final String EXTRA_PASSWORD 
= "PASSWORD"; 
  87     public static final String EXTRA_AUTH_TOKEN 
= "AUTH_TOKEN"; 
  88     public static final String EXTRA_COOKIE 
= "COOKIE"; 
  90     public static final String ACTION_CREATE_SHARE 
= "CREATE_SHARE"; 
  91     public static final String ACTION_UNSHARE 
= "UNSHARE"; 
  92     public static final String ACTION_GET_SERVER_INFO 
= "GET_SERVER_INFO"; 
  93     public static final String ACTION_OAUTH2_GET_ACCESS_TOKEN 
= "OAUTH2_GET_ACCESS_TOKEN"; 
  94     public static final String ACTION_EXISTENCE_CHECK 
= "EXISTENCE_CHECK"; 
  95     public static final String ACTION_GET_USER_NAME 
= "GET_USER_NAME"; 
  96     public static final String ACTION_RENAME 
= "RENAME"; 
  97     public static final String ACTION_REMOVE 
= "REMOVE"; 
  98     public static final String ACTION_CREATE_FOLDER 
= "CREATE_FOLDER"; 
  99     public static final String ACTION_SYNC_FILE 
= "SYNC_FILE"; 
 101     public static final String ACTION_OPERATION_ADDED 
= OperationsService
.class.getName() + ".OPERATION_ADDED"; 
 102     public static final String ACTION_OPERATION_FINISHED 
= OperationsService
.class.getName() + ".OPERATION_FINISHED"; 
 104     private ConcurrentLinkedQueue
<Pair
<Target
, RemoteOperation
>> mPendingOperations 
=  
 105             new ConcurrentLinkedQueue
<Pair
<Target
, RemoteOperation
>>(); 
 107     private ConcurrentMap
<Integer
, Pair
<RemoteOperation
, RemoteOperationResult
>>  
 108         mUndispatchedFinishedOperations 
= 
 109             new ConcurrentHashMap
<Integer
, Pair
<RemoteOperation
, RemoteOperationResult
>>(); 
 111     private static class Target 
{ 
 112         public Uri mServerUrl 
= null
; 
 113         public Account mAccount 
= null
; 
 114         public String mUsername 
= null
; 
 115         public String mPassword 
= null
; 
 116         public String mAuthToken 
= null
; 
 117         public String mCookie 
= null
; 
 119         public Target(Account account
, Uri serverUrl
, String username
, String password
, String authToken
, 
 122             mServerUrl 
= serverUrl
; 
 123             mUsername 
= username
; 
 124             mPassword 
= password
; 
 125             mAuthToken 
= authToken
; 
 130     private Looper mServiceLooper
; 
 131     private ServiceHandler mServiceHandler
; 
 132     private OperationsServiceBinder mBinder
; 
 133     private OwnCloudClient mOwnCloudClient 
= null
; 
 134     private Target mLastTarget 
= null
; 
 135     private FileDataStorageManager mStorageManager
; 
 136     private RemoteOperation mCurrentOperation 
= null
; 
 140      * Service initialization 
 143     public void onCreate() { 
 145         HandlerThread thread 
= new HandlerThread("Operations service thread", Process
.THREAD_PRIORITY_BACKGROUND
); 
 147         mServiceLooper 
= thread
.getLooper(); 
 148         mServiceHandler 
= new ServiceHandler(mServiceLooper
, this); 
 149         mBinder 
= new OperationsServiceBinder(); 
 154      * Entry point to add a new operation to the queue of operations. 
 156      * New operations are added calling to startService(), resulting in a call to this method.  
 157      * This ensures the service will keep on working although the caller activity goes away. 
 159      * IMPORTANT: the only operations performed here right now is {@link GetSharedFilesOperation}. The class 
 160      * is taking advantage of it due to time constraints. 
 163     public int onStartCommand(Intent intent
, int flags
, int startId
) { 
 164         //Log_OC.wtf(TAG, "onStartCommand init" ); 
 165         Message msg 
= mServiceHandler
.obtainMessage(); 
 167         mServiceHandler
.sendMessage(msg
); 
 168         //Log_OC.wtf(TAG, "onStartCommand end" ); 
 169         return START_NOT_STICKY
; 
 173     public void onDestroy() { 
 174         //Log_OC.wtf(TAG, "onDestroy init" ); 
 177             OwnCloudClientManagerFactory
.getDefaultSingleton(). 
 178                 saveAllClients(this, MainApp
.getAccountType()); 
 180             // TODO - get rid of these exceptions 
 181         } catch (AccountNotFoundException e
) { 
 183         } catch (AuthenticatorException e
) { 
 185         } catch (OperationCanceledException e
) { 
 187         } catch (IOException e
) { 
 191         //Log_OC.wtf(TAG, "Clear mUndispatchedFinisiedOperations" ); 
 192         mUndispatchedFinishedOperations
.clear(); 
 194         //Log_OC.wtf(TAG, "onDestroy end" ); 
 200      * Provides a binder object that clients can use to perform actions on the queue of operations,  
 201      * except the addition of new operations.  
 204     public IBinder 
onBind(Intent intent
) { 
 205         //Log_OC.wtf(TAG, "onBind" ); 
 211      * Called when ALL the bound clients were unbound. 
 214     public boolean onUnbind(Intent intent
) { 
 215         ((OperationsServiceBinder
)mBinder
).clearListeners(); 
 216         return false
;   // not accepting rebinding (default behaviour) 
 221      *  Binder to let client components to perform actions on the queue of operations. 
 223      *  It provides by itself the available operations. 
 225     public class OperationsServiceBinder 
extends Binder 
/* implements OnRemoteOperationListener */ { 
 228          * Map of listeners that will be reported about the end of operations from a {@link OperationsServiceBinder} instance  
 230         private ConcurrentMap
<OnRemoteOperationListener
, Handler
> mBoundListeners 
=  
 231                 new ConcurrentHashMap
<OnRemoteOperationListener
, Handler
>(); 
 234          * Cancels an operation 
 238         public void cancel() { 
 243         public void clearListeners() { 
 245             mBoundListeners
.clear(); 
 250          * Adds a listener interested in being reported about the end of operations. 
 252          * @param listener          Object to notify about the end of operations.     
 253          * @param callbackHandler   {@link Handler} to access the listener without breaking Android threading protection. 
 255         public void addOperationListener (OnRemoteOperationListener listener
, Handler callbackHandler
) { 
 256             synchronized (mBoundListeners
) { 
 257                 mBoundListeners
.put(listener
, callbackHandler
); 
 263          * Removes a listener from the list of objects interested in the being reported about the end of operations. 
 265          * @param listener      Object to notify about progress of transfer.     
 267         public void removeOperationListener (OnRemoteOperationListener listener
) { 
 268             synchronized (mBoundListeners
) { 
 269                 mBoundListeners
.remove(listener
); 
 275          * TODO - IMPORTANT: update implementation when more operations are moved into the service  
 277          * @return  'True' when an operation that enforces the user to wait for completion is in process. 
 279         public boolean isPerformingBlockingOperation() { 
 280             return (!mPendingOperations
.isEmpty()); 
 285          * Creates and adds to the queue a new operation, as described by operationIntent 
 287          * @param operationIntent       Intent describing a new operation to queue and execute. 
 288          * @return                      Identifier of the operation created, or null if failed. 
 290         public long newOperation(Intent operationIntent
) { 
 291             RemoteOperation operation 
= null
; 
 292             Target target 
= null
; 
 294                 if (!operationIntent
.hasExtra(EXTRA_ACCOUNT
) &&  
 295                         !operationIntent
.hasExtra(EXTRA_SERVER_URL
)) { 
 296                     Log_OC
.e(TAG
, "Not enough information provided in intent"); 
 299                     Account account 
= operationIntent
.getParcelableExtra(EXTRA_ACCOUNT
); 
 300                     String serverUrl 
= operationIntent
.getStringExtra(EXTRA_SERVER_URL
); 
 301                     String username 
= operationIntent
.getStringExtra(EXTRA_USERNAME
); 
 302                     String password 
= operationIntent
.getStringExtra(EXTRA_PASSWORD
); 
 303                     String authToken 
= operationIntent
.getStringExtra(EXTRA_AUTH_TOKEN
); 
 304                     String cookie 
= operationIntent
.getStringExtra(EXTRA_COOKIE
); 
 307                             (serverUrl 
== null
) ? null 
: Uri
.parse(serverUrl
), 
 314                     String action 
= operationIntent
.getAction(); 
 315                     if (action
.equals(ACTION_CREATE_SHARE
)) {  // Create Share 
 316                         String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 317                         Intent sendIntent 
= operationIntent
.getParcelableExtra(EXTRA_SEND_INTENT
); 
 318                         if (remotePath
.length() > 0) { 
 319                             operation 
= new CreateShareOperation(remotePath
, ShareType
.PUBLIC_LINK
,  
 320                                     "", false
, "", 1, sendIntent
); 
 323                     } else if (action
.equals(ACTION_UNSHARE
)) {  // Unshare file 
 324                         String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 325                         if (remotePath
.length() > 0) { 
 326                             operation 
= new UnshareLinkOperation( 
 328                                     OperationsService
.this); 
 331                     } else if (action
.equals(ACTION_GET_SERVER_INFO
)) {  
 332                         // check OC server and get basic information from it 
 333                         operation 
= new GetServerInfoOperation(serverUrl
, OperationsService
.this); 
 335                     } else if (action
.equals(ACTION_OAUTH2_GET_ACCESS_TOKEN
)) { 
 336                         /// GET ACCESS TOKEN to the OAuth server 
 337                         String oauth2QueryParameters 
= 
 338                                 operationIntent
.getStringExtra(EXTRA_OAUTH2_QUERY_PARAMETERS
); 
 339                         operation 
= new OAuth2GetAccessToken( 
 340                                 getString(R
.string
.oauth2_client_id
),  
 341                                 getString(R
.string
.oauth2_redirect_uri
),        
 342                                 getString(R
.string
.oauth2_grant_type
), 
 343                                 oauth2QueryParameters
); 
 345                     } else if (action
.equals(ACTION_EXISTENCE_CHECK
)) { 
 347                         String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 348                         boolean successIfAbsent 
= operationIntent
.getBooleanExtra(EXTRA_SUCCESS_IF_ABSENT
, false
); 
 349                         operation 
= new ExistenceCheckRemoteOperation(remotePath
, OperationsService
.this, successIfAbsent
); 
 351                     } else if (action
.equals(ACTION_GET_USER_NAME
)) { 
 353                         operation 
= new GetRemoteUserNameOperation(); 
 355                     } else if (action
.equals(ACTION_RENAME
)) { 
 356                         // Rename file or folder 
 357                         String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 358                         String newName 
= operationIntent
.getStringExtra(EXTRA_NEWNAME
); 
 359                         operation 
= new RenameFileOperation(remotePath
, account
, newName
); 
 361                     } else if (action
.equals(ACTION_REMOVE
)) { 
 362                         // Remove file or folder 
 363                         String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 364                         boolean onlyLocalCopy 
= operationIntent
.getBooleanExtra(EXTRA_REMOVE_ONLY_LOCAL
, false
); 
 365                         operation 
= new RemoveFileOperation(remotePath
, onlyLocalCopy
); 
 367                     } else if (action
.equals(ACTION_CREATE_FOLDER
)) { 
 369                         String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 370                         boolean createFullPath 
= operationIntent
.getBooleanExtra(EXTRA_CREATE_FULL_PATH
, true
); 
 371                         operation 
= new CreateFolderOperation(remotePath
, createFullPath
); 
 373                     } else if (action
.equals(ACTION_SYNC_FILE
)) { 
 375                         String remotePath 
= operationIntent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 376                         boolean syncFileContents 
= operationIntent
.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS
, true
); 
 377                         operation 
= new SynchronizeFileOperation(remotePath
, account
, syncFileContents
, getApplicationContext()); 
 382             } catch (IllegalArgumentException e
) { 
 383                 Log_OC
.e(TAG
, "Bad information provided in intent: " + e
.getMessage()); 
 387             if (operation 
!= null
) { 
 388                 mPendingOperations
.add(new Pair
<Target 
, RemoteOperation
>(target
, operation
)); 
 389                 startService(new Intent(OperationsService
.this, OperationsService
.class)); 
 390                 //Log_OC.wtf(TAG, "New operation added, opId: " + operation.hashCode()); 
 391                 // better id than hash? ; should be good enough by the time being 
 392                 return operation
.hashCode(); 
 395                 //Log_OC.wtf(TAG, "New operation failed, returned Long.MAX_VALUE"); 
 396                 return Long
.MAX_VALUE
; 
 400         public boolean dispatchResultIfFinished(int operationId
, OnRemoteOperationListener listener
) { 
 401             Pair
<RemoteOperation
, RemoteOperationResult
> undispatched 
=  
 402                     mUndispatchedFinishedOperations
.remove(operationId
); 
 403             if (undispatched 
!= null
) { 
 404                 listener
.onRemoteOperationFinish(undispatched
.first
, undispatched
.second
); 
 406                 //Log_OC.wtf(TAG, "Sending callback later"); 
 408                 if (!mPendingOperations
.isEmpty()) { 
 413                 //Log_OC.wtf(TAG, "Not finished yet"); 
 421      * Operations worker. Performs the pending operations in the order they were requested.  
 423      * Created with the Looper of a new thread, started in {@link OperationsService#onCreate()}.  
 425     private static class ServiceHandler 
extends Handler 
{ 
 426         // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak 
 427         OperationsService mService
; 
 428         public ServiceHandler(Looper looper
, OperationsService service
) { 
 430             if (service 
== null
) { 
 431                 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); 
 437         public void handleMessage(Message msg
) { 
 438             mService
.nextOperation(); 
 439             mService
.stopSelf(msg
.arg1
); 
 445      * Performs the next operation in the queue 
 447     private void nextOperation() { 
 449         //Log_OC.wtf(TAG, "nextOperation init" ); 
 451         Pair
<Target
, RemoteOperation
> next 
= null
; 
 452         synchronized(mPendingOperations
) { 
 453             next 
= mPendingOperations
.peek(); 
 458             mCurrentOperation 
= next
.second
; 
 459             RemoteOperationResult result 
= null
; 
 461                 /// prepare client object to send the request to the ownCloud server 
 462                 if (mLastTarget 
== null 
|| !mLastTarget
.equals(next
.first
)) { 
 463                     mLastTarget 
= next
.first
; 
 464                     if (mLastTarget
.mAccount 
!= null
) { 
 465                         OwnCloudAccount ocAccount 
= new OwnCloudAccount(mLastTarget
.mAccount
, this); 
 466                         mOwnCloudClient 
= OwnCloudClientManagerFactory
.getDefaultSingleton(). 
 467                                 getClientFor(ocAccount
, this); 
 469                                 new FileDataStorageManager( 
 470                                         mLastTarget
.mAccount
,  
 471                                         getContentResolver()); 
 473                         OwnCloudCredentials credentials 
= null
; 
 474                         if (mLastTarget
.mUsername 
!= null 
&&  
 475                                 mLastTarget
.mUsername
.length() > 0) { 
 476                             credentials 
= OwnCloudCredentialsFactory
.newBasicCredentials( 
 477                                     mLastTarget
.mUsername
,  
 478                                     mLastTarget
.mPassword
);  // basic 
 480                         } else if (mLastTarget
.mAuthToken 
!= null 
&&  
 481                                 mLastTarget
.mAuthToken
.length() > 0) { 
 482                             credentials 
= OwnCloudCredentialsFactory
.newBearerCredentials( 
 483                                     mLastTarget
.mAuthToken
);  // bearer token 
 485                         } else if (mLastTarget
.mCookie 
!= null 
&& 
 486                                 mLastTarget
.mCookie
.length() > 0) { 
 487                             credentials 
= OwnCloudCredentialsFactory
.newSamlSsoCredentials( 
 488                                     mLastTarget
.mCookie
); // SAML SSO 
 490                         OwnCloudAccount ocAccount 
= new OwnCloudAccount( 
 491                                 mLastTarget
.mServerUrl
, credentials
); 
 492                         mOwnCloudClient 
= OwnCloudClientManagerFactory
.getDefaultSingleton(). 
 493                                 getClientFor(ocAccount
, this); 
 494                         mStorageManager 
= null
; 
 498                 /// perform the operation 
 499                 if (mCurrentOperation 
instanceof SyncOperation
) { 
 500                     result 
= ((SyncOperation
)mCurrentOperation
).execute(mOwnCloudClient
, mStorageManager
); 
 502                     result 
= mCurrentOperation
.execute(mOwnCloudClient
); 
 505             } catch (AccountsException e
) { 
 506                 if (mLastTarget
.mAccount 
== null
) { 
 507                     Log_OC
.e(TAG
, "Error while trying to get authorization for a NULL account", e
); 
 509                     Log_OC
.e(TAG
, "Error while trying to get authorization for " + mLastTarget
.mAccount
.name
, e
); 
 511                 result 
= new RemoteOperationResult(e
); 
 513             } catch (IOException e
) { 
 514                 if (mLastTarget
.mAccount 
== null
) { 
 515                     Log_OC
.e(TAG
, "Error while trying to get authorization for a NULL account", e
); 
 517                     Log_OC
.e(TAG
, "Error while trying to get authorization for " + mLastTarget
.mAccount
.name
, e
); 
 519                 result 
= new RemoteOperationResult(e
); 
 520             } catch (Exception e
) { 
 521                 if (mLastTarget
.mAccount 
== null
) { 
 522                     Log_OC
.e(TAG
, "Unexpected error for a NULL account", e
); 
 524                     Log_OC
.e(TAG
, "Unexpected error for " + mLastTarget
.mAccount
.name
, e
); 
 526                 result 
= new RemoteOperationResult(e
); 
 529                 synchronized(mPendingOperations
) { 
 530                     mPendingOperations
.poll(); 
 534             //sendBroadcastOperationFinished(mLastTarget, mCurrentOperation, result); 
 535             dispatchResultToOperationListeners(mLastTarget
, mCurrentOperation
, result
); 
 541      * Sends a broadcast when a new operation is added to the queue. 
 543      * Local broadcasts are only delivered to activities in the same process, but can't be done sticky :\ 
 545      * @param target            Account or URL pointing to an OC server. 
 546      * @param operation         Added operation. 
 548     private void sendBroadcastNewOperation(Target target
, RemoteOperation operation
) { 
 549         Intent intent 
= new Intent(ACTION_OPERATION_ADDED
); 
 550         if (target
.mAccount 
!= null
) { 
 551             intent
.putExtra(EXTRA_ACCOUNT
, target
.mAccount
);     
 553             intent
.putExtra(EXTRA_SERVER_URL
, target
.mServerUrl
);     
 555         //LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); 
 556         //lbm.sendBroadcast(intent); 
 557         sendStickyBroadcast(intent
); 
 561     // TODO - maybe add a notification for real start of operations 
 564      * Sends a LOCAL broadcast when an operations finishes in order to the interested activities can update their view 
 566      * Local broadcasts are only delivered to activities in the same process. 
 568      * @param target            Account or URL pointing to an OC server. 
 569      * @param operation         Finished operation. 
 570      * @param result            Result of the operation. 
 572     private void sendBroadcastOperationFinished(Target target
, RemoteOperation operation
, RemoteOperationResult result
) { 
 573         Intent intent 
= new Intent(ACTION_OPERATION_FINISHED
); 
 574         intent
.putExtra(EXTRA_RESULT
, result
); 
 575         if (target
.mAccount 
!= null
) { 
 576             intent
.putExtra(EXTRA_ACCOUNT
, target
.mAccount
);     
 578             intent
.putExtra(EXTRA_SERVER_URL
, target
.mServerUrl
);     
 580         //LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); 
 581         //lbm.sendBroadcast(intent); 
 582         sendStickyBroadcast(intent
); 
 587      * Notifies the currently subscribed listeners about the end of an operation. 
 589      * @param target            Account or URL pointing to an OC server. 
 590      * @param operation         Finished operation. 
 591      * @param result            Result of the operation. 
 593     private void dispatchResultToOperationListeners( 
 594             Target target
, final RemoteOperation operation
, final RemoteOperationResult result
) { 
 596         Iterator
<OnRemoteOperationListener
> listeners 
= mBinder
.mBoundListeners
.keySet().iterator(); 
 597         while (listeners
.hasNext()) { 
 598             final OnRemoteOperationListener listener 
= listeners
.next(); 
 599             final Handler handler 
= mBinder
.mBoundListeners
.get(listener
); 
 600             if (handler 
!= null
) {  
 601                 handler
.post(new Runnable() { 
 604                         listener
.onRemoteOperationFinish(operation
, result
); 
 611             //mOperationResults.put(operation.hashCode(), result); 
 612             Pair
<RemoteOperation
, RemoteOperationResult
> undispatched 
=  
 613                     new Pair
<RemoteOperation
, RemoteOperationResult
>(operation
, result
); 
 614             mUndispatchedFinishedOperations
.put(operation
.hashCode(), undispatched
); 
 616         Log_OC
.d(TAG
, "Called " + count 
+ " listeners");