VoiceInteractionSession.java revision 20d94749043d0851f1da10c7749fd7eb13a35081
1/**
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.service.voice;
18
19import android.app.Dialog;
20import android.app.Instrumentation;
21import android.content.Context;
22import android.content.Intent;
23import android.content.res.TypedArray;
24import android.graphics.Rect;
25import android.graphics.Region;
26import android.inputmethodservice.SoftInputWindow;
27import android.os.Binder;
28import android.os.Bundle;
29import android.os.Handler;
30import android.os.IBinder;
31import android.os.Message;
32import android.os.RemoteException;
33import android.util.ArrayMap;
34import android.util.Log;
35import android.view.Gravity;
36import android.view.KeyEvent;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.ViewGroup;
40import android.view.ViewTreeObserver;
41import android.view.WindowManager;
42import android.widget.FrameLayout;
43import com.android.internal.app.IVoiceInteractionManagerService;
44import com.android.internal.app.IVoiceInteractor;
45import com.android.internal.app.IVoiceInteractorCallback;
46import com.android.internal.app.IVoiceInteractorRequest;
47import com.android.internal.os.HandlerCaller;
48import com.android.internal.os.SomeArgs;
49
50import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
51import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
52
53public abstract class VoiceInteractionSession implements KeyEvent.Callback {
54    static final String TAG = "VoiceInteractionSession";
55    static final boolean DEBUG = true;
56
57    final Context mContext;
58    final HandlerCaller mHandlerCaller;
59
60    final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState();
61
62    IVoiceInteractionManagerService mSystemService;
63    IBinder mToken;
64
65    int mTheme = 0;
66    LayoutInflater mInflater;
67    TypedArray mThemeAttrs;
68    View mRootView;
69    FrameLayout mContentFrame;
70    SoftInputWindow mWindow;
71
72    boolean mInitialized;
73    boolean mWindowAdded;
74    boolean mWindowVisible;
75    boolean mWindowWasVisible;
76    boolean mInShowWindow;
77
78    final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<IBinder, Request>();
79
80    final Insets mTmpInsets = new Insets();
81    final int[] mTmpLocation = new int[2];
82
83    final IVoiceInteractor mInteractor = new IVoiceInteractor.Stub() {
84        @Override
85        public IVoiceInteractorRequest startConfirmation(String callingPackage,
86                IVoiceInteractorCallback callback, String prompt, Bundle extras) {
87            Request request = findRequest(callback, true);
88            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOOOO(MSG_START_CONFIRMATION,
89                    new Caller(callingPackage, Binder.getCallingUid()), request,
90                    prompt, extras));
91            return request.mInterface;
92        }
93
94        @Override
95        public IVoiceInteractorRequest startCommand(String callingPackage,
96                IVoiceInteractorCallback callback, String command, Bundle extras) {
97            Request request = findRequest(callback, true);
98            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOOOO(MSG_START_COMMAND,
99                    new Caller(callingPackage, Binder.getCallingUid()), request,
100                    command, extras));
101            return request.mInterface;
102        }
103
104        @Override
105        public boolean[] supportsCommands(String callingPackage, String[] commands) {
106            Message msg = mHandlerCaller.obtainMessageIOO(MSG_SUPPORTS_COMMANDS,
107                    0, new Caller(callingPackage, Binder.getCallingUid()), commands);
108            SomeArgs args = mHandlerCaller.sendMessageAndWait(msg);
109            if (args != null) {
110                boolean[] res = (boolean[])args.arg1;
111                args.recycle();
112                return res;
113            }
114            return new boolean[commands.length];
115        }
116    };
117
118    final IVoiceInteractionSession mSession = new IVoiceInteractionSession.Stub() {
119        @Override
120        public void taskStarted(Intent intent, int taskId) {
121            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIO(MSG_TASK_STARTED,
122                    taskId, intent));
123        }
124
125        @Override
126        public void taskFinished(Intent intent, int taskId) {
127            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIO(MSG_TASK_FINISHED,
128                    taskId, intent));
129        }
130
131        @Override
132        public void closeSystemDialogs() {
133            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_CLOSE_SYSTEM_DIALOGS));
134        }
135
136        @Override
137        public void destroy() {
138            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_DESTROY));
139        }
140    };
141
142    public static class Request {
143        final IVoiceInteractorRequest mInterface = new IVoiceInteractorRequest.Stub() {
144            @Override
145            public void cancel() throws RemoteException {
146                mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_CANCEL, Request.this));
147            }
148        };
149        final IVoiceInteractorCallback mCallback;
150        final HandlerCaller mHandlerCaller;
151        Request(IVoiceInteractorCallback callback, HandlerCaller handlerCaller) {
152            mCallback = callback;
153            mHandlerCaller = handlerCaller;
154        }
155
156        public void sendConfirmResult(boolean confirmed, Bundle result) {
157            try {
158                if (DEBUG) Log.d(TAG, "sendConfirmResult: req=" + mInterface
159                        + " confirmed=" + confirmed + " result=" + result);
160                mCallback.deliverConfirmationResult(mInterface, confirmed, result);
161            } catch (RemoteException e) {
162            }
163        }
164
165        public void sendCommandResult(boolean complete, Bundle result) {
166            try {
167                if (DEBUG) Log.d(TAG, "sendCommandResult: req=" + mInterface
168                        + " result=" + result);
169                mCallback.deliverCommandResult(mInterface, complete, result);
170            } catch (RemoteException e) {
171            }
172        }
173
174        public void sendCancelResult() {
175            try {
176                if (DEBUG) Log.d(TAG, "sendCancelResult: req=" + mInterface);
177                mCallback.deliverCancel(mInterface);
178            } catch (RemoteException e) {
179            }
180        }
181    }
182
183    public static class Caller {
184        final String packageName;
185        final int uid;
186
187        Caller(String _packageName, int _uid) {
188            packageName = _packageName;
189            uid = _uid;
190        }
191    }
192
193    static final int MSG_START_CONFIRMATION = 1;
194    static final int MSG_START_COMMAND = 2;
195    static final int MSG_SUPPORTS_COMMANDS = 3;
196    static final int MSG_CANCEL = 4;
197
198    static final int MSG_TASK_STARTED = 100;
199    static final int MSG_TASK_FINISHED = 101;
200    static final int MSG_CLOSE_SYSTEM_DIALOGS = 102;
201    static final int MSG_DESTROY = 103;
202
203    class MyCallbacks implements HandlerCaller.Callback, SoftInputWindow.Callback {
204        @Override
205        public void executeMessage(Message msg) {
206            SomeArgs args;
207            switch (msg.what) {
208                case MSG_START_CONFIRMATION:
209                    args = (SomeArgs)msg.obj;
210                    if (DEBUG) Log.d(TAG, "onConfirm: req=" + ((Request) args.arg2).mInterface
211                            + " prompt=" + args.arg3 + " extras=" + args.arg4);
212                    onConfirm((Caller)args.arg1, (Request)args.arg2, (String)args.arg3,
213                            (Bundle)args.arg4);
214                    break;
215                case MSG_START_COMMAND:
216                    args = (SomeArgs)msg.obj;
217                    if (DEBUG) Log.d(TAG, "onCommand: req=" + ((Request) args.arg2).mInterface
218                            + " command=" + args.arg3 + " extras=" + args.arg4);
219                    onCommand((Caller) args.arg1, (Request) args.arg2, (String) args.arg3,
220                            (Bundle) args.arg4);
221                    break;
222                case MSG_SUPPORTS_COMMANDS:
223                    args = (SomeArgs)msg.obj;
224                    if (DEBUG) Log.d(TAG, "onGetSupportedCommands: cmds=" + args.arg2);
225                    args.arg1 = onGetSupportedCommands((Caller) args.arg1, (String[]) args.arg2);
226                    break;
227                case MSG_CANCEL:
228                    args = (SomeArgs)msg.obj;
229                    if (DEBUG) Log.d(TAG, "onCancel: req=" + ((Request) args.arg1).mInterface);
230                    onCancel((Request)args.arg1);
231                    break;
232                case MSG_TASK_STARTED:
233                    if (DEBUG) Log.d(TAG, "onTaskStarted: intent=" + msg.obj
234                            + " taskId=" + msg.arg1);
235                    onTaskStarted((Intent) msg.obj, msg.arg1);
236                    break;
237                case MSG_TASK_FINISHED:
238                    if (DEBUG) Log.d(TAG, "onTaskFinished: intent=" + msg.obj
239                            + " taskId=" + msg.arg1);
240                    onTaskFinished((Intent) msg.obj, msg.arg1);
241                    break;
242                case MSG_CLOSE_SYSTEM_DIALOGS:
243                    if (DEBUG) Log.d(TAG, "onCloseSystemDialogs");
244                    onCloseSystemDialogs();
245                    break;
246                case MSG_DESTROY:
247                    if (DEBUG) Log.d(TAG, "doDestroy");
248                    doDestroy();
249                    break;
250            }
251        }
252
253        @Override
254        public void onBackPressed() {
255            VoiceInteractionSession.this.onBackPressed();
256        }
257    }
258
259    final MyCallbacks mCallbacks = new MyCallbacks();
260
261    /**
262     * Information about where interesting parts of the input method UI appear.
263     */
264    public static final class Insets {
265        /**
266         * This is the part of the UI that is the main content.  It is
267         * used to determine the basic space needed, to resize/pan the
268         * application behind.  It is assumed that this inset does not
269         * change very much, since any change will cause a full resize/pan
270         * of the application behind.  This value is relative to the top edge
271         * of the input method window.
272         */
273        public final Rect contentInsets = new Rect();
274
275        /**
276         * This is the region of the UI that is touchable.  It is used when
277         * {@link #touchableInsets} is set to {@link #TOUCHABLE_INSETS_REGION}.
278         * The region should be specified relative to the origin of the window frame.
279         */
280        public final Region touchableRegion = new Region();
281
282        /**
283         * Option for {@link #touchableInsets}: the entire window frame
284         * can be touched.
285         */
286        public static final int TOUCHABLE_INSETS_FRAME
287                = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME;
288
289        /**
290         * Option for {@link #touchableInsets}: the area inside of
291         * the content insets can be touched.
292         */
293        public static final int TOUCHABLE_INSETS_CONTENT
294                = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT;
295
296        /**
297         * Option for {@link #touchableInsets}: the region specified by
298         * {@link #touchableRegion} can be touched.
299         */
300        public static final int TOUCHABLE_INSETS_REGION
301                = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
302
303        /**
304         * Determine which area of the window is touchable by the user.  May
305         * be one of: {@link #TOUCHABLE_INSETS_FRAME},
306         * {@link #TOUCHABLE_INSETS_CONTENT}, or {@link #TOUCHABLE_INSETS_REGION}.
307         */
308        public int touchableInsets;
309    }
310
311    final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
312            new ViewTreeObserver.OnComputeInternalInsetsListener() {
313        public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
314            onComputeInsets(mTmpInsets);
315            info.contentInsets.set(mTmpInsets.contentInsets);
316            info.visibleInsets.set(mTmpInsets.contentInsets);
317            info.touchableRegion.set(mTmpInsets.touchableRegion);
318            info.setTouchableInsets(mTmpInsets.touchableInsets);
319        }
320    };
321
322    public VoiceInteractionSession(Context context) {
323        this(context, new Handler());
324    }
325
326    public VoiceInteractionSession(Context context, Handler handler) {
327        mContext = context;
328        mHandlerCaller = new HandlerCaller(context, handler.getLooper(),
329                mCallbacks, true);
330    }
331
332    Request findRequest(IVoiceInteractorCallback callback, boolean newRequest) {
333        synchronized (this) {
334            Request req = mActiveRequests.get(callback.asBinder());
335            if (req != null) {
336                if (newRequest) {
337                    throw new IllegalArgumentException("Given request callback " + callback
338                            + " is already active");
339                }
340                return req;
341            }
342            req = new Request(callback, mHandlerCaller);
343            mActiveRequests.put(callback.asBinder(), req);
344            return req;
345        }
346    }
347
348    void doCreate(IVoiceInteractionManagerService service, IBinder token, Bundle args) {
349        mSystemService = service;
350        mToken = token;
351        onCreate(args);
352    }
353
354    void doDestroy() {
355        if (mInitialized) {
356            mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener(
357                    mInsetsComputer);
358            if (mWindowAdded) {
359                mWindow.dismiss();
360                mWindowAdded = false;
361            }
362            mInitialized = false;
363        }
364    }
365
366    void initViews() {
367        mInitialized = true;
368
369        mThemeAttrs = mContext.obtainStyledAttributes(android.R.styleable.VoiceInteractionSession);
370        mRootView = mInflater.inflate(
371                com.android.internal.R.layout.voice_interaction_session, null);
372        mRootView.setSystemUiVisibility(
373                View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
374        mWindow.setContentView(mRootView);
375        mRootView.getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsComputer);
376
377        mContentFrame = (FrameLayout)mRootView.findViewById(android.R.id.content);
378    }
379
380    public void showWindow() {
381        if (DEBUG) Log.v(TAG, "Showing window: mWindowAdded=" + mWindowAdded
382                + " mWindowVisible=" + mWindowVisible);
383
384        if (mInShowWindow) {
385            Log.w(TAG, "Re-entrance in to showWindow");
386            return;
387        }
388
389        try {
390            mInShowWindow = true;
391            if (!mWindowVisible) {
392                mWindowVisible = true;
393                if (!mWindowAdded) {
394                    mWindowAdded = true;
395                    View v = onCreateContentView();
396                    if (v != null) {
397                        setContentView(v);
398                    }
399                }
400                mWindow.show();
401            }
402        } finally {
403            mWindowWasVisible = true;
404            mInShowWindow = false;
405        }
406    }
407
408    public void hideWindow() {
409        if (mWindowVisible) {
410            mWindow.hide();
411            mWindowVisible = false;
412        }
413    }
414
415    /**
416     * You can call this to customize the theme used by your IME's window.
417     * This must be set before {@link #onCreate}, so you
418     * will typically call it in your constructor with the resource ID
419     * of your custom theme.
420     */
421    public void setTheme(int theme) {
422        if (mWindow != null) {
423            throw new IllegalStateException("Must be called before onCreate()");
424        }
425        mTheme = theme;
426    }
427
428    public void startVoiceActivity(Intent intent) {
429        if (mToken == null) {
430            throw new IllegalStateException("Can't call before onCreate()");
431        }
432        try {
433            intent.migrateExtraStreamToClipData();
434            intent.prepareToLeaveProcess();
435            int res = mSystemService.startVoiceActivity(mToken, intent,
436                    intent.resolveType(mContext.getContentResolver()));
437            Instrumentation.checkStartActivityResult(res, intent);
438        } catch (RemoteException e) {
439        }
440    }
441
442    public LayoutInflater getLayoutInflater() {
443        return mInflater;
444    }
445
446    public Dialog getWindow() {
447        return mWindow;
448    }
449
450    public void finish() {
451        if (mToken == null) {
452            throw new IllegalStateException("Can't call before onCreate()");
453        }
454        hideWindow();
455        try {
456            mSystemService.finish(mToken);
457        } catch (RemoteException e) {
458        }
459    }
460
461    public void onCreate(Bundle args) {
462        mTheme = mTheme != 0 ? mTheme
463                : com.android.internal.R.style.Theme_DeviceDefault_VoiceInteractionSession;
464        mInflater = (LayoutInflater)mContext.getSystemService(
465                Context.LAYOUT_INFLATER_SERVICE);
466        mWindow = new SoftInputWindow(mContext, "VoiceInteractionSession", mTheme,
467                mCallbacks, this, mDispatcherState,
468                WindowManager.LayoutParams.TYPE_VOICE_INTERACTION, Gravity.TOP, true);
469        mWindow.getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
470        initViews();
471        mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT);
472        mWindow.setToken(mToken);
473    }
474
475    public void onDestroy() {
476    }
477
478    public View onCreateContentView() {
479        return null;
480    }
481
482    public void setContentView(View view) {
483        mContentFrame.removeAllViews();
484        mContentFrame.addView(view, new FrameLayout.LayoutParams(
485                ViewGroup.LayoutParams.MATCH_PARENT,
486                ViewGroup.LayoutParams.WRAP_CONTENT));
487
488    }
489
490    public boolean onKeyDown(int keyCode, KeyEvent event) {
491        return false;
492    }
493
494    public boolean onKeyLongPress(int keyCode, KeyEvent event) {
495        return false;
496    }
497
498    public boolean onKeyUp(int keyCode, KeyEvent event) {
499        return false;
500    }
501
502    public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
503        return false;
504    }
505
506    public void onBackPressed() {
507        finish();
508    }
509
510    public void onCloseSystemDialogs() {
511        finish();
512    }
513
514    /**
515     * Compute the interesting insets into your UI.  The default implementation
516     * uses the entire window frame as the insets.  The default touchable
517     * insets are {@link Insets#TOUCHABLE_INSETS_FRAME}.
518     *
519     * @param outInsets Fill in with the current UI insets.
520     */
521    public void onComputeInsets(Insets outInsets) {
522        int[] loc = mTmpLocation;
523        View decor = getWindow().getWindow().getDecorView();
524        decor.getLocationInWindow(loc);
525        outInsets.contentInsets.top = 0;
526        outInsets.contentInsets.left = 0;
527        outInsets.contentInsets.right = 0;
528        outInsets.contentInsets.bottom = 0;
529        outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_FRAME;
530        outInsets.touchableRegion.setEmpty();
531    }
532
533    public void onTaskStarted(Intent intent, int taskId) {
534    }
535
536    public void onTaskFinished(Intent intent, int taskId) {
537        finish();
538    }
539
540    public abstract boolean[] onGetSupportedCommands(Caller caller, String[] commands);
541    public abstract void onConfirm(Caller caller, Request request, String prompt, Bundle extras);
542    public abstract void onCommand(Caller caller, Request request, String command, Bundle extras);
543    public abstract void onCancel(Request request);
544}
545