/** * 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 static android.view.Display.DEFAULT_DISPLAY; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.app.Activity; import android.app.ActivityOptions; import android.app.Dialog; import android.app.DirectAction; 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.pm.ParceledListSlice; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.Region; import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; import android.os.IBinder; import android.os.ICancellationSignal; import android.os.Message; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.UserHandle; import android.service.voice.flags.Flags; 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.ViewTreeObserver; import android.view.WindowManager; import android.widget.FrameLayout; import com.android.internal.annotations.Immutable; 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 com.android.internal.util.function.pooled.PooledLambda; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; /** * 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 by 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; /** * Flag for use with {@link #onShow}: indicates that an Activity has invoked the voice * interaction service for a local interaction using * {@link Activity#startLocalVoiceInteraction(Bundle)}. */ public static final int SHOW_SOURCE_ACTIVITY = 1<<4; /** * Flag for use with {@link #onShow}: indicates that the voice interaction service was invoked * from a physical button. */ public static final int SHOW_SOURCE_PUSH_TO_TALK = 1 << 5; /** * Flag for use with {@link #onShow}: indicates that the voice interaction service was invoked * from a notification. */ public static final int SHOW_SOURCE_NOTIFICATION = 1 << 6; /** * Flag for use with {@link #onShow}: indicates that the voice interaction service was invoked * from an Android automotive system UI. */ public static final int SHOW_SOURCE_AUTOMOTIVE_SYSTEM_UI = 1 << 7; /** @hide */ public static final int VOICE_INTERACTION_ACTIVITY_EVENT_START = 1; /** @hide */ public static final int VOICE_INTERACTION_ACTIVITY_EVENT_RESUME = 2; /** @hide */ public static final int VOICE_INTERACTION_ACTIVITY_EVENT_PAUSE = 3; /** @hide */ public static final int VOICE_INTERACTION_ACTIVITY_EVENT_STOP = 4; /** @hide */ @IntDef(prefix = { "VOICE_INTERACTION_ACTIVITY_EVENT_" }, value = { VOICE_INTERACTION_ACTIVITY_EVENT_START, VOICE_INTERACTION_ACTIVITY_EVENT_RESUME, VOICE_INTERACTION_ACTIVITY_EVENT_PAUSE, VOICE_INTERACTION_ACTIVITY_EVENT_STOP }) @Retention(RetentionPolicy.SOURCE) public @interface VoiceInteractionActivityEventType{} /** * Bundle key used to specify the id when the system prepares to show session. It increases for * each request. *

* Type: int *

* @see VoiceInteractionService#showSession(Bundle, int) * @see VoiceInteractionService#onPrepareToShowSession(Bundle, int) * @see VoiceInteractionService#onShowSessionFailed(Bundle) * @see #onShow(Bundle, int) * @see #show(Bundle, int) */ public static final String KEY_SHOW_SESSION_ID = "android.service.voice.SHOW_SESSION_ID"; /** * Bundle key used to specify foreground activity app components. *

* Type: ArrayList<ComponentName> *

* @see #onShow(Bundle, int) */ @FlaggedApi(Flags.FLAG_ALLOW_FOREGROUND_ACTIVITIES_IN_ON_SHOW) public static final String KEY_FOREGROUND_ACTIVITIES = "android.service.voice.FOREGROUND_ACTIVITIES"; 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; VoiceInteractionWindow mWindow; boolean mUiEnabled = true; boolean mInitialized; boolean mWindowAdded; boolean mWindowVisible; boolean mWindowWasVisible; boolean mInShowWindow; final ArrayMap mActiveRequests = new ArrayMap(); final Insets mTmpInsets = new Insets(); final WeakReference mWeakRef = new WeakReference(this); // Registry of remote callbacks pending a reply with reply handles. final Map> mRemoteCallbacks = new ArrayMap<>(); ICancellationSignal mKillCallback; private final Map mVisibleActivityCallbacks = new ArrayMap<>(); private final List mVisibleActivityInfos = new ArrayList<>(); final IVoiceInteractor mInteractor = new IVoiceInteractor.Stub() { @Override public IVoiceInteractorRequest startConfirmation(String callingPackage, IVoiceInteractorCallback callback, VoiceInteractor.Prompt prompt, Bundle extras) { ConfirmationRequest request = new ConfirmationRequest(callingPackage, Binder.getCallingUid(), callback, VoiceInteractionSession.this, prompt, extras); addRequest(request); mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_START_CONFIRMATION, request)); return request.mInterface; } @Override public IVoiceInteractorRequest startPickOption(String callingPackage, IVoiceInteractorCallback callback, VoiceInteractor.Prompt prompt, VoiceInteractor.PickOptionRequest.Option[] options, Bundle extras) { PickOptionRequest request = new PickOptionRequest(callingPackage, Binder.getCallingUid(), callback, VoiceInteractionSession.this, prompt, options, extras); addRequest(request); mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_START_PICK_OPTION, request)); return request.mInterface; } @Override public IVoiceInteractorRequest startCompleteVoice(String callingPackage, IVoiceInteractorCallback callback, VoiceInteractor.Prompt message, Bundle extras) { CompleteVoiceRequest request = new CompleteVoiceRequest(callingPackage, Binder.getCallingUid(), callback, VoiceInteractionSession.this, message, extras); addRequest(request); mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_START_COMPLETE_VOICE, request)); return request.mInterface; } @Override public IVoiceInteractorRequest startAbortVoice(String callingPackage, IVoiceInteractorCallback callback, VoiceInteractor.Prompt message, Bundle extras) { AbortVoiceRequest request = new AbortVoiceRequest(callingPackage, Binder.getCallingUid(), callback, VoiceInteractionSession.this, message, extras); addRequest(request); mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_START_ABORT_VOICE, request)); return request.mInterface; } @Override public IVoiceInteractorRequest startCommand(String callingPackage, IVoiceInteractorCallback callback, String command, Bundle extras) { CommandRequest request = new CommandRequest(callingPackage, Binder.getCallingUid(), callback, VoiceInteractionSession.this, command, extras); addRequest(request); mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_START_COMMAND, request)); return request.mInterface; } @Override public boolean[] supportsCommands(String callingPackage, String[] commands) { Message msg = mHandlerCaller.obtainMessageIOO(MSG_SUPPORTS_COMMANDS, 0, commands, null); SomeArgs args = mHandlerCaller.sendMessageAndWait(msg); if (args != null) { boolean[] res = (boolean[])args.arg1; args.recycle(); return res; } return new boolean[commands.length]; } @Override public void notifyDirectActionsChanged(int taskId, IBinder assistToken) { mHandlerCaller.getHandler().sendMessage(PooledLambda.obtainMessage( VoiceInteractionSession::onDirectActionsInvalidated, VoiceInteractionSession.this, new ActivityId(taskId, assistToken)) ); } @Override public void setKillCallback(ICancellationSignal callback) { mKillCallback = callback; } }; final IVoiceInteractionSession mSession = new IVoiceInteractionSession.Stub() { @Override public void show(Bundle sessionArgs, int flags, IVoiceInteractionSessionShowCallback showCallback) { mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(MSG_SHOW, flags, sessionArgs, showCallback)); } @Override public void hide() { // Remove any pending messages to show the session mHandlerCaller.removeMessages(MSG_SHOW); mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_HIDE)); } @Override public void handleAssist(final int taskId, final IBinder assistToken, final Bundle data, final AssistStructure structure, final AssistContent content, final int index, final int count) { // We want to pre-warm the AssistStructure before handing it off to the main // thread. We also want to do this on a separate thread, so that if the app // is for some reason slow (due to slow filling in of async children in the // structure), we don't block other incoming IPCs (such as the screenshot) to // us (since we are a oneway interface, they get serialized). (Okay?) Thread retriever = new Thread("AssistStructure retriever") { @Override public void run() { Throwable failure = null; if (structure != null) { try { structure.ensureData(); } catch (Throwable e) { Log.w(TAG, "Failure retrieving AssistStructure", e); failure = e; } } SomeArgs args = SomeArgs.obtain(); args.argi1 = taskId; args.arg1 = data; args.arg2 = (failure == null) ? structure : null; args.arg3 = failure; args.arg4 = content; args.arg5 = assistToken; args.argi5 = index; args.argi6 = count; mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO( MSG_HANDLE_ASSIST, args)); } }; retriever.start(); } @Override public void handleScreenshot(Bitmap screenshot) { mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_HANDLE_SCREENSHOT, screenshot)); } @Override public void taskStarted(Intent intent, int taskId) { mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIO(MSG_TASK_STARTED, taskId, intent)); } @Override public void taskFinished(Intent intent, int taskId) { mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIO(MSG_TASK_FINISHED, taskId, intent)); } @Override public void closeSystemDialogs() { mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_CLOSE_SYSTEM_DIALOGS)); } @Override public void onLockscreenShown() { mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_ON_LOCKSCREEN_SHOWN)); } @Override public void destroy() { mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_DESTROY)); } @Override public void notifyVisibleActivityInfoChanged(VisibleActivityInfo visibleActivityInfo, int type) { mHandlerCaller.sendMessage( mHandlerCaller.obtainMessageIO(MSG_NOTIFY_VISIBLE_ACTIVITY_INFO_CHANGED, type, visibleActivityInfo)); } }; /** * Base class representing a request from a voice-driver app to perform a particular * voice operation with the user. See related subclasses for the types of requests * that are possible. */ public static class Request { final IVoiceInteractorRequest mInterface = new IVoiceInteractorRequest.Stub() { @Override public void cancel() throws RemoteException { VoiceInteractionSession session = mSession.get(); if (session != null) { session.mHandlerCaller.sendMessage( session.mHandlerCaller.obtainMessageO(MSG_CANCEL, Request.this)); } } }; final String mCallingPackage; final int mCallingUid; final IVoiceInteractorCallback mCallback; final WeakReference mSession; final Bundle mExtras; Request(String packageName, int uid, IVoiceInteractorCallback callback, VoiceInteractionSession session, Bundle extras) { mCallingPackage = packageName; mCallingUid = uid; mCallback = callback; mSession = session.mWeakRef; mExtras = extras; } /** * Return the uid of the application that initiated the request. */ public int getCallingUid() { return mCallingUid; } /** * Return the package name of the application that initiated the request. */ public String getCallingPackage() { return mCallingPackage; } /** * Return any additional extra information that was supplied as part of the request. */ public Bundle getExtras() { return mExtras; } /** * Check whether this request is currently active. A request becomes inactive after * calling {@link #cancel} or a final result method that completes the request. After * this point, further interactions with the request will result in * {@link java.lang.IllegalStateException} errors; you should not catch these errors, * but can use this method if you need to determine the state of the request. Returns * true if the request is still active. */ public boolean isActive() { VoiceInteractionSession session = mSession.get(); if (session == null) { return false; } return session.isRequestActive(mInterface.asBinder()); } void finishRequest() { VoiceInteractionSession session = mSession.get(); if (session == null) { throw new IllegalStateException("VoiceInteractionSession has been destroyed"); } Request req = session.removeRequest(mInterface.asBinder()); if (req == null) { throw new IllegalStateException("Request not active: " + this); } else if (req != this) { throw new IllegalStateException("Current active request " + req + " not same as calling request " + this); } } /** * Ask the app to cancel this current request. * This also finishes the request (it is no longer active). */ public void cancel() { try { if (DEBUG) Log.d(TAG, "sendCancelResult: req=" + mInterface); finishRequest(); mCallback.deliverCancel(mInterface); } catch (RemoteException e) { } } @Override public String toString() { StringBuilder sb = new StringBuilder(128); DebugUtils.buildShortClassTag(this, sb); sb.append(" "); sb.append(mInterface.asBinder()); sb.append(" pkg="); sb.append(mCallingPackage); sb.append(" uid="); UserHandle.formatUid(sb, mCallingUid); sb.append('}'); return sb.toString(); } void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { writer.print(prefix); writer.print("mInterface="); writer.println(mInterface.asBinder()); writer.print(prefix); writer.print("mCallingPackage="); writer.print(mCallingPackage); writer.print(" mCallingUid="); UserHandle.formatUid(writer, mCallingUid); writer.println(); writer.print(prefix); writer.print("mCallback="); writer.println(mCallback.asBinder()); if (mExtras != null) { writer.print(prefix); writer.print("mExtras="); writer.println(mExtras); } } } /** * A request for confirmation from the user of an operation, as per * {@link android.app.VoiceInteractor.ConfirmationRequest * VoiceInteractor.ConfirmationRequest}. */ public static final class ConfirmationRequest extends Request { final VoiceInteractor.Prompt mPrompt; ConfirmationRequest(String packageName, int uid, IVoiceInteractorCallback callback, VoiceInteractionSession session, VoiceInteractor.Prompt prompt, Bundle extras) { super(packageName, uid, callback, session, extras); mPrompt = prompt; } /** * Return the prompt informing the user of what will happen, as per * {@link android.app.VoiceInteractor.ConfirmationRequest * VoiceInteractor.ConfirmationRequest}. */ @Nullable public VoiceInteractor.Prompt getVoicePrompt() { return mPrompt; } /** * Return the prompt informing the user of what will happen, as per * {@link android.app.VoiceInteractor.ConfirmationRequest * VoiceInteractor.ConfirmationRequest}. * @deprecated Prefer {@link #getVoicePrompt()} which allows multiple voice prompts. */ @Deprecated @Nullable public CharSequence getPrompt() { return (mPrompt != null ? mPrompt.getVoicePromptAt(0) : null); } /** * Report that the voice interactor has confirmed the operation with the user, resulting * in a call to * {@link android.app.VoiceInteractor.ConfirmationRequest#onConfirmationResult * VoiceInteractor.ConfirmationRequest.onConfirmationResult}. * This finishes the request (it is no longer active). */ public void sendConfirmationResult(boolean confirmed, Bundle result) { try { if (DEBUG) Log.d(TAG, "sendConfirmationResult: req=" + mInterface + " confirmed=" + confirmed + " result=" + result); finishRequest(); mCallback.deliverConfirmationResult(mInterface, confirmed, result); } catch (RemoteException e) { } } void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { super.dump(prefix, fd, writer, args); writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt); } } /** * A request for the user to pick from a set of option, as per * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}. */ public static final class PickOptionRequest extends Request { final VoiceInteractor.Prompt mPrompt; final VoiceInteractor.PickOptionRequest.Option[] mOptions; PickOptionRequest(String packageName, int uid, IVoiceInteractorCallback callback, VoiceInteractionSession session, VoiceInteractor.Prompt prompt, VoiceInteractor.PickOptionRequest.Option[] options, Bundle extras) { super(packageName, uid, callback, session, extras); mPrompt = prompt; mOptions = options; } /** * Return the prompt informing the user of what they are picking, as per * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}. */ @Nullable public VoiceInteractor.Prompt getVoicePrompt() { return mPrompt; } /** * Return the prompt informing the user of what they are picking, as per * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}. * @deprecated Prefer {@link #getVoicePrompt()} which allows multiple voice prompts. */ @Deprecated @Nullable public CharSequence getPrompt() { return (mPrompt != null ? mPrompt.getVoicePromptAt(0) : null); } /** * Return the set of options the user is picking from, as per * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}. */ public VoiceInteractor.PickOptionRequest.Option[] getOptions() { return mOptions; } void sendPickOptionResult(boolean finished, VoiceInteractor.PickOptionRequest.Option[] selections, Bundle result) { try { if (DEBUG) Log.d(TAG, "sendPickOptionResult: req=" + mInterface + " finished=" + finished + " selections=" + Arrays.toString(selections) + " result=" + result); if (finished) { finishRequest(); } mCallback.deliverPickOptionResult(mInterface, finished, selections, result); } catch (RemoteException e) { } } /** * Report an intermediate option selection from the request, without completing it (the * request is still active and the app is waiting for the final option selection), * resulting in a call to * {@link android.app.VoiceInteractor.PickOptionRequest#onPickOptionResult * VoiceInteractor.PickOptionRequest.onPickOptionResult} with false for finished. */ public void sendIntermediatePickOptionResult( VoiceInteractor.PickOptionRequest.Option[] selections, Bundle result) { sendPickOptionResult(false, selections, result); } /** * Report the final option selection for the request, completing the request * and resulting in a call to * {@link android.app.VoiceInteractor.PickOptionRequest#onPickOptionResult * VoiceInteractor.PickOptionRequest.onPickOptionResult} with false for finished. * This finishes the request (it is no longer active). */ public void sendPickOptionResult( VoiceInteractor.PickOptionRequest.Option[] selections, Bundle result) { sendPickOptionResult(true, selections, result); } void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { super.dump(prefix, fd, writer, args); writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt); if (mOptions != null) { writer.print(prefix); writer.println("Options:"); for (int i=0; i callback.onVisible(visibleActivityInfo)); } } } private void doUnregisterVisibleActivityCallback(@NonNull VisibleActivityCallback callback) { mVisibleActivityCallbacks.remove(callback); if (mVisibleActivityCallbacks.size() == 0) { mVisibleActivityInfos.clear(); try { mSystemService.stopListeningVisibleActivityChanged(mToken); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } } private void notifyVisibleActivityChanged(VisibleActivityInfo visibleActivityInfo, int type) { for (Map.Entry e : mVisibleActivityCallbacks.entrySet()) { final Executor executor = e.getValue(); final VisibleActivityCallback visibleActivityCallback = e.getKey(); switch (type) { case VisibleActivityInfo.TYPE_ACTIVITY_ADDED: Binder.withCleanCallingIdentity(() -> { executor.execute( () -> visibleActivityCallback.onVisible(visibleActivityInfo)); }); break; case VisibleActivityInfo.TYPE_ACTIVITY_REMOVED: Binder.withCleanCallingIdentity(() -> { executor.execute(() -> visibleActivityCallback.onInvisible( visibleActivityInfo.getActivityId())); }); break; } } } void ensureWindowCreated() { if (mInitialized) { return; } if (!mUiEnabled) { throw new IllegalStateException("setUiEnabled is false"); } mInitialized = true; mInflater = (LayoutInflater)mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); mWindow = new VoiceInteractionWindow(mContext, "VoiceInteractionSession", mTheme, mCallbacks, this, mDispatcherState, WindowManager.LayoutParams.TYPE_VOICE_INTERACTION, Gravity.BOTTOM, true); mWindow.getWindow().getAttributes().setFitInsetsTypes(0 /* types */); mWindow.getWindow().addFlags( WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); mThemeAttrs = mContext.obtainStyledAttributes(android.R.styleable.VoiceInteractionSession); mRootView = mInflater.inflate( com.android.internal.R.layout.voice_interaction_session, null); mRootView.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); mWindow.setContentView(mRootView); mRootView.getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsComputer); mContentFrame = (FrameLayout)mRootView.findViewById(android.R.id.content); mWindow.getWindow().setLayout(MATCH_PARENT, MATCH_PARENT); mWindow.setToken(mToken); } void ensureWindowAdded() { if (mUiEnabled && !mWindowAdded) { mWindowAdded = true; ensureWindowCreated(); View v = onCreateContentView(); if (v != null) { setContentView(v); } } } void showWindow() { if (mWindow != null) { mWindow.show(); try { mSystemService.setSessionWindowVisible(mToken, true); } catch (RemoteException e) { Log.w(TAG, "Failed to notify session window shown", e); } } } void ensureWindowHidden() { if (mWindow != null) { mWindow.hide(); try { mSystemService.setSessionWindowVisible(mToken, false); } catch (RemoteException e) { Log.w(TAG, "Failed to notify session window hidden", e); } } } /** * Equivalent to {@link VoiceInteractionService#setDisabledShowContext * VoiceInteractionService.setDisabledShowContext(int)}. */ public void setDisabledShowContext(int flags) { try { mSystemService.setDisabledShowContext(flags); } catch (RemoteException e) { } } /** * Equivalent to {@link VoiceInteractionService#getDisabledShowContext * VoiceInteractionService.getDisabledShowContext}. */ public int getDisabledShowContext() { try { return mSystemService.getDisabledShowContext(); } catch (RemoteException e) { return 0; } } /** * Return which show context flags have been disabled by the user through the system * settings UI, so the session will never get this data. Returned flags are any combination of * {@link VoiceInteractionSession#SHOW_WITH_ASSIST VoiceInteractionSession.SHOW_WITH_ASSIST} and * {@link VoiceInteractionSession#SHOW_WITH_SCREENSHOT * VoiceInteractionSession.SHOW_WITH_SCREENSHOT}. Note that this only tells you about * global user settings, not about restrictions that may be applied contextual based on * the current application the user is in or other transient states. */ public int getUserDisabledShowContext() { try { return mSystemService.getUserDisabledShowContext(); } catch (RemoteException e) { return 0; } } /** * Show the UI for this session. This asks the system to go through the process of showing * your UI, which will eventually culminate in {@link #onShow}. This is similar to calling * {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}. * @param args Arbitrary arguments that will be propagated {@link #onShow}. * @param flags Indicates additional optional behavior that should be performed. May * be any combination of * {@link VoiceInteractionSession#SHOW_WITH_ASSIST VoiceInteractionSession.SHOW_WITH_ASSIST} and * {@link VoiceInteractionSession#SHOW_WITH_SCREENSHOT * VoiceInteractionSession.SHOW_WITH_SCREENSHOT} * to request that the system generate and deliver assist data on the current foreground * app as part of showing the session UI. */ public void show(Bundle args, int flags) { if (mToken == null) { throw new IllegalStateException("Can't call before onCreate()"); } try { mSystemService.showSessionFromSession(mToken, args, flags, mContext.getAttributionTag()); } catch (RemoteException e) { } } /** * Hide the session's UI, if currently shown. Call this when you are done with your * user interaction. */ public void hide() { if (mToken == null) { throw new IllegalStateException("Can't call before onCreate()"); } try { mSystemService.hideSessionFromSession(mToken); } catch (RemoteException e) { } } /** * Control whether the UI layer for this session is enabled. It is enabled by default. * If set to false, you will not be able to provide a UI through {@link #onCreateContentView()}. */ public void setUiEnabled(boolean enabled) { if (mUiEnabled != enabled) { mUiEnabled = enabled; if (mWindowVisible) { if (enabled) { ensureWindowAdded(); showWindow(); } else { ensureWindowHidden(); } } } } /** * You can call this to customize the theme used by your IME's window. * This must be set before {@link #onCreate}, so you * will typically call it in your constructor with the resource ID * of your custom theme. */ public void setTheme(int theme) { if (mWindow != null) { throw new IllegalStateException("Must be called before onCreate()"); } mTheme = theme; } /** * Ask that a new activity be started for voice interaction. This will create a * new dedicated task in the activity manager for this voice interaction session; * this means that {@link Intent#FLAG_ACTIVITY_NEW_TASK Intent.FLAG_ACTIVITY_NEW_TASK} * will be set for you to make it a new task. * *

