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