VoiceInteractor.java revision dcff99598f16737e75278e8ea9dbfe92579ea234
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.app;
18
19import android.content.Context;
20import android.os.Bundle;
21import android.os.IBinder;
22import android.os.Looper;
23import android.os.Message;
24import android.os.RemoteException;
25import android.util.ArrayMap;
26import android.util.Log;
27import com.android.internal.app.IVoiceInteractor;
28import com.android.internal.app.IVoiceInteractorCallback;
29import com.android.internal.app.IVoiceInteractorRequest;
30import com.android.internal.os.HandlerCaller;
31import com.android.internal.os.SomeArgs;
32
33import java.util.WeakHashMap;
34
35/**
36 * Interface for an {@link Activity} to interact with the user through voice.
37 * @hide
38 */
39public class VoiceInteractor {
40    static final String TAG = "VoiceInteractor";
41    static final boolean DEBUG = true;
42
43    final Context mContext;
44    final Activity mActivity;
45    final IVoiceInteractor mInteractor;
46    final HandlerCaller mHandlerCaller;
47    final HandlerCaller.Callback mHandlerCallerCallback = new HandlerCaller.Callback() {
48        @Override
49        public void executeMessage(Message msg) {
50            SomeArgs args = (SomeArgs)msg.obj;
51            Request request;
52            switch (msg.what) {
53                case MSG_CONFIRMATION_RESULT:
54                    request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
55                    if (DEBUG) Log.d(TAG, "onConfirmResult: req="
56                            + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
57                            + " confirmed=" + msg.arg1 + " result=" + args.arg2);
58                    if (request != null) {
59                        ((ConfirmationRequest)request).onConfirmationResult(msg.arg1 != 0,
60                                (Bundle) args.arg2);
61                        request.clear();
62                    }
63                    break;
64                case MSG_COMMAND_RESULT:
65                    request = pullRequest((IVoiceInteractorRequest)args.arg1, msg.arg1 != 0);
66                    if (DEBUG) Log.d(TAG, "onCommandResult: req="
67                            + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
68                            + " result=" + args.arg2);
69                    if (request != null) {
70                        ((CommandRequest)request).onCommandResult((Bundle) args.arg2);
71                        if (msg.arg1 != 0) {
72                            request.clear();
73                        }
74                    }
75                    break;
76                case MSG_CANCEL_RESULT:
77                    request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
78                    if (DEBUG) Log.d(TAG, "onCancelResult: req="
79                            + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request);
80                    if (request != null) {
81                        request.onCancel();
82                        request.clear();
83                    }
84                    break;
85            }
86        }
87    };
88
89    final IVoiceInteractorCallback.Stub mCallback = new IVoiceInteractorCallback.Stub() {
90        @Override
91        public void deliverConfirmationResult(IVoiceInteractorRequest request, boolean confirmed,
92                Bundle result) {
93            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(
94                    MSG_CONFIRMATION_RESULT, confirmed ? 1 : 0, request, result));
95        }
96
97        @Override
98        public void deliverCommandResult(IVoiceInteractorRequest request, boolean complete,
99                Bundle result) {
100            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(
101                    MSG_COMMAND_RESULT, complete ? 1 : 0, request, result));
102        }
103
104        @Override
105        public void deliverCancel(IVoiceInteractorRequest request) throws RemoteException {
106            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(
107                    MSG_CANCEL_RESULT, request));
108        }
109    };
110
111    final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<IBinder, Request>();
112
113    static final int MSG_CONFIRMATION_RESULT = 1;
114    static final int MSG_COMMAND_RESULT = 2;
115    static final int MSG_CANCEL_RESULT = 3;
116
117    public static abstract class Request {
118        IVoiceInteractorRequest mRequestInterface;
119        Context mContext;
120        Activity mActivity;
121
122        public Request() {
123        }
124
125        public void cancel() {
126            try {
127                mRequestInterface.cancel();
128            } catch (RemoteException e) {
129                Log.w(TAG, "Voice interactor has died", e);
130            }
131        }
132
133        public Context getContext() {
134            return mContext;
135        }
136
137        public Activity getActivity() {
138            return mActivity;
139        }
140
141        public void onCancel() {
142        }
143
144        void clear() {
145            mRequestInterface = null;
146            mContext = null;
147            mActivity = null;
148        }
149
150        abstract IVoiceInteractorRequest submit(IVoiceInteractor interactor,
151                String packageName, IVoiceInteractorCallback callback) throws RemoteException;
152    }
153
154    public static class ConfirmationRequest extends Request {
155        final CharSequence mPrompt;
156        final Bundle mExtras;
157
158        /**
159         * Confirms an operation with the user via the trusted system
160         * VoiceInteractionService.  This allows an Activity to complete an unsafe operation that
161         * would require the user to touch the screen when voice interaction mode is not enabled.
162         * The result of the confirmation will be returned through an asynchronous call to
163         * either {@link #onConfirmationResult(boolean, android.os.Bundle)} or
164         * {@link #onCancel()}.
165         *
166         * <p>In some cases this may be a simple yes / no confirmation or the confirmation could
167         * include context information about how the action will be completed
168         * (e.g. booking a cab might include details about how long until the cab arrives)
169         * so the user can give a confirmation.
170         * @param prompt Optional confirmation text to read to the user as the action being
171         * confirmed.
172         * @param extras Additional optional information.
173         */
174        public ConfirmationRequest(CharSequence prompt, Bundle extras) {
175            mPrompt = prompt;
176            mExtras = extras;
177        }
178
179        public void onConfirmationResult(boolean confirmed, Bundle result) {
180        }
181
182        IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
183                IVoiceInteractorCallback callback) throws RemoteException {
184            return interactor.startConfirmation(packageName, callback, mPrompt.toString(), mExtras);
185        }
186   }
187
188    public static class CommandRequest extends Request {
189        final String mCommand;
190        final Bundle mArgs;
191
192        /**
193         * Execute a command using the trusted system VoiceInteractionService.
194         * This allows an Activity to request additional information from the user needed to
195         * complete an action (e.g. booking a table might have several possible times that the
196         * user could select from or an app might need the user to agree to a terms of service).
197         * The result of the confirmation will be returned through an asynchronous call to
198         * either {@link #onCommandResult(android.os.Bundle)} or
199         * {@link #onCancel()}.
200         *
201         * <p>The command is a string that describes the generic operation to be performed.
202         * The command will determine how the properties in extras are interpreted and the set of
203         * available commands is expected to grow over time.  An example might be
204         * "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number of bags as part of
205         * airline check-in.  (This is not an actual working example.)
206         *
207         * @param command The desired command to perform.
208         * @param args Additional arguments to control execution of the command.
209         */
210        public CommandRequest(String command, Bundle args) {
211            mCommand = command;
212            mArgs = args;
213        }
214
215        public void onCommandResult(Bundle result) {
216        }
217
218        IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
219                IVoiceInteractorCallback callback) throws RemoteException {
220            return interactor.startConfirmation(packageName, callback, mCommand, mArgs);
221        }
222   }
223
224    VoiceInteractor(Context context, Activity activity, IVoiceInteractor interactor,
225            Looper looper) {
226        mContext = context;
227        mActivity = activity;
228        mInteractor = interactor;
229        mHandlerCaller = new HandlerCaller(context, looper, mHandlerCallerCallback, true);
230    }
231
232    Request pullRequest(IVoiceInteractorRequest request, boolean complete) {
233        synchronized (mActiveRequests) {
234            Request req = mActiveRequests.get(request.asBinder());
235            if (req != null && complete) {
236                mActiveRequests.remove(request.asBinder());
237            }
238            return req;
239        }
240    }
241
242    public boolean submitRequest(Request request) {
243        try {
244            IVoiceInteractorRequest ireq = request.submit(mInteractor,
245                    mContext.getOpPackageName(), mCallback);
246            request.mRequestInterface = ireq;
247            request.mContext = mContext;
248            request.mActivity = mActivity;
249            synchronized (mActiveRequests) {
250                mActiveRequests.put(ireq.asBinder(), request);
251            }
252            return true;
253        } catch (RemoteException e) {
254            Log.w(TAG, "Remove voice interactor service died", e);
255            return false;
256        }
257    }
258
259    /**
260     * Queries the supported commands available from the VoiceinteractionService.
261     * The command is a string that describes the generic operation to be performed.
262     * An example might be "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number
263     * of bags as part of airline check-in.  (This is not an actual working example.)
264     *
265     * @param commands
266     */
267    public boolean[] supportsCommands(String[] commands) {
268        try {
269            boolean[] res = mInteractor.supportsCommands(mContext.getOpPackageName(), commands);
270            if (DEBUG) Log.d(TAG, "supportsCommands: cmds=" + commands + " res=" + res);
271            return res;
272        } catch (RemoteException e) {
273            throw new RuntimeException("Voice interactor has died", e);
274        }
275    }
276}
277