The newly started activity will be displayed to the user in a special way, as * a layer under the voice interaction UI.

* *

As 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(mContext); intent.prepareToLeaveProcess(mContext); int res = mSystemService.startVoiceActivity(mToken, intent, intent.resolveType(mContext.getContentResolver()), mContext.getAttributionTag()); Instrumentation.checkStartActivityResult(res, intent); } catch (RemoteException e) { } } /** *

Ask that a new assistant activity be started. This will create a new task in the * in activity manager: this means that * {@link Intent#FLAG_ACTIVITY_NEW_TASK Intent.FLAG_ACTIVITY_NEW_TASK} * will be set for you to make it a new task.

* *

The newly started activity will be displayed on top of other activities in the system * in a new layer that is not affected by multi-window mode. Tasks started from this activity * will go into the normal activity layer and not this new layer.

* *

By default, the system will create a window for the UI for this session. If you are using * an assistant activity instead, then you can disable the window creation by calling * {@link #setUiEnabled} in {@link #onPrepareShow(Bundle, int)}.

* * NOTE: if the app would like to override some options to start the Activity, * use {@link #startAssistantActivity(Intent, Bundle)} instead. */ public void startAssistantActivity(Intent intent) { startAssistantActivity(intent, ActivityOptions.makeBasic().toBundle()); } /** *

Ask that a new assistant activity be started. This will create a new task in the * in activity manager: this means that * {@link Intent#FLAG_ACTIVITY_NEW_TASK Intent.FLAG_ACTIVITY_NEW_TASK} * will be set for you to make it a new task.

* *

The newly started activity will be displayed on top of other activities in the system * in a new layer that is not affected by multi-window mode. Tasks started from this activity * will go into the normal activity layer and not this new layer.

* *

By default, the system will create a window for the UI for this session. If you are using * an assistant activity instead, then you can disable the window creation by calling * {@link #setUiEnabled} in {@link #onPrepareShow(Bundle, int)}.

* * @param intent the intent used to start an assistant activity * @param bundle Additional options for how the Activity should be started. See * {@link ActivityOptions} for how to build the Bundle supplied here. */ public void startAssistantActivity(@NonNull Intent intent, @NonNull Bundle bundle) { Objects.requireNonNull(bundle); if (mToken == null) { throw new IllegalStateException("Can't call before onCreate()"); } try { intent.migrateExtraStreamToClipData(mContext); intent.prepareToLeaveProcess(mContext); int res = mSystemService.startAssistantActivity(mToken, intent, intent.resolveType(mContext.getContentResolver()), mContext.getAttributionTag(), bundle); Instrumentation.checkStartActivityResult(res, intent); } catch (RemoteException e) { } } /** * Requests a list of supported actions from an app. * * @param activityId Ths activity id of the app to get the actions from. * @param cancellationSignal A signal to cancel the operation in progress, * or {@code null} if none. * @param resultExecutor The handler to receive the callback. * @param callback The callback to receive the response. */ public final void requestDirectActions(@NonNull ActivityId activityId, @Nullable CancellationSignal cancellationSignal, @NonNull @CallbackExecutor Executor resultExecutor, @NonNull Consumer> callback) { Objects.requireNonNull(activityId); Objects.requireNonNull(resultExecutor); Objects.requireNonNull(callback); if (mToken == null) { throw new IllegalStateException("Can't call before onCreate()"); } if (cancellationSignal != null) { cancellationSignal.throwIfCanceled(); } final RemoteCallback cancellationCallback = (cancellationSignal != null) ? new RemoteCallback(b -> { if (b != null) { final IBinder cancellation = b.getBinder( VoiceInteractor.KEY_CANCELLATION_SIGNAL); if (cancellation != null) { cancellationSignal.setRemote(ICancellationSignal.Stub.asInterface( cancellation)); } } }) : null; try { mSystemService.requestDirectActions(mToken, activityId.getTaskId(), activityId.getAssistToken(), cancellationCallback, new RemoteCallback(createSafeResultListener((result) -> { List list; if (result == null) { list = Collections.emptyList(); } else { final ParceledListSlice pls = result.getParcelable( DirectAction.KEY_ACTIONS_LIST, android.content.pm.ParceledListSlice.class); if (pls != null) { final List receivedList = pls.getList(); list = (receivedList != null) ? receivedList : Collections.emptyList(); } else { list = Collections.emptyList(); } } resultExecutor.execute(() -> callback.accept(list)); }))); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Called when the direct actions are invalidated. */ public void onDirectActionsInvalidated(@NonNull ActivityId activityId) { } /** * Asks that an action be performed by the app. This will send a request to the app which * provided this action. * *

An action could take time to execute and the result is provided asynchronously * via a callback. If the action is taking longer and you want to cancel its execution * you can pass in a cancellation signal through which to notify the app to abort the * action. * * @param action The action to be performed. * @param extras Any optional extras sent to the app as part of the request * @param cancellationSignal A signal to cancel the operation in progress, * or {@code null} if none. * @param resultExecutor The handler to receive the callback. * @param resultListener The callback to receive the response. * * @see #requestDirectActions(ActivityId, CancellationSignal, Executor, Consumer) * @see Activity#onGetDirectActions(CancellationSignal, Consumer) */ public final void performDirectAction(@NonNull DirectAction action, @Nullable Bundle extras, @Nullable CancellationSignal cancellationSignal, @NonNull @CallbackExecutor Executor resultExecutor, @NonNull Consumer resultListener) { if (mToken == null) { throw new IllegalStateException("Can't call before onCreate()"); } Objects.requireNonNull(resultExecutor); Objects.requireNonNull(resultListener); if (cancellationSignal != null) { cancellationSignal.throwIfCanceled(); } final RemoteCallback cancellationCallback = (cancellationSignal != null) ? new RemoteCallback(createSafeResultListener(b -> { if (b != null) { final IBinder cancellation = b.getBinder( VoiceInteractor.KEY_CANCELLATION_SIGNAL); if (cancellation != null) { cancellationSignal.setRemote(ICancellationSignal.Stub.asInterface( cancellation)); } } })) : null; final RemoteCallback resultCallback = new RemoteCallback(createSafeResultListener(b -> { if (b != null) { resultExecutor.execute(() -> resultListener.accept(b)); } else { resultExecutor.execute(() -> resultListener.accept(Bundle.EMPTY)); } })); try { mSystemService.performDirectAction(mToken, action.getId(), extras, action.getTaskId(), action.getActivityId(), cancellationCallback, resultCallback); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * 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() { ensureWindowCreated(); return mInflater; } /** * Retrieve the window being used to show the session's UI. */ public Dialog getWindow() { ensureWindowCreated(); 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; } /** * Called prior to {@link #onShow} before any UI setup has occurred. Not generally useful. * * @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 onPrepareShow(Bundle args, int showFlags) { } /** * 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}. * Some example keys include : *
    *
  • * invocation_type *
  • *
  • * invocation_phone_state *
  • *
  • * {@link #KEY_SHOW_SESSION_ID} *
  • *
  • * invocation_time_ms *
  • *
  • * Intent.EXTRA_TIME ("android.intent.extra.TIME") indicating timing in milliseconds of * the KeyEvent that triggered Assistant *
  • *
  • * Intent.EXTRA_ASSIST_INPUT_DEVICE_ID (android.intent.extra.ASSIST_INPUT_DEVICE_ID) * referring to the device that sent the request *
  • *
  • * {@link #KEY_FOREGROUND_ACTIVITIES} provides foreground activities of up coming * onHandleAssist/onHandleScreenshot calls earlier. This is only populated if session * was requested with {@link VoiceInteractionSession.SHOW_WITH_ASSIST} show flag. *
  • *
  • * Starting from Android 14, the system will add {@link #KEY_SHOW_SESSION_ID}, the * Bundle is not null. But the application should handle null case before Android 14. *
  • *
* * @param showFlags The show flags originally provided to * {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}. */ public void onShow(@Nullable 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) { ensureWindowCreated(); mContentFrame.removeAllViews(); mContentFrame.addView(view, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentFrame.requestApplyInsets(); } void doOnHandleAssist(int taskId, IBinder assistToken, Bundle data, AssistStructure structure, Throwable failure, AssistContent content, int index, int count) { if (failure != null) { onAssistStructureFailure(failure); } AssistState assistState = new AssistState(new ActivityId(taskId, assistToken), data, structure, content, index, count); onHandleAssist(assistState); } /** * 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, the data has been cleared during transferring due to * the new incoming assist data, 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. * * @deprecated use {@link #onHandleAssist(AssistState)} */ @Deprecated public void onHandleAssist(@Nullable Bundle data, @Nullable AssistStructure structure, @Nullable AssistContent content) { } /** * 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}, {@link AssistState} parameter will only provide * {@link ActivityId}. If there was a failure to write the assist data to * {@link AssistStructure}, the {@link AssistState#getAssistStructure()} will return null. * *

This method is called for all activities along with an index and count that indicates * which activity the data is for. {@code index} will be between 0 and {@code count}-1 and * this method is called once for each activity in no particular order. The {@code count} * indicates how many activities to expect assist data for, including the top focused one. * The focused activity can be determined by calling {@link AssistState#isFocused()}. * *

To be responsive to assist requests, process assist data as soon as it is received, * without waiting for all queued activities to return assist data. * * @param state The state object capturing the state of an activity. */ public void onHandleAssist(@NonNull AssistState state) { if (state.getAssistData() == null && state.getAssistStructure() == null && state.getAssistContent() == null) { return; } else if (state.getIndex() == 0) { onHandleAssist(state.getAssistData(), state.getAssistStructure(), state.getAssistContent()); } else { onHandleAssistSecondary(state.getAssistData(), state.getAssistStructure(), state.getAssistContent(), state.getIndex(), state.getCount()); } } /** * Called to receive data from other applications that the user was or is interacting with, * that are currently on the screen in a multi-window display environment, not including the * currently focused activity. This could be * a free-form window, a picture-in-picture window, or another window in a split-screen display. *

* This method is very similar to * {@link #onHandleAssist} except that it is called * for additional non-focused activities along with an index and count that indicates * which additional activity the data is for. {@code index} will be between 1 and * {@code count}-1 and this method is called once for each additional window, in no particular * order. The {@code count} indicates how many windows to expect assist data for, including the * top focused activity, which continues to be returned via {@link #onHandleAssist}. *

* To be responsive to assist requests, process assist data as soon as it is received, * without waiting for all queued activities to return assist data. * * @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. * @param index the index of the additional activity that this data * is for. * @param count the total number of additional activities for which the assist data is being * returned, including the focused activity that is returned via * {@link #onHandleAssist}. * * @deprecated use {@link #onHandleAssist(AssistState)} */ @Deprecated public void onHandleAssistSecondary(@Nullable Bundle data, @Nullable AssistStructure structure, @Nullable AssistContent content, int index, int count) { } /** * 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) { } /** * Registers a callback that will be notified when visible activities have been changed. * * Note: The {@link VisibleActivityCallback#onVisible(VisibleActivityInfo)} will be called * immediately with current visible activities when the callback is registered for the first * time. If the callback is already registered, this method does nothing. * * @param executor The executor which will be used to invoke the callback. * @param callback The callback to receive the response. * * @throws IllegalStateException if calling this method before onCreate(). */ public final void registerVisibleActivityCallback(@NonNull @CallbackExecutor Executor executor, @NonNull VisibleActivityCallback callback) { if (DEBUG) { Log.d(TAG, "registerVisibleActivityCallback"); } if (mToken == null) { throw new IllegalStateException("Can't call before onCreate()"); } Objects.requireNonNull(executor); Objects.requireNonNull(callback); mHandlerCaller.sendMessage( mHandlerCaller.obtainMessageOO(MSG_REGISTER_VISIBLE_ACTIVITY_CALLBACK, executor, callback)); } /** * Unregisters the callback. * * @param callback The callback to receive the response. */ public final void unregisterVisibleActivityCallback(@NonNull VisibleActivityCallback callback) { if (DEBUG) { Log.d(TAG, "unregisterVisibleActivityCallback"); } Objects.requireNonNull(callback); mHandlerCaller.sendMessage( mHandlerCaller.obtainMessageO(MSG_UNREGISTER_VISIBLE_ACTIVITY_CALLBACK, callback)); } /** * 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("mUiEnabled="); writer.println(mUiEnabled); 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 consumer) { synchronized (this) { final SafeResultListener listener = new SafeResultListener(consumer, this); mRemoteCallbacks.put(listener, consumer); return listener; } } private Consumer removeSafeResultListener(@NonNull SafeResultListener listener) { synchronized (this) { return mRemoteCallbacks.remove(listener); } } /** * Callback interface for receiving visible activity changes used for assistant usage. */ public interface VisibleActivityCallback { /** Callback to inform that an activity has become visible. */ default void onVisible(@NonNull VisibleActivityInfo activityInfo) {} /** Callback to inform that a visible activity has gone. */ default void onInvisible(@NonNull ActivityId activityId) {} } /** * Represents assist state captured when this session was started. * It contains the various assist data objects and a reference to * the source activity. */ @Immutable public static final class AssistState { private final @NonNull ActivityId mActivityId; private final int mIndex; private final int mCount; private final @Nullable Bundle mData; private final @Nullable AssistStructure mStructure; private final @Nullable AssistContent mContent; AssistState(@NonNull ActivityId activityId, @Nullable Bundle data, @Nullable AssistStructure structure, @Nullable AssistContent content, int index, int count) { mActivityId = activityId; mIndex = index; mCount = count; mData = data; mStructure = structure; mContent = content; } /** * @return whether the source activity is focused. */ public boolean isFocused() { return mIndex == 0; } /** * @return the index of the activity that this state is for or -1 * if there was no assist data captured. */ public @IntRange(from = -1) int getIndex() { return mIndex; } /** * @return the total number of activities for which the assist data is * being returned. */ public @IntRange(from = 0) int getCount() { return mCount; } /** * @return the id of the source activity */ public @NonNull ActivityId getActivityId() { return mActivityId; } /** * @return 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; will be null * if the original show request did not specify {@link #SHOW_WITH_ASSIST}. */ public @Nullable Bundle getAssistData() { return mData; } /** * @return 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 null if the original show request did not specify * {@link #SHOW_WITH_ASSIST} or the assist data has been corrupt when writing the data to * {@link AssistStructure}; will be an empty stub if the application has disabled assist * by marking its window as secure. */ public @Nullable AssistStructure getAssistStructure() { return mStructure; } /** * @return 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 be null * if the original show request did not specify {@link #SHOW_WITH_ASSIST}. Will not be * automatically filled in with data from the app if the app has marked its window as * secure. */ public @Nullable AssistContent getAssistContent() { return mContent; } } /** * Represents the id of an assist source activity. You can use * {@link #equals(Object)} to compare instances of this class. */ public static class ActivityId { private final int mTaskId; private final IBinder mAssistToken; ActivityId(int taskId, IBinder assistToken) { mTaskId = taskId; mAssistToken = assistToken; } /** @hide */ @TestApi public int getTaskId() { return mTaskId; } /** @hide */ @TestApi @NonNull public IBinder getAssistToken() { return mAssistToken; } @Override public boolean equals(@Nullable Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ActivityId that = (ActivityId) o; if (mTaskId != that.mTaskId) { return false; } return mAssistToken != null ? mAssistToken.equals(that.mAssistToken) : that.mAssistToken == null; } @Override public int hashCode() { int result = mTaskId; result = 31 * result + (mAssistToken != null ? mAssistToken.hashCode() : 0); return result; } } private static class SafeResultListener implements RemoteCallback.OnResultListener { private final @NonNull WeakReference mWeakSession; SafeResultListener(@NonNull Consumer action, @NonNull VoiceInteractionSession session) { mWeakSession = new WeakReference<>(session); } @Override public void onResult(Bundle result) { final VoiceInteractionSession session = mWeakSession.get(); if (session != null) { final Consumer consumer = session.removeSafeResultListener(this); if (consumer != null) { consumer.accept(result); } } } } }