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 
getIntentForEmailPackage(Context context, String actionName)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      */
onConnected(IBinder binder)88     public abstract void onConnected(IBinder binder);
89 
ServiceProxy(Context _context, Intent _intent)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
onServiceConnected(ComponentName name, IBinder binder)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
onServiceDisconnected(ComponentName name)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 {
run()158         public void run() throws RemoteException;
159     }
160 
setTimeout(int secs)161     public ServiceProxy setTimeout(int secs) {
162         mTimeout = secs;
163         return this;
164     }
165 
getTimeout()166     public int getTimeout() {
167         return mTimeout;
168     }
169 
setTask(ProxyTask task, String name)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      */
waitForCompletion()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      */
test()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