/** * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.service.voice; import android.annotation.Nullable; import android.app.Dialog; import android.app.Instrumentation; import android.app.VoiceInteractor; import android.app.assist.AssistContent; import android.app.assist.AssistStructure; import android.content.ComponentCallbacks2; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.Region; import android.inputmethodservice.SoftInputWindow; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; import android.util.DebugUtils; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.widget.FrameLayout; import com.android.internal.app.IVoiceInteractionManagerService; import com.android.internal.app.IVoiceInteractionSessionShowCallback; import com.android.internal.app.IVoiceInteractor; import com.android.internal.app.IVoiceInteractorCallback; import com.android.internal.app.IVoiceInteractorRequest; import com.android.internal.os.HandlerCaller; import com.android.internal.os.SomeArgs; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; /** * An active voice interaction session, providing a facility for the implementation * to interact with the user in the voice interaction layer. The user interface is * initially shown by default, and can be created be overriding {@link #onCreateContentView()} * in which the UI can be built. * *
A voice interaction session can be self-contained, ultimately calling {@link #finish} * when done. It can also initiate voice interactions with applications by calling * {@link #startVoiceActivity}
. */ public class VoiceInteractionSession implements KeyEvent.Callback, ComponentCallbacks2 { static final String TAG = "VoiceInteractionSession"; static final boolean DEBUG = false; /** * Flag received in {@link #onShow}: originator requested that the session be started with * assist data from the currently focused activity. */ public static final int SHOW_WITH_ASSIST = 1<<0; /** * Flag received in {@link #onShow}: originator requested that the session be started with * a screen shot of the currently focused activity. */ public static final int SHOW_WITH_SCREENSHOT = 1<<1; /** * Flag for use with {@link #onShow}: indicates that the session has been started from the * system assist gesture. */ public static final int SHOW_SOURCE_ASSIST_GESTURE = 1<<2; /** * Flag for use with {@link #onShow}: indicates that the application itself has invoked * the assistant. */ public static final int SHOW_SOURCE_APPLICATION = 1<<3; final Context mContext; final HandlerCaller mHandlerCaller; final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState(); IVoiceInteractionManagerService mSystemService; IBinder mToken; int mTheme = 0; LayoutInflater mInflater; TypedArray mThemeAttrs; View mRootView; FrameLayout mContentFrame; SoftInputWindow mWindow; boolean mInitialized; boolean mWindowAdded; boolean mWindowVisible; boolean mWindowWasVisible; boolean mInShowWindow; final ArrayMapAs the voice activity runs, it can retrieve a {@link android.app.VoiceInteractor} * through which it can perform voice interactions through your session. These requests * for voice interactions will appear as callbacks on {@link #onGetSupportedCommands}, * {@link #onRequestConfirmation}, {@link #onRequestPickOption}, * {@link #onRequestCompleteVoice}, {@link #onRequestAbortVoice}, * or {@link #onRequestCommand} * *
You will receive a call to {@link #onTaskStarted} when the task starts up * and {@link #onTaskFinished} when the last activity has finished. * * @param intent The Intent to start this voice interaction. The given Intent will * always have {@link Intent#CATEGORY_VOICE Intent.CATEGORY_VOICE} added to it, since * this is part of a voice interaction. */ public void startVoiceActivity(Intent intent) { if (mToken == null) { throw new IllegalStateException("Can't call before onCreate()"); } try { intent.migrateExtraStreamToClipData(); intent.prepareToLeaveProcess(); int res = mSystemService.startVoiceActivity(mToken, intent, intent.resolveType(mContext.getContentResolver())); Instrumentation.checkStartActivityResult(res, intent); } catch (RemoteException e) { } } /** * Set whether this session will keep the device awake while it is running a voice * activity. By default, the system holds a wake lock for it while in this state, * so that it can work even if the screen is off. Setting this to false removes that * wake lock, allowing the CPU to go to sleep. This is typically used if the * session decides it has been waiting too long for a response from the user and * doesn't want to let this continue to drain the battery. * *
Passing false here will release the wake lock, and you can call later with * true to re-acquire it. It will also be automatically re-acquired for you each * time you start a new voice activity task -- that is when you call * {@link #startVoiceActivity}.
*/ public void setKeepAwake(boolean keepAwake) { if (mToken == null) { throw new IllegalStateException("Can't call before onCreate()"); } try { mSystemService.setKeepAwake(mToken, keepAwake); } catch (RemoteException e) { } } /** * Request that all system dialogs (and status bar shade etc) be closed, allowing * access to the session's UI. This will not cause the lock screen to be * dismissed. */ public void closeSystemDialogs() { if (mToken == null) { throw new IllegalStateException("Can't call before onCreate()"); } try { mSystemService.closeSystemDialogs(mToken); } catch (RemoteException e) { } } /** * Convenience for inflating views. */ public LayoutInflater getLayoutInflater() { return mInflater; } /** * Retrieve the window being used to show the session's UI. */ public Dialog getWindow() { return mWindow; } /** * Finish the session. This completely destroys the session -- the next time it is shown, * an entirely new one will be created. You do not normally call this function; instead, * use {@link #hide} and allow the system to destroy your session if it needs its RAM. */ public void finish() { if (mToken == null) { throw new IllegalStateException("Can't call before onCreate()"); } try { mSystemService.finish(mToken); } catch (RemoteException e) { } } /** * Initiatize a new session. At this point you don't know exactly what this * session will be used for; you will find that out in {@link #onShow}. */ public void onCreate() { doOnCreate(); } private void doOnCreate() { mTheme = mTheme != 0 ? mTheme : com.android.internal.R.style.Theme_DeviceDefault_VoiceInteractionSession; mInflater = (LayoutInflater)mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); mWindow = new SoftInputWindow(mContext, "VoiceInteractionSession", mTheme, mCallbacks, this, mDispatcherState, WindowManager.LayoutParams.TYPE_VOICE_INTERACTION, Gravity.BOTTOM, true); mWindow.getWindow().addFlags( WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); initViews(); mWindow.getWindow().setLayout(MATCH_PARENT, MATCH_PARENT); mWindow.setToken(mToken); } /** * Called when the session UI is going to be shown. This is called after * {@link #onCreateContentView} (if the session's content UI needed to be created) and * immediately prior to the window being shown. This may be called while the window * is already shown, if a show request has come in while it is shown, to allow you to * update the UI to match the new show arguments. * * @param args The arguments that were supplied to * {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}. * @param showFlags The show flags originally provided to * {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}. */ public void onShow(Bundle args, int showFlags) { } /** * Called immediately after stopping to show the session UI. */ public void onHide() { } /** * Last callback to the session as it is being finished. */ public void onDestroy() { } /** * Hook in which to create the session's UI. */ public View onCreateContentView() { return null; } public void setContentView(View view) { mContentFrame.removeAllViews(); mContentFrame.addView(view, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); mContentFrame.requestApplyInsets(); } void doOnHandleAssist(Bundle data, AssistStructure structure, Throwable failure, AssistContent content) { if (failure != null) { onAssistStructureFailure(failure); } onHandleAssist(data, structure, content); } /** * Called when there has been a failure transferring the {@link AssistStructure} to * the assistant. This may happen, for example, if the data is too large and results * in an out of memory exception, or the client has provided corrupt data. This will * be called immediately before {@link #onHandleAssist} and the AssistStructure supplied * there afterwards will be null. * * @param failure The failure exception that was thrown when building the * {@link AssistStructure}. */ public void onAssistStructureFailure(Throwable failure) { } /** * Called to receive data from the application that the user was currently viewing when * an assist session is started. If the original show request did not specify * {@link #SHOW_WITH_ASSIST}, this method will not be called. * * @param data Arbitrary data supplied by the app through * {@link android.app.Activity#onProvideAssistData Activity.onProvideAssistData}. * May be null if assist data has been disabled by the user or device policy. * @param structure If available, the structure definition of all windows currently * displayed by the app. May be null if assist data has been disabled by the user * or device policy; will be an empty stub if the application has disabled assist * by marking its window as secure. * @param content Additional content data supplied by the app through * {@link android.app.Activity#onProvideAssistContent Activity.onProvideAssistContent}. * May be null if assist data has been disabled by the user or device policy; will * not be automatically filled in with data from the app if the app has marked its * window as secure. */ public void onHandleAssist(@Nullable Bundle data, @Nullable AssistStructure structure, @Nullable AssistContent content) { } /** * Called to receive a screenshot of what the user was currently viewing when an assist * session is started. May be null if screenshots are disabled by the user, policy, * or application. If the original show request did not specify * {@link #SHOW_WITH_SCREENSHOT}, this method will not be called. */ public void onHandleScreenshot(@Nullable Bitmap screenshot) { } public boolean onKeyDown(int keyCode, KeyEvent event) { return false; } public boolean onKeyLongPress(int keyCode, KeyEvent event) { return false; } public boolean onKeyUp(int keyCode, KeyEvent event) { return false; } public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { return false; } /** * Called when the user presses the back button while focus is in the session UI. Note * that this will only happen if the session UI has requested input focus in its window; * otherwise, the back key will go to whatever window has focus and do whatever behavior * it normally has there. The default implementation simply calls {@link #hide}. */ public void onBackPressed() { hide(); } /** * Sessions automatically watch for requests that all system UI be closed (such as when * the user presses HOME), which will appear here. The default implementation always * calls {@link #hide}. */ public void onCloseSystemDialogs() { hide(); } /** * Called when the lockscreen was shown. */ public void onLockscreenShown() { hide(); } @Override public void onConfigurationChanged(Configuration newConfig) { } @Override public void onLowMemory() { } @Override public void onTrimMemory(int level) { } /** * Compute the interesting insets into your UI. The default implementation * sets {@link Insets#contentInsets outInsets.contentInsets.top} to the height * of the window, meaning it should not adjust content underneath. The default touchable * insets are {@link Insets#TOUCHABLE_INSETS_FRAME}, meaning it consumes all touch * events within its window frame. * * @param outInsets Fill in with the current UI insets. */ public void onComputeInsets(Insets outInsets) { outInsets.contentInsets.left = 0; outInsets.contentInsets.bottom = 0; outInsets.contentInsets.right = 0; View decor = getWindow().getWindow().getDecorView(); outInsets.contentInsets.top = decor.getHeight(); outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_FRAME; outInsets.touchableRegion.setEmpty(); } /** * Called when a task initiated by {@link #startVoiceActivity(android.content.Intent)} * has actually started. * * @param intent The original {@link Intent} supplied to * {@link #startVoiceActivity(android.content.Intent)}. * @param taskId Unique ID of the now running task. */ public void onTaskStarted(Intent intent, int taskId) { } /** * Called when the last activity of a task initiated by * {@link #startVoiceActivity(android.content.Intent)} has finished. The default * implementation calls {@link #finish()} on the assumption that this represents * the completion of a voice action. You can override the implementation if you would * like a different behavior. * * @param intent The original {@link Intent} supplied to * {@link #startVoiceActivity(android.content.Intent)}. * @param taskId Unique ID of the finished task. */ public void onTaskFinished(Intent intent, int taskId) { hide(); } /** * Request to query for what extended commands the session supports. * * @param commands An array of commands that are being queried. * @return Return an array of booleans indicating which of each entry in the * command array is supported. A true entry in the array indicates the command * is supported; false indicates it is not. The default implementation returns * an array of all false entries. */ public boolean[] onGetSupportedCommands(String[] commands) { return new boolean[commands.length]; } /** * Request to confirm with the user before proceeding with an unrecoverable operation, * corresponding to a {@link android.app.VoiceInteractor.ConfirmationRequest * VoiceInteractor.ConfirmationRequest}. * * @param request The active request. */ public void onRequestConfirmation(ConfirmationRequest request) { } /** * Request for the user to pick one of N options, corresponding to a * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}. * * @param request The active request. */ public void onRequestPickOption(PickOptionRequest request) { } /** * Request to complete the voice interaction session because the voice activity successfully * completed its interaction using voice. Corresponds to * {@link android.app.VoiceInteractor.CompleteVoiceRequest * VoiceInteractor.CompleteVoiceRequest}. The default implementation just sends an empty * confirmation back to allow the activity to exit. * * @param request The active request. */ public void onRequestCompleteVoice(CompleteVoiceRequest request) { } /** * Request to abort the voice interaction session because the voice activity can not * complete its interaction using voice. Corresponds to * {@link android.app.VoiceInteractor.AbortVoiceRequest * VoiceInteractor.AbortVoiceRequest}. The default implementation just sends an empty * confirmation back to allow the activity to exit. * * @param request The active request. */ public void onRequestAbortVoice(AbortVoiceRequest request) { } /** * Process an arbitrary extended command from the caller, * corresponding to a {@link android.app.VoiceInteractor.CommandRequest * VoiceInteractor.CommandRequest}. * * @param request The active request. */ public void onRequestCommand(CommandRequest request) { } /** * Called when the {@link android.app.VoiceInteractor} has asked to cancel a {@link Request} * that was previously delivered to {@link #onRequestConfirmation}, * {@link #onRequestPickOption}, {@link #onRequestCompleteVoice}, {@link #onRequestAbortVoice}, * or {@link #onRequestCommand}. * * @param request The request that is being canceled. */ public void onCancelRequest(Request request) { } /** * Print the Service's state into the given stream. This gets invoked by * {@link VoiceInteractionSessionService} when its Service * {@link android.app.Service#dump} method is called. * * @param prefix Text to print at the front of each line. * @param fd The raw file descriptor that the dump is being sent to. * @param writer The PrintWriter to which you should dump your state. This will be * closed for you after you return. * @param args additional arguments to the dump request. */ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { writer.print(prefix); writer.print("mToken="); writer.println(mToken); writer.print(prefix); writer.print("mTheme=#"); writer.println(Integer.toHexString(mTheme)); writer.print(prefix); writer.print("mInitialized="); writer.println(mInitialized); writer.print(prefix); writer.print("mWindowAdded="); writer.print(mWindowAdded); writer.print(" mWindowVisible="); writer.println(mWindowVisible); writer.print(prefix); writer.print("mWindowWasVisible="); writer.print(mWindowWasVisible); writer.print(" mInShowWindow="); writer.println(mInShowWindow); if (mActiveRequests.size() > 0) { writer.print(prefix); writer.println("Active requests:"); String innerPrefix = prefix + " "; for (int i=0; i