1 /* ownCloud Android client application 
   2  *   Copyright (C) 2012-2013 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
.HashMap
; 
  22 import java
.util
.Iterator
; 
  24 import java
.util
.concurrent
.ConcurrentLinkedQueue
; 
  26 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
; 
  28 import com
.owncloud
.android
.lib
.network
.OwnCloudClientFactory
; 
  29 import com
.owncloud
.android
.lib
.network
.OwnCloudClient
; 
  30 import com
.owncloud
.android
.operations
.CreateShareOperation
; 
  31 import com
.owncloud
.android
.operations
.GetSharesOperation
; 
  32 import com
.owncloud
.android
.operations
.UnshareLinkOperation
; 
  33 import com
.owncloud
.android
.operations
.common
.SyncOperation
; 
  34 import com
.owncloud
.android
.lib
.operations
.common
.OnRemoteOperationListener
; 
  35 import com
.owncloud
.android
.lib
.operations
.common
.RemoteOperation
; 
  36 import com
.owncloud
.android
.lib
.operations
.common
.RemoteOperationResult
; 
  37 import com
.owncloud
.android
.lib
.operations
.common
.ShareType
; 
  38 import com
.owncloud
.android
.utils
.Log_OC
; 
  40 import android
.accounts
.Account
; 
  41 import android
.accounts
.AccountsException
; 
  42 import android
.app
.Service
; 
  43 import android
.content
.Intent
; 
  44 import android
.net
.Uri
; 
  45 import android
.os
.Binder
; 
  46 import android
.os
.Handler
; 
  47 import android
.os
.HandlerThread
; 
  48 import android
.os
.IBinder
; 
  49 import android
.os
.Looper
; 
  50 import android
.os
.Message
; 
  51 import android
.os
.Process
; 
  52 //import android.support.v4.content.LocalBroadcastManager; 
  53 import android
.util
.Pair
; 
  55 public class OperationsService 
extends Service 
{ 
  57     private static final String TAG 
= OperationsService
.class.getSimpleName(); 
  59     public static final String EXTRA_ACCOUNT 
= "ACCOUNT"; 
  60     public static final String EXTRA_SERVER_URL 
= "SERVER_URL"; 
  61     public static final String EXTRA_REMOTE_PATH 
= "REMOTE_PATH"; 
  62     public static final String EXTRA_SEND_INTENT 
= "SEND_INTENT"; 
  63     public static final String EXTRA_RESULT 
= "RESULT"; 
  65     public static final String ACTION_CREATE_SHARE 
= "CREATE_SHARE"; 
  66     public static final String ACTION_UNSHARE 
= "UNSHARE"; 
  68     public static final String ACTION_OPERATION_ADDED 
= OperationsService
.class.getName() + ".OPERATION_ADDED"; 
  69     public static final String ACTION_OPERATION_FINISHED 
= OperationsService
.class.getName() + ".OPERATION_FINISHED"; 
  71     private ConcurrentLinkedQueue
<Pair
<Target
, RemoteOperation
>> mPendingOperations 
= new ConcurrentLinkedQueue
<Pair
<Target
, RemoteOperation
>>(); 
  73     private static class Target 
{ 
  74         public Uri mServerUrl 
= null
; 
  75         public Account mAccount 
= null
; 
  76         public Target(Account account
, Uri serverUrl
) { 
  78             mServerUrl 
= serverUrl
; 
  82     private Looper mServiceLooper
; 
  83     private ServiceHandler mServiceHandler
; 
  84     private OperationsServiceBinder mBinder
; 
  85     private OwnCloudClient mOwnCloudClient 
= null
; 
  86     private Target mLastTarget 
= null
; 
  87     private FileDataStorageManager mStorageManager
; 
  88     private RemoteOperation mCurrentOperation 
= null
; 
  92      * Service initialization 
  95     public void onCreate() { 
  97         HandlerThread thread 
= new HandlerThread("Operations service thread", Process
.THREAD_PRIORITY_BACKGROUND
); 
  99         mServiceLooper 
= thread
.getLooper(); 
 100         mServiceHandler 
= new ServiceHandler(mServiceLooper
, this); 
 101         mBinder 
= new OperationsServiceBinder(); 
 105      * Entry point to add a new operation to the queue of operations. 
 107      * New operations are added calling to startService(), resulting in a call to this method.  
 108      * This ensures the service will keep on working although the caller activity goes away. 
 110      * IMPORTANT: the only operations performed here right now is {@link GetSharedFilesOperation}. The class 
 111      * is taking advantage of it due to time constraints. 
 114     public int onStartCommand(Intent intent
, int flags
, int startId
) { 
 115         if (!intent
.hasExtra(EXTRA_ACCOUNT
) && !intent
.hasExtra(EXTRA_SERVER_URL
)) { 
 116             Log_OC
.e(TAG
, "Not enough information provided in intent"); 
 117             return START_NOT_STICKY
; 
 120             Account account 
= intent
.getParcelableExtra(EXTRA_ACCOUNT
); 
 121             String serverUrl 
= intent
.getStringExtra(EXTRA_SERVER_URL
); 
 123             Target target 
= new Target(account
, (serverUrl 
== null
) ? null 
: Uri
.parse(serverUrl
)); 
 124             RemoteOperation operation 
= null
; 
 126             String action 
= intent
.getAction(); 
 127             if (action
.equals(ACTION_CREATE_SHARE
)) {  // Create Share 
 128                 String remotePath 
= intent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 129                 Intent sendIntent 
= intent
.getParcelableExtra(EXTRA_SEND_INTENT
); 
 130                 if (remotePath
.length() > 0) { 
 131                     operation 
= new CreateShareOperation(remotePath
, ShareType
.PUBLIC_LINK
,  
 132                             "", false
, "", 1, sendIntent
); 
 134             } else if (action
.equals(ACTION_UNSHARE
)) {  // Unshare file 
 135                 String remotePath 
= intent
.getStringExtra(EXTRA_REMOTE_PATH
); 
 136                 if (remotePath
.length() > 0) { 
 137                     operation 
= new UnshareLinkOperation(remotePath
, this.getApplicationContext()); 
 140                 operation 
= new GetSharesOperation(); 
 143             mPendingOperations
.add(new Pair
<Target 
, RemoteOperation
>(target
, operation
)); 
 144             //sendBroadcastNewOperation(target, operation); 
 146             Message msg 
= mServiceHandler
.obtainMessage(); 
 148             mServiceHandler
.sendMessage(msg
); 
 150         } catch (IllegalArgumentException e
) { 
 151             Log_OC
.e(TAG
, "Bad information provided in intent: " + e
.getMessage()); 
 152             return START_NOT_STICKY
; 
 155         return START_NOT_STICKY
; 
 160      * Provides a binder object that clients can use to perform actions on the queue of operations,  
 161      * except the addition of new operations.  
 164     public IBinder 
onBind(Intent intent
) { 
 170      * Called when ALL the bound clients were unbound. 
 173     public boolean onUnbind(Intent intent
) { 
 174         //((OperationsServiceBinder)mBinder).clearListeners(); 
 175         return false
;   // not accepting rebinding (default behaviour) 
 180      *  Binder to let client components to perform actions on the queue of operations. 
 182      *  It provides by itself the available operations. 
 184     public class OperationsServiceBinder 
extends Binder 
/* implements OnRemoteOperationListener */ { 
 187          * Map of listeners that will be reported about the end of operations from a {@link OperationsServiceBinder} instance  
 189         private Map
<OnRemoteOperationListener
, Handler
> mBoundListeners 
= new HashMap
<OnRemoteOperationListener
, Handler
>(); 
 192          * Cancels an operation 
 196         public void cancel() { 
 201         public void clearListeners() { 
 203             mBoundListeners
.clear(); 
 208          * Adds a listener interested in being reported about the end of operations. 
 210          * @param listener          Object to notify about the end of operations.     
 211          * @param callbackHandler   {@link Handler} to access the listener without breaking Android threading protection. 
 213         public void addOperationListener (OnRemoteOperationListener listener
, Handler callbackHandler
) { 
 214             mBoundListeners
.put(listener
, callbackHandler
); 
 219          * Removes a listener from the list of objects interested in the being reported about the end of operations. 
 221          * @param listener      Object to notify about progress of transfer.     
 223         public void removeOperationListener (OnRemoteOperationListener listener
) { 
 224             mBoundListeners
.remove(listener
); 
 229          * TODO - IMPORTANT: update implementation when more operations are moved into the service  
 231          * @return  'True' when an operation that enforces the user to wait for completion is in process. 
 233         public boolean isPerformingBlockingOperation() { 
 234             return (!mPendingOperations
.isEmpty()); 
 241      * Operations worker. Performs the pending operations in the order they were requested.  
 243      * Created with the Looper of a new thread, started in {@link OperationsService#onCreate()}.  
 245     private static class ServiceHandler 
extends Handler 
{ 
 246         // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak 
 247         OperationsService mService
; 
 248         public ServiceHandler(Looper looper
, OperationsService service
) { 
 250             if (service 
== null
) { 
 251                 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); 
 257         public void handleMessage(Message msg
) { 
 258             mService
.nextOperation(); 
 259             mService
.stopSelf(msg
.arg1
); 
 265      * Performs the next operation in the queue 
 267     private void nextOperation() { 
 269         Pair
<Target
, RemoteOperation
> next 
= null
; 
 270         synchronized(mPendingOperations
) { 
 271             next 
= mPendingOperations
.peek(); 
 276             mCurrentOperation 
= next
.second
; 
 277             RemoteOperationResult result 
= null
; 
 279                 /// prepare client object to send the request to the ownCloud server 
 280                 if (mLastTarget 
== null 
|| !mLastTarget
.equals(next
.first
)) { 
 281                     mLastTarget 
= next
.first
; 
 282                     if (mLastTarget
.mAccount 
!= null
) { 
 283                         mOwnCloudClient 
= OwnCloudClientFactory
.createOwnCloudClient(mLastTarget
.mAccount
, getApplicationContext()); 
 284                         mStorageManager 
= new FileDataStorageManager(mLastTarget
.mAccount
, getContentResolver()); 
 286                         mOwnCloudClient 
= OwnCloudClientFactory
.createOwnCloudClient(mLastTarget
.mServerUrl
, getApplicationContext(), true
);    // this is not good enough 
 287                         mStorageManager 
= null
; 
 291                 /// perform the operation 
 292                 if (mCurrentOperation 
instanceof SyncOperation
) { 
 293                     result 
= ((SyncOperation
)mCurrentOperation
).execute(mOwnCloudClient
, mStorageManager
); 
 295                     result 
= mCurrentOperation
.execute(mOwnCloudClient
); 
 298             } catch (AccountsException e
) { 
 299                 if (mLastTarget
.mAccount 
== null
) { 
 300                     Log_OC
.e(TAG
, "Error while trying to get autorization for a NULL account", e
); 
 302                     Log_OC
.e(TAG
, "Error while trying to get autorization for " + mLastTarget
.mAccount
.name
, e
); 
 304                 result 
= new RemoteOperationResult(e
); 
 306             } catch (IOException e
) { 
 307                 if (mLastTarget
.mAccount 
== null
) { 
 308                     Log_OC
.e(TAG
, "Error while trying to get autorization for a NULL account", e
); 
 310                     Log_OC
.e(TAG
, "Error while trying to get autorization for " + mLastTarget
.mAccount
.name
, e
); 
 312                 result 
= new RemoteOperationResult(e
); 
 313             } catch (Exception e
) { 
 314                 if (mLastTarget
.mAccount 
== null
) { 
 315                     Log_OC
.e(TAG
, "Unexpected error for a NULL account", e
); 
 317                     Log_OC
.e(TAG
, "Unexpected error for " + mLastTarget
.mAccount
.name
, e
); 
 319                 result 
= new RemoteOperationResult(e
); 
 322                 synchronized(mPendingOperations
) { 
 323                     mPendingOperations
.poll(); 
 327             //sendBroadcastOperationFinished(mLastTarget, mCurrentOperation, result); 
 328             callbackOperationListeners(mLastTarget
, mCurrentOperation
, result
); 
 334      * Sends a broadcast when a new operation is added to the queue. 
 336      * Local broadcasts are only delivered to activities in the same process, but can't be done sticky :\ 
 338      * @param target            Account or URL pointing to an OC server. 
 339      * @param operation         Added operation. 
 341     private void sendBroadcastNewOperation(Target target
, RemoteOperation operation
) { 
 342         Intent intent 
= new Intent(ACTION_OPERATION_ADDED
); 
 343         if (target
.mAccount 
!= null
) { 
 344             intent
.putExtra(EXTRA_ACCOUNT
, target
.mAccount
);     
 346             intent
.putExtra(EXTRA_SERVER_URL
, target
.mServerUrl
);     
 348         //LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); 
 349         //lbm.sendBroadcast(intent); 
 350         sendStickyBroadcast(intent
); 
 354     // TODO - maybe add a notification for real start of operations 
 357      * Sends a LOCAL broadcast when an operations finishes in order to the interested activities can update their view 
 359      * Local broadcasts are only delivered to activities in the same process. 
 361      * @param target            Account or URL pointing to an OC server. 
 362      * @param operation         Finished operation. 
 363      * @param result            Result of the operation. 
 365     private void sendBroadcastOperationFinished(Target target
, RemoteOperation operation
, RemoteOperationResult result
) { 
 366         Intent intent 
= new Intent(ACTION_OPERATION_FINISHED
); 
 367         intent
.putExtra(EXTRA_RESULT
, result
); 
 368         if (target
.mAccount 
!= null
) { 
 369             intent
.putExtra(EXTRA_ACCOUNT
, target
.mAccount
);     
 371             intent
.putExtra(EXTRA_SERVER_URL
, target
.mServerUrl
);     
 373         //LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); 
 374         //lbm.sendBroadcast(intent); 
 375         sendStickyBroadcast(intent
); 
 380      * Notifies the currently subscribed listeners about the end of an operation. 
 382      * @param target            Account or URL pointing to an OC server. 
 383      * @param operation         Finished operation. 
 384      * @param result            Result of the operation. 
 386     private void callbackOperationListeners(Target target
, final RemoteOperation operation
, final RemoteOperationResult result
) { 
 387         Iterator
<OnRemoteOperationListener
> listeners 
= mBinder
.mBoundListeners
.keySet().iterator(); 
 388         while (listeners
.hasNext()) { 
 389             final OnRemoteOperationListener listener 
= listeners
.next(); 
 390             final Handler handler 
= mBinder
.mBoundListeners
.get(listener
); 
 391             if (handler 
!= null
) {  
 392                 handler
.post(new Runnable() { 
 395                         listener
.onRemoteOperationFinish(operation
, result
);