Removed accidently added remote thumbnail generation.
[pub/Android/ownCloud.git] / src / com / owncloud / android / services / OperationsService.java
1 /* ownCloud Android client application
2 * Copyright (C) 2012-2014 ownCloud Inc.
3 *
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.
7 *
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.
12 *
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/>.
15 *
16 */
17
18 package com.owncloud.android.services;
19
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;
25
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;
51
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;
67
68 public class OperationsService extends Service {
69
70 private static final String TAG = OperationsService.class.getSimpleName();
71
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";
82
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";
89
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";
100
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";
103
104 private ConcurrentLinkedQueue<Pair<Target, RemoteOperation>> mPendingOperations =
105 new ConcurrentLinkedQueue<Pair<Target, RemoteOperation>>();
106
107 private ConcurrentMap<Integer, Pair<RemoteOperation, RemoteOperationResult>>
108 mUndispatchedFinishedOperations =
109 new ConcurrentHashMap<Integer, Pair<RemoteOperation, RemoteOperationResult>>();
110
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;
118
119 public Target(Account account, Uri serverUrl, String username, String password, String authToken,
120 String cookie) {
121 mAccount = account;
122 mServerUrl = serverUrl;
123 mUsername = username;
124 mPassword = password;
125 mAuthToken = authToken;
126 mCookie = cookie;
127 }
128 }
129
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;
137
138
139 /**
140 * Service initialization
141 */
142 @Override
143 public void onCreate() {
144 super.onCreate();
145 HandlerThread thread = new HandlerThread("Operations service thread", Process.THREAD_PRIORITY_BACKGROUND);
146 thread.start();
147 mServiceLooper = thread.getLooper();
148 mServiceHandler = new ServiceHandler(mServiceLooper, this);
149 mBinder = new OperationsServiceBinder();
150 }
151
152
153 /**
154 * Entry point to add a new operation to the queue of operations.
155 *
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.
158 *
159 * IMPORTANT: the only operations performed here right now is {@link GetSharedFilesOperation}. The class
160 * is taking advantage of it due to time constraints.
161 */
162 @Override
163 public int onStartCommand(Intent intent, int flags, int startId) {
164 //Log_OC.wtf(TAG, "onStartCommand init" );
165 Message msg = mServiceHandler.obtainMessage();
166 msg.arg1 = startId;
167 mServiceHandler.sendMessage(msg);
168 //Log_OC.wtf(TAG, "onStartCommand end" );
169 return START_NOT_STICKY;
170 }
171
172 @Override
173 public void onDestroy() {
174 //Log_OC.wtf(TAG, "onDestroy init" );
175 // Saving cookies
176 try {
177 OwnCloudClientManagerFactory.getDefaultSingleton().
178 saveAllClients(this, MainApp.getAccountType());
179
180 // TODO - get rid of these exceptions
181 } catch (AccountNotFoundException e) {
182 e.printStackTrace();
183 } catch (AuthenticatorException e) {
184 e.printStackTrace();
185 } catch (OperationCanceledException e) {
186 e.printStackTrace();
187 } catch (IOException e) {
188 e.printStackTrace();
189 }
190
191 //Log_OC.wtf(TAG, "Clear mUndispatchedFinisiedOperations" );
192 mUndispatchedFinishedOperations.clear();
193
194 //Log_OC.wtf(TAG, "onDestroy end" );
195 super.onDestroy();
196 }
197
198
199 /**
200 * Provides a binder object that clients can use to perform actions on the queue of operations,
201 * except the addition of new operations.
202 */
203 @Override
204 public IBinder onBind(Intent intent) {
205 //Log_OC.wtf(TAG, "onBind" );
206 return mBinder;
207 }
208
209
210 /**
211 * Called when ALL the bound clients were unbound.
212 */
213 @Override
214 public boolean onUnbind(Intent intent) {
215 ((OperationsServiceBinder)mBinder).clearListeners();
216 return false; // not accepting rebinding (default behaviour)
217 }
218
219
220 /**
221 * Binder to let client components to perform actions on the queue of operations.
222 *
223 * It provides by itself the available operations.
224 */
225 public class OperationsServiceBinder extends Binder /* implements OnRemoteOperationListener */ {
226
227 /**
228 * Map of listeners that will be reported about the end of operations from a {@link OperationsServiceBinder} instance
229 */
230 private ConcurrentMap<OnRemoteOperationListener, Handler> mBoundListeners =
231 new ConcurrentHashMap<OnRemoteOperationListener, Handler>();
232
233 /**
234 * Cancels an operation
235 *
236 * TODO
237 */
238 public void cancel() {
239 // TODO
240 }
241
242
243 public void clearListeners() {
244
245 mBoundListeners.clear();
246 }
247
248
249 /**
250 * Adds a listener interested in being reported about the end of operations.
251 *
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.
254 */
255 public void addOperationListener (OnRemoteOperationListener listener, Handler callbackHandler) {
256 synchronized (mBoundListeners) {
257 mBoundListeners.put(listener, callbackHandler);
258 }
259 }
260
261
262 /**
263 * Removes a listener from the list of objects interested in the being reported about the end of operations.
264 *
265 * @param listener Object to notify about progress of transfer.
266 */
267 public void removeOperationListener (OnRemoteOperationListener listener) {
268 synchronized (mBoundListeners) {
269 mBoundListeners.remove(listener);
270 }
271 }
272
273
274 /**
275 * TODO - IMPORTANT: update implementation when more operations are moved into the service
276 *
277 * @return 'True' when an operation that enforces the user to wait for completion is in process.
278 */
279 public boolean isPerformingBlockingOperation() {
280 return (!mPendingOperations.isEmpty());
281 }
282
283
284 /**
285 * Creates and adds to the queue a new operation, as described by operationIntent
286 *
287 * @param operationIntent Intent describing a new operation to queue and execute.
288 * @return Identifier of the operation created, or null if failed.
289 */
290 public long newOperation(Intent operationIntent) {
291 RemoteOperation operation = null;
292 Target target = null;
293 try {
294 if (!operationIntent.hasExtra(EXTRA_ACCOUNT) &&
295 !operationIntent.hasExtra(EXTRA_SERVER_URL)) {
296 Log_OC.e(TAG, "Not enough information provided in intent");
297
298 } else {
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);
305 target = new Target(
306 account,
307 (serverUrl == null) ? null : Uri.parse(serverUrl),
308 username,
309 password,
310 authToken,
311 cookie
312 );
313
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);
321 }
322
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(
327 remotePath,
328 OperationsService.this);
329 }
330
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);
334
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);
344
345 } else if (action.equals(ACTION_EXISTENCE_CHECK)) {
346 // 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);
350
351 } else if (action.equals(ACTION_GET_USER_NAME)) {
352 // Get User Name
353 operation = new GetRemoteUserNameOperation();
354
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);
360
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);
366
367 } else if (action.equals(ACTION_CREATE_FOLDER)) {
368 // 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);
372
373 } else if (action.equals(ACTION_SYNC_FILE)) {
374 // 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());
378 }
379
380 }
381
382 } catch (IllegalArgumentException e) {
383 Log_OC.e(TAG, "Bad information provided in intent: " + e.getMessage());
384 operation = null;
385 }
386
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();
393
394 } else {
395 //Log_OC.wtf(TAG, "New operation failed, returned Long.MAX_VALUE");
396 return Long.MAX_VALUE;
397 }
398 }
399
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);
405 return true;
406 //Log_OC.wtf(TAG, "Sending callback later");
407 } else {
408 if (!mPendingOperations.isEmpty()) {
409 return true;
410 } else {
411 return false;
412 }
413 //Log_OC.wtf(TAG, "Not finished yet");
414 }
415 }
416
417 }
418
419
420 /**
421 * Operations worker. Performs the pending operations in the order they were requested.
422 *
423 * Created with the Looper of a new thread, started in {@link OperationsService#onCreate()}.
424 */
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) {
429 super(looper);
430 if (service == null) {
431 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'");
432 }
433 mService = service;
434 }
435
436 @Override
437 public void handleMessage(Message msg) {
438 mService.nextOperation();
439 mService.stopSelf(msg.arg1);
440 }
441 }
442
443
444 /**
445 * Performs the next operation in the queue
446 */
447 private void nextOperation() {
448
449 //Log_OC.wtf(TAG, "nextOperation init" );
450
451 Pair<Target, RemoteOperation> next = null;
452 synchronized(mPendingOperations) {
453 next = mPendingOperations.peek();
454 }
455
456 if (next != null) {
457
458 mCurrentOperation = next.second;
459 RemoteOperationResult result = null;
460 try {
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);
468 mStorageManager =
469 new FileDataStorageManager(
470 mLastTarget.mAccount,
471 getContentResolver());
472 } else {
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
479
480 } else if (mLastTarget.mAuthToken != null &&
481 mLastTarget.mAuthToken.length() > 0) {
482 credentials = OwnCloudCredentialsFactory.newBearerCredentials(
483 mLastTarget.mAuthToken); // bearer token
484
485 } else if (mLastTarget.mCookie != null &&
486 mLastTarget.mCookie.length() > 0) {
487 credentials = OwnCloudCredentialsFactory.newSamlSsoCredentials(
488 mLastTarget.mCookie); // SAML SSO
489 }
490 OwnCloudAccount ocAccount = new OwnCloudAccount(
491 mLastTarget.mServerUrl, credentials);
492 mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton().
493 getClientFor(ocAccount, this);
494 mStorageManager = null;
495 }
496 }
497
498 /// perform the operation
499 if (mCurrentOperation instanceof SyncOperation) {
500 result = ((SyncOperation)mCurrentOperation).execute(mOwnCloudClient, mStorageManager);
501 } else {
502 result = mCurrentOperation.execute(mOwnCloudClient);
503 }
504
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);
508 } else {
509 Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e);
510 }
511 result = new RemoteOperationResult(e);
512
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);
516 } else {
517 Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e);
518 }
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);
523 } else {
524 Log_OC.e(TAG, "Unexpected error for " + mLastTarget.mAccount.name, e);
525 }
526 result = new RemoteOperationResult(e);
527
528 } finally {
529 synchronized(mPendingOperations) {
530 mPendingOperations.poll();
531 }
532 }
533
534 //sendBroadcastOperationFinished(mLastTarget, mCurrentOperation, result);
535 dispatchResultToOperationListeners(mLastTarget, mCurrentOperation, result);
536 }
537 }
538
539
540 /**
541 * Sends a broadcast when a new operation is added to the queue.
542 *
543 * Local broadcasts are only delivered to activities in the same process, but can't be done sticky :\
544 *
545 * @param target Account or URL pointing to an OC server.
546 * @param operation Added operation.
547 */
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);
552 } else {
553 intent.putExtra(EXTRA_SERVER_URL, target.mServerUrl);
554 }
555 //LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
556 //lbm.sendBroadcast(intent);
557 sendStickyBroadcast(intent);
558 }
559
560
561 // TODO - maybe add a notification for real start of operations
562
563 /**
564 * Sends a LOCAL broadcast when an operations finishes in order to the interested activities can update their view
565 *
566 * Local broadcasts are only delivered to activities in the same process.
567 *
568 * @param target Account or URL pointing to an OC server.
569 * @param operation Finished operation.
570 * @param result Result of the operation.
571 */
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);
577 } else {
578 intent.putExtra(EXTRA_SERVER_URL, target.mServerUrl);
579 }
580 //LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
581 //lbm.sendBroadcast(intent);
582 sendStickyBroadcast(intent);
583 }
584
585
586 /**
587 * Notifies the currently subscribed listeners about the end of an operation.
588 *
589 * @param target Account or URL pointing to an OC server.
590 * @param operation Finished operation.
591 * @param result Result of the operation.
592 */
593 private void dispatchResultToOperationListeners(
594 Target target, final RemoteOperation operation, final RemoteOperationResult result) {
595 int count = 0;
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() {
602 @Override
603 public void run() {
604 listener.onRemoteOperationFinish(operation, result);
605 }
606 });
607 count += 1;
608 }
609 }
610 if (count == 0) {
611 //mOperationResults.put(operation.hashCode(), result);
612 Pair<RemoteOperation, RemoteOperationResult> undispatched =
613 new Pair<RemoteOperation, RemoteOperationResult>(operation, result);
614 mUndispatchedFinishedOperations.put(operation.hashCode(), undispatched);
615 }
616 Log_OC.d(TAG, "Called " + count + " listeners");
617 }
618
619
620 }