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
18package com.android.emailcommon.service;
19
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.ServiceConnection;
24import android.content.pm.ProviderInfo;
25import android.os.AsyncTask;
26import android.os.Debug;
27import android.os.IBinder;
28import android.os.Looper;
29import android.os.RemoteException;
30
31import com.android.emailcommon.provider.EmailContent;
32import 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
46public 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