Home | History | Annotate | Download | only in app
      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 
     17 package android.app;
     18 
     19 import android.annotation.SystemApi;
     20 import android.content.Context;
     21 import android.os.Bundle;
     22 import android.os.IBinder;
     23 import android.os.Looper;
     24 import android.os.Message;
     25 import android.os.RemoteException;
     26 import android.util.ArrayMap;
     27 import android.util.Log;
     28 import com.android.internal.app.IVoiceInteractor;
     29 import com.android.internal.app.IVoiceInteractorCallback;
     30 import com.android.internal.app.IVoiceInteractorRequest;
     31 import com.android.internal.os.HandlerCaller;
     32 import com.android.internal.os.SomeArgs;
     33 
     34 import java.util.ArrayList;
     35 
     36 /**
     37  * @hide
     38  * Interface for an {@link Activity} to interact with the user through voice.  Use
     39  * {@link android.app.Activity#getVoiceInteractor() Activity.getVoiceInteractor}
     40  * to retrieve the interface, if the activity is currently involved in a voice interaction.
     41  *
     42  * <p>The voice interactor revolves around submitting voice interaction requests to the
     43  * back-end voice interaction service that is working with the user.  These requests are
     44  * submitted with {@link #submitRequest}, providing a new instance of a
     45  * {@link Request} subclass describing the type of operation to perform -- currently the
     46  * possible requests are {@link ConfirmationRequest} and {@link CommandRequest}.
     47  *
     48  * <p>Once a request is submitted, the voice system will process it and eventually deliver
     49  * the result to the request object.  The application can cancel a pending request at any
     50  * time.
     51  *
     52  * <p>The VoiceInteractor is integrated with Activity's state saving mechanism, so that
     53  * if an activity is being restarted with retained state, it will retain the current
     54  * VoiceInteractor and any outstanding requests.  Because of this, you should always use
     55  * {@link Request#getActivity() Request.getActivity} to get back to the activity of a
     56  * request, rather than holding on to the activity instance yourself, either explicitly
     57  * or implicitly through a non-static inner class.
     58  */
     59 @SystemApi
     60 public class VoiceInteractor {
     61     static final String TAG = "VoiceInteractor";
     62     static final boolean DEBUG = true;
     63 
     64     final IVoiceInteractor mInteractor;
     65 
     66     Context mContext;
     67     Activity mActivity;
     68 
     69     final HandlerCaller mHandlerCaller;
     70     final HandlerCaller.Callback mHandlerCallerCallback = new HandlerCaller.Callback() {
     71         @Override
     72         public void executeMessage(Message msg) {
     73             SomeArgs args = (SomeArgs)msg.obj;
     74             Request request;
     75             switch (msg.what) {
     76                 case MSG_CONFIRMATION_RESULT:
     77                     request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
     78                     if (DEBUG) Log.d(TAG, "onConfirmResult: req="
     79                             + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
     80                             + " confirmed=" + msg.arg1 + " result=" + args.arg2);
     81                     if (request != null) {
     82                         ((ConfirmationRequest)request).onConfirmationResult(msg.arg1 != 0,
     83                                 (Bundle) args.arg2);
     84                         request.clear();
     85                     }
     86                     break;
     87                 case MSG_COMPLETE_VOICE_RESULT:
     88                     request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
     89                     if (DEBUG) Log.d(TAG, "onCompleteVoice: req="
     90                             + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
     91                             + " result=" + args.arg1);
     92                     if (request != null) {
     93                         ((CompleteVoiceRequest)request).onCompleteResult((Bundle) args.arg2);
     94                         request.clear();
     95                     }
     96                     break;
     97                 case MSG_ABORT_VOICE_RESULT:
     98                     request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
     99                     if (DEBUG) Log.d(TAG, "onAbortVoice: req="
    100                             + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
    101                             + " result=" + args.arg1);
    102                     if (request != null) {
    103                         ((AbortVoiceRequest)request).onAbortResult((Bundle) args.arg2);
    104                         request.clear();
    105                     }
    106                     break;
    107                 case MSG_COMMAND_RESULT:
    108                     request = pullRequest((IVoiceInteractorRequest)args.arg1, msg.arg1 != 0);
    109                     if (DEBUG) Log.d(TAG, "onCommandResult: req="
    110                             + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
    111                             + " result=" + args.arg2);
    112                     if (request != null) {
    113                         ((CommandRequest)request).onCommandResult((Bundle) args.arg2);
    114                         if (msg.arg1 != 0) {
    115                             request.clear();
    116                         }
    117                     }
    118                     break;
    119                 case MSG_CANCEL_RESULT:
    120                     request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
    121                     if (DEBUG) Log.d(TAG, "onCancelResult: req="
    122                             + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request);
    123                     if (request != null) {
    124                         request.onCancel();
    125                         request.clear();
    126                     }
    127                     break;
    128             }
    129         }
    130     };
    131 
    132     final IVoiceInteractorCallback.Stub mCallback = new IVoiceInteractorCallback.Stub() {
    133         @Override
    134         public void deliverConfirmationResult(IVoiceInteractorRequest request, boolean confirmed,
    135                 Bundle result) {
    136             mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(
    137                     MSG_CONFIRMATION_RESULT, confirmed ? 1 : 0, request, result));
    138         }
    139 
    140         @Override
    141         public void deliverCompleteVoiceResult(IVoiceInteractorRequest request, Bundle result) {
    142             mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(
    143                     MSG_COMPLETE_VOICE_RESULT, request, result));
    144         }
    145 
    146         @Override
    147         public void deliverAbortVoiceResult(IVoiceInteractorRequest request, Bundle result) {
    148             mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(
    149                     MSG_ABORT_VOICE_RESULT, request, result));
    150         }
    151 
    152         @Override
    153         public void deliverCommandResult(IVoiceInteractorRequest request, boolean complete,
    154                 Bundle result) {
    155             mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(
    156                     MSG_COMMAND_RESULT, complete ? 1 : 0, request, result));
    157         }
    158 
    159         @Override
    160         public void deliverCancel(IVoiceInteractorRequest request) throws RemoteException {
    161             mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(
    162                     MSG_CANCEL_RESULT, request));
    163         }
    164     };
    165 
    166     final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<IBinder, Request>();
    167 
    168     static final int MSG_CONFIRMATION_RESULT = 1;
    169     static final int MSG_COMPLETE_VOICE_RESULT = 2;
    170     static final int MSG_ABORT_VOICE_RESULT = 3;
    171     static final int MSG_COMMAND_RESULT = 4;
    172     static final int MSG_CANCEL_RESULT = 5;
    173 
    174     public static abstract class Request {
    175         IVoiceInteractorRequest mRequestInterface;
    176         Context mContext;
    177         Activity mActivity;
    178 
    179         public Request() {
    180         }
    181 
    182         public void cancel() {
    183             try {
    184                 mRequestInterface.cancel();
    185             } catch (RemoteException e) {
    186                 Log.w(TAG, "Voice interactor has died", e);
    187             }
    188         }
    189 
    190         public Context getContext() {
    191             return mContext;
    192         }
    193 
    194         public Activity getActivity() {
    195             return mActivity;
    196         }
    197 
    198         public void onCancel() {
    199         }
    200 
    201         public void onAttached(Activity activity) {
    202         }
    203 
    204         public void onDetached() {
    205         }
    206 
    207         void clear() {
    208             mRequestInterface = null;
    209             mContext = null;
    210             mActivity = null;
    211         }
    212 
    213         abstract IVoiceInteractorRequest submit(IVoiceInteractor interactor,
    214                 String packageName, IVoiceInteractorCallback callback) throws RemoteException;
    215     }
    216 
    217     public static class ConfirmationRequest extends Request {
    218         final CharSequence mPrompt;
    219         final Bundle mExtras;
    220 
    221         /**
    222          * Confirms an operation with the user via the trusted system
    223          * VoiceInteractionService.  This allows an Activity to complete an unsafe operation that
    224          * would require the user to touch the screen when voice interaction mode is not enabled.
    225          * The result of the confirmation will be returned through an asynchronous call to
    226          * either {@link #onConfirmationResult(boolean, android.os.Bundle)} or
    227          * {@link #onCancel()}.
    228          *
    229          * <p>In some cases this may be a simple yes / no confirmation or the confirmation could
    230          * include context information about how the action will be completed
    231          * (e.g. booking a cab might include details about how long until the cab arrives)
    232          * so the user can give a confirmation.
    233          * @param prompt Optional confirmation text to read to the user as the action being
    234          * confirmed.
    235          * @param extras Additional optional information.
    236          */
    237         public ConfirmationRequest(CharSequence prompt, Bundle extras) {
    238             mPrompt = prompt;
    239             mExtras = extras;
    240         }
    241 
    242         public void onConfirmationResult(boolean confirmed, Bundle result) {
    243         }
    244 
    245         IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
    246                 IVoiceInteractorCallback callback) throws RemoteException {
    247             return interactor.startConfirmation(packageName, callback, mPrompt, mExtras);
    248         }
    249     }
    250 
    251     public static class CompleteVoiceRequest extends Request {
    252         final CharSequence mMessage;
    253         final Bundle mExtras;
    254 
    255         /**
    256          * Reports that the current interaction was successfully completed with voice, so the
    257          * application can report the final status to the user. When the response comes back, the
    258          * voice system has handled the request and is ready to switch; at that point the
    259          * application can start a new non-voice activity or finish.  Be sure when starting the new
    260          * activity to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK
    261          * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice
    262          * interaction task.
    263          *
    264          * @param message Optional message to tell user about the completion status of the task.
    265          * @param extras Additional optional information.
    266          */
    267         public CompleteVoiceRequest(CharSequence message, Bundle extras) {
    268             mMessage = message;
    269             mExtras = extras;
    270         }
    271 
    272         public void onCompleteResult(Bundle result) {
    273         }
    274 
    275         IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
    276                 IVoiceInteractorCallback callback) throws RemoteException {
    277             return interactor.startCompleteVoice(packageName, callback, mMessage, mExtras);
    278         }
    279     }
    280 
    281     public static class AbortVoiceRequest extends Request {
    282         final CharSequence mMessage;
    283         final Bundle mExtras;
    284 
    285         /**
    286          * Reports that the current interaction can not be complete with voice, so the
    287          * application will need to switch to a traditional input UI.  Applications should
    288          * only use this when they need to completely bail out of the voice interaction
    289          * and switch to a traditional UI.  When the response comes back, the voice
    290          * system has handled the request and is ready to switch; at that point the application
    291          * can start a new non-voice activity.  Be sure when starting the new activity
    292          * to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK
    293          * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice
    294          * interaction task.
    295          *
    296          * @param message Optional message to tell user about not being able to complete
    297          * the interaction with voice.
    298          * @param extras Additional optional information.
    299          */
    300         public AbortVoiceRequest(CharSequence message, Bundle extras) {
    301             mMessage = message;
    302             mExtras = extras;
    303         }
    304 
    305         public void onAbortResult(Bundle result) {
    306         }
    307 
    308         IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
    309                 IVoiceInteractorCallback callback) throws RemoteException {
    310             return interactor.startAbortVoice(packageName, callback, mMessage, mExtras);
    311         }
    312     }
    313 
    314     public static class CommandRequest extends Request {
    315         final String mCommand;
    316         final Bundle mArgs;
    317 
    318         /**
    319          * Execute a command using the trusted system VoiceInteractionService.
    320          * This allows an Activity to request additional information from the user needed to
    321          * complete an action (e.g. booking a table might have several possible times that the
    322          * user could select from or an app might need the user to agree to a terms of service).
    323          * The result of the confirmation will be returned through an asynchronous call to
    324          * either {@link #onCommandResult(android.os.Bundle)} or
    325          * {@link #onCancel()}.
    326          *
    327          * <p>The command is a string that describes the generic operation to be performed.
    328          * The command will determine how the properties in extras are interpreted and the set of
    329          * available commands is expected to grow over time.  An example might be
    330          * "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number of bags as part of
    331          * airline check-in.  (This is not an actual working example.)
    332          *
    333          * @param command The desired command to perform.
    334          * @param args Additional arguments to control execution of the command.
    335          */
    336         public CommandRequest(String command, Bundle args) {
    337             mCommand = command;
    338             mArgs = args;
    339         }
    340 
    341         public void onCommandResult(Bundle result) {
    342         }
    343 
    344         IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
    345                 IVoiceInteractorCallback callback) throws RemoteException {
    346             return interactor.startCommand(packageName, callback, mCommand, mArgs);
    347         }
    348    }
    349 
    350     VoiceInteractor(IVoiceInteractor interactor, Context context, Activity activity,
    351             Looper looper) {
    352         mInteractor = interactor;
    353         mContext = context;
    354         mActivity = activity;
    355         mHandlerCaller = new HandlerCaller(context, looper, mHandlerCallerCallback, true);
    356     }
    357 
    358     Request pullRequest(IVoiceInteractorRequest request, boolean complete) {
    359         synchronized (mActiveRequests) {
    360             Request req = mActiveRequests.get(request.asBinder());
    361             if (req != null && complete) {
    362                 mActiveRequests.remove(request.asBinder());
    363             }
    364             return req;
    365         }
    366     }
    367 
    368     private ArrayList<Request> makeRequestList() {
    369         final int N = mActiveRequests.size();
    370         if (N < 1) {
    371             return null;
    372         }
    373         ArrayList<Request> list = new ArrayList<Request>(N);
    374         for (int i=0; i<N; i++) {
    375             list.add(mActiveRequests.valueAt(i));
    376         }
    377         return list;
    378     }
    379 
    380     void attachActivity(Activity activity) {
    381         if (mActivity == activity) {
    382             return;
    383         }
    384         mContext = activity;
    385         mActivity = activity;
    386         ArrayList<Request> reqs = makeRequestList();
    387         if (reqs != null) {
    388             for (int i=0; i<reqs.size(); i++) {
    389                 Request req = reqs.get(i);
    390                 req.mContext = activity;
    391                 req.mActivity = activity;
    392                 req.onAttached(activity);
    393             }
    394         }
    395     }
    396 
    397     void detachActivity() {
    398         ArrayList<Request> reqs = makeRequestList();
    399         if (reqs != null) {
    400             for (int i=0; i<reqs.size(); i++) {
    401                 Request req = reqs.get(i);
    402                 req.onDetached();
    403                 req.mActivity = null;
    404                 req.mContext = null;
    405             }
    406         }
    407         mContext = null;
    408         mActivity = null;
    409     }
    410 
    411     public boolean submitRequest(Request request) {
    412         try {
    413             IVoiceInteractorRequest ireq = request.submit(mInteractor,
    414                     mContext.getOpPackageName(), mCallback);
    415             request.mRequestInterface = ireq;
    416             request.mContext = mContext;
    417             request.mActivity = mActivity;
    418             synchronized (mActiveRequests) {
    419                 mActiveRequests.put(ireq.asBinder(), request);
    420             }
    421             return true;
    422         } catch (RemoteException e) {
    423             Log.w(TAG, "Remove voice interactor service died", e);
    424             return false;
    425         }
    426     }
    427 
    428     /**
    429      * Queries the supported commands available from the VoiceinteractionService.
    430      * The command is a string that describes the generic operation to be performed.
    431      * An example might be "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number
    432      * of bags as part of airline check-in.  (This is not an actual working example.)
    433      *
    434      * @param commands
    435      */
    436     public boolean[] supportsCommands(String[] commands) {
    437         try {
    438             boolean[] res = mInteractor.supportsCommands(mContext.getOpPackageName(), commands);
    439             if (DEBUG) Log.d(TAG, "supportsCommands: cmds=" + commands + " res=" + res);
    440             return res;
    441         } catch (RemoteException e) {
    442             throw new RuntimeException("Voice interactor has died", e);
    443         }
    444     }
    445 }
    446