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.os.AsyncTask;
25import android.os.Debug;
26import android.os.IBinder;
27import android.os.Looper;
28import android.os.RemoteException;
29
30import com.android.mail.utils.LogUtils;
31
32/**
33 * ServiceProxy is a superclass for proxy objects which make a single call to a service. It handles
34 * connecting to the service, running a task supplied by the subclass when the connection is ready,
35 * and disconnecting from the service afterwards. ServiceProxy objects cannot be reused (trying to
36 * do so generates an {@link IllegalStateException}).
37 *
38 * Subclasses must override {@link #onConnected} to store the binder. Then, when the subclass wants
39 * to make a service call, it should call {@link #setTask}, supplying the {@link ProxyTask} that
40 * should run when the connection is ready. {@link ProxyTask#run} should implement the necessary
41 * logic to make the call on the service.
42 */
43
44public abstract class ServiceProxy {
45    public static final String EXTRA_FORCE_SHUTDOWN = "ServiceProxy.FORCE_SHUTDOWN";
46
47    private static final boolean DEBUG_PROXY = false; // DO NOT CHECK THIS IN SET TO TRUE
48    private final String mTag;
49
50    private final Context mContext;
51    protected final Intent mIntent;
52    private ProxyTask mTask;
53    private String mName = " unnamed";
54    private final ServiceConnection mConnection = new ProxyConnection();
55    // Service call timeout (in seconds)
56    private int mTimeout = 45;
57    private long mStartTime;
58    private boolean mTaskSet = false;
59    private boolean mTaskCompleted = false;
60
61    public static Intent getIntentForEmailPackage(Context context, String actionName) {
62        return new Intent(getIntentStringForEmailPackage(context, actionName));
63    }
64
65    /**
66     * Create Intent action based on the Email package name
67     * Package com.android.email + ACTION -> com.android.email.ACTION
68     * Package com.google.android.email + ACTION -> com.google.android.email.ACTION
69     * Package com.android.exchange + ACTION -> com.android.email.ACTION
70     * Package com.google.exchange + ACTION -> com.google.android.email.ACTION
71     *
72     * @param context the caller's context
73     * @param actionName the Intent action
74     * @return an Intent action based on the package name
75     */
76    public static String getIntentStringForEmailPackage(Context context, String actionName) {
77        String packageName = context.getPackageName();
78        int lastDot = packageName.lastIndexOf('.');
79        return packageName.substring(0, lastDot + 1) + "email." + actionName;
80    }
81
82    /**
83     * This function is called after the proxy connects to the service but before it runs its task.
84     * Subclasses must override this to store the binder correctly.
85     * @param binder The service IBinder.
86     */
87    public abstract void onConnected(IBinder binder);
88
89    public ServiceProxy(Context _context, Intent _intent) {
90        mContext = _context;
91        mIntent = _intent;
92        mTag = getClass().getSimpleName();
93        if (Debug.isDebuggerConnected()) {
94            mTimeout <<= 2;
95        }
96    }
97
98    private class ProxyConnection implements ServiceConnection {
99        @Override
100        public void onServiceConnected(ComponentName name, IBinder binder) {
101            if (DEBUG_PROXY) {
102                LogUtils.v(mTag, "Connected: " + name.getShortClassName() + " at " +
103                        (System.currentTimeMillis() - mStartTime) + "ms");
104            }
105
106            // Let subclasses handle the binder.
107            onConnected(binder);
108
109            // Do our work in another thread.
110            new AsyncTask<Void, Void, Void>() {
111                @Override
112                protected Void doInBackground(Void... params) {
113                    try {
114                        mTask.run();
115                    } catch (RemoteException e) {
116                    }
117                    try {
118                        // Each ServiceProxy handles just one task, so we unbind after we're
119                        // done with our work.
120                        mContext.unbindService(mConnection);
121                    } catch (IllegalArgumentException e) {
122                        // This can happen if the user ended the activity that was using the
123                        // service. This is harmless, but we've got to catch it.
124                    }
125                    mTaskCompleted = true;
126                    synchronized(mConnection) {
127                        if (DEBUG_PROXY) {
128                            LogUtils.v(mTag, "Task " + mName + " completed; disconnecting");
129                        }
130                        mConnection.notify();
131                    }
132                    return null;
133                }
134            }.execute();
135        }
136
137        @Override
138        public void onServiceDisconnected(ComponentName name) {
139            if (DEBUG_PROXY) {
140                LogUtils.v(mTag, "Disconnected: " + name.getShortClassName() + " at " +
141                        (System.currentTimeMillis() - mStartTime) + "ms");
142            }
143        }
144    }
145
146    protected interface ProxyTask {
147        public void run() throws RemoteException;
148    }
149
150    public ServiceProxy setTimeout(int secs) {
151        mTimeout = secs;
152        return this;
153    }
154
155    public int getTimeout() {
156        return mTimeout;
157    }
158
159    protected boolean setTask(ProxyTask task, String name) throws IllegalStateException {
160        if (mTaskSet) {
161            throw new IllegalStateException("Cannot call setTask twice on the same ServiceProxy.");
162        }
163        mTaskSet = true;
164        mName = name;
165        mTask = task;
166        mStartTime = System.currentTimeMillis();
167        if (DEBUG_PROXY) {
168            LogUtils.v(mTag, "Bind requested for task " + mName);
169        }
170        return mContext.bindService(mIntent, mConnection, Context.BIND_AUTO_CREATE);
171    }
172
173    /**
174     * Callers that want to wait on the {@link ProxyTask} should call this immediately after calling
175     * {@link #setTask}. This will wait until the task completes, up to the timeout (which can be
176     * set with {@link #setTimeout}).
177     */
178    protected void waitForCompletion() {
179        /*
180         * onServiceConnected() is always called on the main thread, and we block the current thread
181         * for up to 10 seconds as a timeout. If we're currently on the main thread,
182         * onServiceConnected() is not called until our timeout elapses (and the UI is frozen for
183         * the duration).
184         */
185        if (Looper.myLooper() == Looper.getMainLooper()) {
186            throw new IllegalStateException("This cannot be called on the main thread.");
187        }
188
189        synchronized (mConnection) {
190            long time = System.currentTimeMillis();
191            try {
192                if (DEBUG_PROXY) {
193                    LogUtils.v(mTag, "Waiting for task " + mName + " to complete...");
194                }
195                mConnection.wait(mTimeout * 1000L);
196            } catch (InterruptedException e) {
197                // Can be ignored safely
198            }
199            if (DEBUG_PROXY) {
200                LogUtils.v(mTag, "Wait for " + mName +
201                        (mTaskCompleted ? " finished in " : " timed out in ") +
202                        (System.currentTimeMillis() - time) + "ms");
203            }
204        }
205    }
206
207    /**
208     * Connection test; return indicates whether the remote service can be connected to
209     * @return the result of trying to connect to the remote service
210     */
211    public boolean test() {
212        try {
213            return setTask(new ProxyTask() {
214                @Override
215                public void run() throws RemoteException {
216                    if (DEBUG_PROXY) {
217                        LogUtils.v(mTag, "Connection test succeeded in " +
218                                (System.currentTimeMillis() - mStartTime) + "ms");
219                    }
220                }
221            }, "test");
222        } catch (Exception e) {
223            // For any failure, return false.
224            return false;
225        }
226    }
227}
228