Home | History | Annotate | Download | only in service
      1 /*
      2  /*
      3  * Copyright (C) 2011 The Android Open Source Project
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.emailcommon.service;
     19 
     20 import android.content.ComponentName;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.ServiceConnection;
     24 import android.content.pm.ProviderInfo;
     25 import android.os.AsyncTask;
     26 import android.os.Debug;
     27 import android.os.IBinder;
     28 import android.os.Looper;
     29 import android.os.RemoteException;
     30 
     31 import com.android.emailcommon.provider.EmailContent;
     32 import com.android.mail.utils.LogUtils;
     33 
     34 /**
     35  * ServiceProxy is a superclass for proxy objects which make a single call to a service. It handles
     36  * connecting to the service, running a task supplied by the subclass when the connection is ready,
     37  * and disconnecting from the service afterwards. ServiceProxy objects cannot be reused (trying to
     38  * do so generates an {@link IllegalStateException}).
     39  *
     40  * Subclasses must override {@link #onConnected} to store the binder. Then, when the subclass wants
     41  * to make a service call, it should call {@link #setTask}, supplying the {@link ProxyTask} that
     42  * should run when the connection is ready. {@link ProxyTask#run} should implement the necessary
     43  * logic to make the call on the service.
     44  */
     45 
     46 public abstract class ServiceProxy {
     47     public static final String EXTRA_FORCE_SHUTDOWN = "ServiceProxy.FORCE_SHUTDOWN";
     48 
     49     private static final boolean DEBUG_PROXY = false; // DO NOT CHECK THIS IN SET TO TRUE
     50     private final String mTag;
     51 
     52     private final Context mContext;
     53     protected final Intent mIntent;
     54     private ProxyTask mTask;
     55     private String mName = " unnamed";
     56     private final ServiceConnection mConnection = new ProxyConnection();
     57     // Service call timeout (in seconds)
     58     private int mTimeout = 45;
     59     private long mStartTime;
     60     private boolean mTaskSet = false;
     61     private boolean mTaskCompleted = false;
     62 
     63     public static Intent getIntentForEmailPackage(Context context, String actionName) {
     64         /**
     65          * We want to scope the intent so that only the Email app will handle it. Unfortunately
     66          * we found that there are many instances where the package name of the Email app is
     67          * not what we expect. The easiest way to find the package of the correct app is to
     68          * see who is the EmailContent.AUTHORITY as there is only one app that can implement
     69          * the content provider for this authority and this is the right app to handle this intent.
     70          */
     71         final Intent intent = new Intent(EmailContent.EMAIL_PACKAGE_NAME + "." + actionName);
     72         final ProviderInfo info = context.getPackageManager().resolveContentProvider(
     73                 EmailContent.AUTHORITY, 0);
     74         if (info != null) {
     75             final String packageName = info.packageName;
     76             intent.setPackage(packageName);
     77         } else {
     78             LogUtils.e(LogUtils.TAG, "Could not find the Email Content Provider");
     79         }
     80         return intent;
     81     }
     82 
     83     /**
     84      * This function is called after the proxy connects to the service but before it runs its task.
     85      * Subclasses must override this to store the binder correctly.
     86      * @param binder The service IBinder.
     87      */
     88     public abstract void onConnected(IBinder binder);
     89 
     90     public ServiceProxy(Context _context, Intent _intent) {
     91         mContext = _context;
     92         mIntent = _intent;
     93         mTag = getClass().getSimpleName();
     94         if (Debug.isDebuggerConnected()) {
     95             mTimeout <<= 2;
     96         }
     97     }
     98 
     99     private class ProxyConnection implements ServiceConnection {
    100         @Override
    101         public void onServiceConnected(ComponentName name, IBinder binder) {
    102             if (DEBUG_PROXY) {
    103                 LogUtils.v(mTag, "Connected: " + name.getShortClassName() + " at " +
    104                         (System.currentTimeMillis() - mStartTime) + "ms");
    105             }
    106 
    107             // Let subclasses handle the binder.
    108             onConnected(binder);
    109 
    110             // Do our work in another thread.
    111             new AsyncTask<Void, Void, Void>() {
    112                 @Override
    113                 protected Void doInBackground(Void... params) {
    114                     try {
    115                         mTask.run();
    116                     } catch (RemoteException e) {
    117                         LogUtils.e(mTag, e, "RemoteException thrown running mTask!");
    118                     } finally {
    119                         // Make sure that we unbind the mConnection even on exceptions in the
    120                         // task provided by the subclass.
    121                         try {
    122                             // Each ServiceProxy handles just one task, so we unbind after we're
    123                             // done with our work.
    124                             mContext.unbindService(mConnection);
    125                         } catch (RuntimeException e) {
    126                             // The exceptions that are thrown here look like IllegalStateException,
    127                             // IllegalArgumentException and RuntimeException. Catching
    128                             // RuntimeException which get them all. Reasons for these exceptions
    129                             // include services that have already been stopped or unbound. This can
    130                             // happen if the user ended the activity that was using the service.
    131                             // This is harmless, but we've got to catch it.
    132                             LogUtils.e(mTag, e,
    133                                     "RuntimeException when trying to unbind from service");
    134                         }
    135                     }
    136                     mTaskCompleted = true;
    137                     synchronized(mConnection) {
    138                         if (DEBUG_PROXY) {
    139                             LogUtils.v(mTag, "Task " + mName + " completed; disconnecting");
    140                         }
    141                         mConnection.notify();
    142                     }
    143                     return null;
    144                 }
    145             }.execute();
    146         }
    147 
    148         @Override
    149         public void onServiceDisconnected(ComponentName name) {
    150             if (DEBUG_PROXY) {
    151                 LogUtils.v(mTag, "Disconnected: " + name.getShortClassName() + " at " +
    152                         (System.currentTimeMillis() - mStartTime) + "ms");
    153             }
    154         }
    155     }
    156 
    157     protected interface ProxyTask {
    158         public void run() throws RemoteException;
    159     }
    160 
    161     public ServiceProxy setTimeout(int secs) {
    162         mTimeout = secs;
    163         return this;
    164     }
    165 
    166     public int getTimeout() {
    167         return mTimeout;
    168     }
    169 
    170     protected boolean setTask(ProxyTask task, String name) throws IllegalStateException {
    171         if (mTaskSet) {
    172             throw new IllegalStateException("Cannot call setTask twice on the same ServiceProxy.");
    173         }
    174         mTaskSet = true;
    175         mName = name;
    176         mTask = task;
    177         mStartTime = System.currentTimeMillis();
    178         if (DEBUG_PROXY) {
    179             LogUtils.v(mTag, "Bind requested for task " + mName);
    180         }
    181         return mContext.bindService(mIntent, mConnection, Context.BIND_AUTO_CREATE);
    182     }
    183 
    184     /**
    185      * Callers that want to wait on the {@link ProxyTask} should call this immediately after calling
    186      * {@link #setTask}. This will wait until the task completes, up to the timeout (which can be
    187      * set with {@link #setTimeout}).
    188      */
    189     protected void waitForCompletion() {
    190         /*
    191          * onServiceConnected() is always called on the main thread, and we block the current thread
    192          * for up to 10 seconds as a timeout. If we're currently on the main thread,
    193          * onServiceConnected() is not called until our timeout elapses (and the UI is frozen for
    194          * the duration).
    195          */
    196         if (Looper.myLooper() == Looper.getMainLooper()) {
    197             throw new IllegalStateException("This cannot be called on the main thread.");
    198         }
    199 
    200         synchronized (mConnection) {
    201             long time = System.currentTimeMillis();
    202             try {
    203                 if (DEBUG_PROXY) {
    204                     LogUtils.v(mTag, "Waiting for task " + mName + " to complete...");
    205                 }
    206                 mConnection.wait(mTimeout * 1000L);
    207             } catch (InterruptedException e) {
    208                 // Can be ignored safely
    209             }
    210             if (DEBUG_PROXY) {
    211                 LogUtils.v(mTag, "Wait for " + mName +
    212                         (mTaskCompleted ? " finished in " : " timed out in ") +
    213                         (System.currentTimeMillis() - time) + "ms");
    214             }
    215         }
    216     }
    217 
    218     /**
    219      * Connection test; return indicates whether the remote service can be connected to
    220      * @return the result of trying to connect to the remote service
    221      */
    222     public boolean test() {
    223         try {
    224             return setTask(new ProxyTask() {
    225                 @Override
    226                 public void run() throws RemoteException {
    227                     if (DEBUG_PROXY) {
    228                         LogUtils.v(mTag, "Connection test succeeded in " +
    229                                 (System.currentTimeMillis() - mStartTime) + "ms");
    230                     }
    231                 }
    232             }, "test");
    233         } catch (Exception e) {
    234             // For any failure, return false.
    235             return false;
    236         }
    237     }
    238 }
    239