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