Voice Interaction from within an Activity
This allows an app to show a voice search button
and invoke a voice interaction session for use
within the activity. Once the activity exits, the
session is stopped.
Test application has a new activity that
demonstrates it with the test voice interaction
service.
This initial version is functional enough for
an integration test, with some more tests
and improvements to come later.
Bug: 22791070
Change-Id: Ib1e5bc8cae1fde40570c999b9cf4bb29efe4916d
diff --git a/api/current.txt b/api/current.txt
index c700501..e379c44 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -1183,6 +1183,7 @@
field public static final int summaryOn = 16843247; // 0x10101ef
field public static final int supportsAssist = 16844016; // 0x10104f0
field public static final int supportsLaunchVoiceAssistFromKeyguard = 16844017; // 0x10104f1
+ field public static final int supportsLocalInteraction = 16844048; // 0x1010510
field public static final int supportsPictureInPicture = 16844024; // 0x10104f8
field public static final int supportsRtl = 16843695; // 0x10103af
field public static final int supportsSwitchingToNextInputMethod = 16843755; // 0x10103eb
@@ -3436,6 +3437,7 @@
method public boolean isDestroyed();
method public boolean isFinishing();
method public boolean isImmersive();
+ method public boolean isLocalVoiceInteractionSupported();
method public boolean isTaskRoot();
method public boolean isVoiceInteraction();
method public boolean isVoiceInteractionRoot();
@@ -3477,6 +3479,8 @@
method public boolean onKeyMultiple(int, int, android.view.KeyEvent);
method public boolean onKeyShortcut(int, android.view.KeyEvent);
method public boolean onKeyUp(int, android.view.KeyEvent);
+ method public void onLocalVoiceInteractionStarted();
+ method public void onLocalVoiceInteractionStopped();
method public void onLowMemory();
method public boolean onMenuItemSelected(int, android.view.MenuItem);
method public boolean onMenuOpened(int, android.view.Menu);
@@ -3591,12 +3595,14 @@
method public void startIntentSenderForResult(android.content.IntentSender, int, android.content.Intent, int, int, int, android.os.Bundle) throws android.content.IntentSender.SendIntentException;
method public void startIntentSenderFromChild(android.app.Activity, android.content.IntentSender, int, android.content.Intent, int, int, int) throws android.content.IntentSender.SendIntentException;
method public void startIntentSenderFromChild(android.app.Activity, android.content.IntentSender, int, android.content.Intent, int, int, int, android.os.Bundle) throws android.content.IntentSender.SendIntentException;
+ method public void startLocalVoiceInteraction(android.os.Bundle);
method public void startLockTask();
method public deprecated void startManagingCursor(android.database.Cursor);
method public boolean startNextMatchingActivity(android.content.Intent);
method public boolean startNextMatchingActivity(android.content.Intent, android.os.Bundle);
method public void startPostponedEnterTransition();
method public void startSearch(java.lang.String, boolean, android.os.Bundle, boolean);
+ method public void stopLocalVoiceInteraction();
method public void stopLockTask();
method public deprecated void stopManagingCursor(android.database.Cursor);
method public void takeKeyEvents(boolean);
@@ -34005,6 +34011,7 @@
method public void setTheme(int);
method public void show(android.os.Bundle, int);
method public void startVoiceActivity(android.content.Intent);
+ field public static final int SHOW_SOURCE_ACTIVITY = 16; // 0x10
field public static final int SHOW_SOURCE_APPLICATION = 8; // 0x8
field public static final int SHOW_SOURCE_ASSIST_GESTURE = 4; // 0x4
field public static final int SHOW_WITH_ASSIST = 1; // 0x1
diff --git a/api/system-current.txt b/api/system-current.txt
index 7444db1..2fd54b3 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -1282,6 +1282,7 @@
field public static final int summaryOn = 16843247; // 0x10101ef
field public static final int supportsAssist = 16844016; // 0x10104f0
field public static final int supportsLaunchVoiceAssistFromKeyguard = 16844017; // 0x10104f1
+ field public static final int supportsLocalInteraction = 16844048; // 0x1010510
field public static final int supportsPictureInPicture = 16844024; // 0x10104f8
field public static final int supportsRtl = 16843695; // 0x10103af
field public static final int supportsSwitchingToNextInputMethod = 16843755; // 0x10103eb
@@ -3551,6 +3552,7 @@
method public boolean isDestroyed();
method public boolean isFinishing();
method public boolean isImmersive();
+ method public boolean isLocalVoiceInteractionSupported();
method public boolean isTaskRoot();
method public boolean isVoiceInteraction();
method public boolean isVoiceInteractionRoot();
@@ -3593,6 +3595,8 @@
method public boolean onKeyMultiple(int, int, android.view.KeyEvent);
method public boolean onKeyShortcut(int, android.view.KeyEvent);
method public boolean onKeyUp(int, android.view.KeyEvent);
+ method public void onLocalVoiceInteractionStarted();
+ method public void onLocalVoiceInteractionStopped();
method public void onLowMemory();
method public boolean onMenuItemSelected(int, android.view.MenuItem);
method public boolean onMenuOpened(int, android.view.Menu);
@@ -3707,12 +3711,14 @@
method public void startIntentSenderForResult(android.content.IntentSender, int, android.content.Intent, int, int, int, android.os.Bundle) throws android.content.IntentSender.SendIntentException;
method public void startIntentSenderFromChild(android.app.Activity, android.content.IntentSender, int, android.content.Intent, int, int, int) throws android.content.IntentSender.SendIntentException;
method public void startIntentSenderFromChild(android.app.Activity, android.content.IntentSender, int, android.content.Intent, int, int, int, android.os.Bundle) throws android.content.IntentSender.SendIntentException;
+ method public void startLocalVoiceInteraction(android.os.Bundle);
method public void startLockTask();
method public deprecated void startManagingCursor(android.database.Cursor);
method public boolean startNextMatchingActivity(android.content.Intent);
method public boolean startNextMatchingActivity(android.content.Intent, android.os.Bundle);
method public void startPostponedEnterTransition();
method public void startSearch(java.lang.String, boolean, android.os.Bundle, boolean);
+ method public void stopLocalVoiceInteraction();
method public void stopLockTask();
method public deprecated void stopManagingCursor(android.database.Cursor);
method public void takeKeyEvents(boolean);
@@ -36248,6 +36254,7 @@
method public void setTheme(int);
method public void show(android.os.Bundle, int);
method public void startVoiceActivity(android.content.Intent);
+ field public static final int SHOW_SOURCE_ACTIVITY = 16; // 0x10
field public static final int SHOW_SOURCE_APPLICATION = 8; // 0x8
field public static final int SHOW_SOURCE_ASSIST_GESTURE = 4; // 0x4
field public static final int SHOW_WITH_ASSIST = 1; // 0x1
diff --git a/api/test-current.txt b/api/test-current.txt
index 8067480..2eb8d75 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1183,6 +1183,7 @@
field public static final int summaryOn = 16843247; // 0x10101ef
field public static final int supportsAssist = 16844016; // 0x10104f0
field public static final int supportsLaunchVoiceAssistFromKeyguard = 16844017; // 0x10104f1
+ field public static final int supportsLocalInteraction = 16844048; // 0x1010510
field public static final int supportsPictureInPicture = 16844024; // 0x10104f8
field public static final int supportsRtl = 16843695; // 0x10103af
field public static final int supportsSwitchingToNextInputMethod = 16843755; // 0x10103eb
@@ -3436,6 +3437,7 @@
method public boolean isDestroyed();
method public boolean isFinishing();
method public boolean isImmersive();
+ method public boolean isLocalVoiceInteractionSupported();
method public boolean isTaskRoot();
method public boolean isVoiceInteraction();
method public boolean isVoiceInteractionRoot();
@@ -3477,6 +3479,8 @@
method public boolean onKeyMultiple(int, int, android.view.KeyEvent);
method public boolean onKeyShortcut(int, android.view.KeyEvent);
method public boolean onKeyUp(int, android.view.KeyEvent);
+ method public void onLocalVoiceInteractionStarted();
+ method public void onLocalVoiceInteractionStopped();
method public void onLowMemory();
method public boolean onMenuItemSelected(int, android.view.MenuItem);
method public boolean onMenuOpened(int, android.view.Menu);
@@ -3591,12 +3595,14 @@
method public void startIntentSenderForResult(android.content.IntentSender, int, android.content.Intent, int, int, int, android.os.Bundle) throws android.content.IntentSender.SendIntentException;
method public void startIntentSenderFromChild(android.app.Activity, android.content.IntentSender, int, android.content.Intent, int, int, int) throws android.content.IntentSender.SendIntentException;
method public void startIntentSenderFromChild(android.app.Activity, android.content.IntentSender, int, android.content.Intent, int, int, int, android.os.Bundle) throws android.content.IntentSender.SendIntentException;
+ method public void startLocalVoiceInteraction(android.os.Bundle);
method public void startLockTask();
method public deprecated void startManagingCursor(android.database.Cursor);
method public boolean startNextMatchingActivity(android.content.Intent);
method public boolean startNextMatchingActivity(android.content.Intent, android.os.Bundle);
method public void startPostponedEnterTransition();
method public void startSearch(java.lang.String, boolean, android.os.Bundle, boolean);
+ method public void stopLocalVoiceInteraction();
method public void stopLockTask();
method public deprecated void stopManagingCursor(android.database.Cursor);
method public void takeKeyEvents(boolean);
@@ -34019,6 +34025,7 @@
method public void setTheme(int);
method public void show(android.os.Bundle, int);
method public void startVoiceActivity(android.content.Intent);
+ field public static final int SHOW_SOURCE_ACTIVITY = 16; // 0x10
field public static final int SHOW_SOURCE_APPLICATION = 8; // 0x8
field public static final int SHOW_SOURCE_ASSIST_GESTURE = 4; // 0x4
field public static final int SHOW_WITH_ASSIST = 1; // 0x1
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index e312596..e36a427 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1267,6 +1267,15 @@
mCalled = true;
}
+ void setVoiceInteractor(IVoiceInteractor voiceInteractor) {
+ if (voiceInteractor == null) {
+ mVoiceInteractor = null;
+ } else {
+ mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
+ Looper.myLooper());
+ }
+ }
+
/**
* Check whether this activity is running as part of a voice interaction with the user.
* If true, it should perform its interaction with the user through the
@@ -1301,6 +1310,62 @@
}
/**
+ * Queries whether the currently enabled voice interaction service supports returning
+ * a voice interactor for use by the activity. This is valid only for the duration of the
+ * activity.
+ *
+ * @return whether the current voice interaction service supports local voice interaction
+ */
+ public boolean isLocalVoiceInteractionSupported() {
+ try {
+ return ActivityManagerNative.getDefault().supportsLocalVoiceInteraction();
+ } catch (RemoteException re) {
+ }
+ return false;
+ }
+
+ /**
+ * Starts a local voice interaction session. When ready,
+ * {@link #onLocalVoiceInteractionStarted()} is called. You can pass a bundle of private options
+ * to the registered voice interaction service.
+ * @param privateOptions a Bundle of private arguments to the current voice interaction service
+ */
+ public void startLocalVoiceInteraction(Bundle privateOptions) {
+ try {
+ ActivityManagerNative.getDefault().startLocalVoiceInteraction(mToken, privateOptions);
+ } catch (RemoteException re) {
+ }
+ }
+
+ /**
+ * Callback to indicate that {@link #startLocalVoiceInteraction(Bundle)} has resulted in a
+ * voice interaction session being started. You can now retrieve a voice interactor using
+ * {@link #getVoiceInteractor()}.
+ */
+ public void onLocalVoiceInteractionStarted() {
+ Log.i(TAG, "onLocalVoiceInteractionStarted! " + getVoiceInteractor());
+ }
+
+ /**
+ * Callback to indicate that the local voice interaction has stopped for some
+ * reason.
+ */
+ public void onLocalVoiceInteractionStopped() {
+ Log.i(TAG, "onLocalVoiceInteractionStopped :( " + getVoiceInteractor());
+ }
+
+ /**
+ * Request to terminate the current voice interaction that was previously started
+ * using {@link #startLocalVoiceInteraction(Bundle)}.
+ */
+ public void stopLocalVoiceInteraction() {
+ try {
+ ActivityManagerNative.getDefault().stopLocalVoiceInteraction(mToken);
+ } catch (RemoteException re) {
+ }
+ }
+
+ /**
* This is called for activities that set launchMode to "singleTop" in
* their package, or if a client used the {@link Intent#FLAG_ACTIVITY_SINGLE_TOP}
* flag when calling {@link #startActivity}. In either case, when the
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 373a23f..4fa654f 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -18,6 +18,10 @@
import android.annotation.NonNull;
import android.content.ComponentName;
+import android.os.IBinder;
+import android.service.voice.IVoiceInteractionSession;
+
+import com.android.internal.app.IVoiceInteractor;
/**
* Activity manager local system service interface.
@@ -64,4 +68,8 @@
* @param userId The user being cleaned up.
*/
public abstract void onUserRemoved(int userId);
+
+ public abstract void onLocalVoiceInteractionStarted(IBinder callingActivity,
+ IVoiceInteractionSession mSession,
+ IVoiceInteractor mInteractor);
}
diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java
index 63b6825..42ff8e8 100644
--- a/core/java/android/app/ActivityManagerNative.java
+++ b/core/java/android/app/ActivityManagerNative.java
@@ -315,6 +315,34 @@
return true;
}
+ case START_LOCAL_VOICE_INTERACTION_TRANSACTION:
+ {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ Bundle options = data.readBundle();
+ startLocalVoiceInteraction(token, options);
+ reply.writeNoException();
+ return true;
+ }
+
+ case STOP_LOCAL_VOICE_INTERACTION_TRANSACTION:
+ {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ stopLocalVoiceInteraction(token);
+ reply.writeNoException();
+ return true;
+ }
+
+ case SUPPORTS_LOCAL_VOICE_INTERACTION_TRANSACTION:
+ {
+ data.enforceInterface(IActivityManager.descriptor);
+ boolean result = supportsLocalVoiceInteraction();
+ reply.writeNoException();
+ reply.writeInt(result? 1 : 0);
+ return true;
+ }
+
case START_NEXT_MATCHING_ACTIVITY_TRANSACTION:
{
data.enforceInterface(IActivityManager.descriptor);
@@ -3136,6 +3164,43 @@
data.recycle();
return result;
}
+
+ public void startLocalVoiceInteraction(IBinder callingActivity, Bundle options)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(callingActivity);
+ data.writeBundle(options);
+ mRemote.transact(START_LOCAL_VOICE_INTERACTION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ reply.recycle();
+ data.recycle();
+ }
+
+ public void stopLocalVoiceInteraction(IBinder callingActivity) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(callingActivity);
+ mRemote.transact(STOP_LOCAL_VOICE_INTERACTION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ reply.recycle();
+ data.recycle();
+ }
+
+ public boolean supportsLocalVoiceInteraction() throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ mRemote.transact(SUPPORTS_LOCAL_VOICE_INTERACTION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ int result = reply.readInt();
+ reply.recycle();
+ data.recycle();
+ return result != 0;
+ }
+
public boolean startNextMatchingActivity(IBinder callingActivity,
Intent intent, Bundle options) throws RemoteException {
Parcel data = Parcel.obtain();
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 37b861e..f3e1fc3 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -1254,6 +1254,15 @@
throws RemoteException {
sendMessage(H.PICTURE_IN_PICTURE_MODE_CHANGED, token, pipMode ? 1 : 0);
}
+
+ @Override
+ public void scheduleLocalVoiceInteractionStarted(IBinder token,
+ IVoiceInteractor voiceInteractor) throws RemoteException {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = token;
+ args.arg2 = voiceInteractor;
+ sendMessage(H.LOCAL_VOICE_INTERACTION_STARTED, args);
+ }
}
private int getLifecycleSeq() {
@@ -1317,6 +1326,7 @@
public static final int STOP_BINDER_TRACKING_AND_DUMP = 151;
public static final int MULTI_WINDOW_MODE_CHANGED = 152;
public static final int PICTURE_IN_PICTURE_MODE_CHANGED = 153;
+ public static final int LOCAL_VOICE_INTERACTION_STARTED = 154;
String codeToString(int code) {
if (DEBUG_MESSAGES) {
@@ -1372,6 +1382,7 @@
case ENTER_ANIMATION_COMPLETE: return "ENTER_ANIMATION_COMPLETE";
case MULTI_WINDOW_MODE_CHANGED: return "MULTI_WINDOW_MODE_CHANGED";
case PICTURE_IN_PICTURE_MODE_CHANGED: return "PICTURE_IN_PICTURE_MODE_CHANGED";
+ case LOCAL_VOICE_INTERACTION_STARTED: return "LOCAL_VOICE_INTERACTION_STARTED";
}
}
return Integer.toString(code);
@@ -1621,6 +1632,10 @@
case PICTURE_IN_PICTURE_MODE_CHANGED:
handlePictureInPictureModeChanged((IBinder) msg.obj, msg.arg1 == 1);
break;
+ case LOCAL_VOICE_INTERACTION_STARTED:
+ handleLocalVoiceInteractionStarted((IBinder) ((SomeArgs) msg.obj).arg1,
+ (IVoiceInteractor) ((SomeArgs) msg.obj).arg2);
+ break;
}
Object obj = msg.obj;
if (obj instanceof SomeArgs) {
@@ -2878,6 +2893,19 @@
}
}
+ private void handleLocalVoiceInteractionStarted(IBinder token, IVoiceInteractor interactor) {
+ final ActivityClientRecord r = mActivities.get(token);
+ if (r != null) {
+ r.voiceInteractor = interactor;
+ r.activity.setVoiceInteractor(interactor);
+ if (interactor == null) {
+ r.activity.onLocalVoiceInteractionStopped();
+ } else {
+ r.activity.onLocalVoiceInteractionStarted();
+ }
+ }
+ }
+
private static final ThreadLocal<Intent> sCurrentBroadcastIntent = new ThreadLocal<Intent>();
/**
diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java
index 5951c8d..9be7f23 100644
--- a/core/java/android/app/ApplicationThreadNative.java
+++ b/core/java/android/app/ApplicationThreadNative.java
@@ -425,6 +425,16 @@
return true;
}
+ case SCHEDULE_LOCAL_VOICE_INTERACTION_STARTED_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder token = data.readStrongBinder();
+ IVoiceInteractor voiceInteractor = IVoiceInteractor.Stub.asInterface(
+ data.readStrongBinder());
+ scheduleLocalVoiceInteractionStarted(token, voiceInteractor);
+ return true;
+ }
+
case PROFILER_CONTROL_TRANSACTION:
{
data.enforceInterface(IApplicationThread.descriptor);
@@ -1101,6 +1111,17 @@
data.recycle();
}
+ public final void scheduleLocalVoiceInteractionStarted(IBinder token,
+ IVoiceInteractor voiceInteractor) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ data.writeStrongBinder(voiceInteractor != null ? voiceInteractor.asBinder() : null);
+ mRemote.transact(SCHEDULE_LOCAL_VOICE_INTERACTION_STARTED_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
public void updateTimeZone() throws RemoteException {
Parcel data = Parcel.obtain();
data.writeInterfaceToken(IApplicationThread.descriptor);
diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java
index 5bb2cf5..22de2ff 100644
--- a/core/java/android/app/IActivityManager.java
+++ b/core/java/android/app/IActivityManager.java
@@ -591,6 +591,12 @@
public boolean isAppForeground(int uid) throws RemoteException;
+ public void startLocalVoiceInteraction(IBinder token, Bundle options) throws RemoteException;
+
+ public void stopLocalVoiceInteraction(IBinder token) throws RemoteException;
+
+ public boolean supportsLocalVoiceInteraction() throws RemoteException;
+
/*
* Private non-Binder interfaces
*/
@@ -963,4 +969,7 @@
int GET_GRANTED_URI_PERMISSIONS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 360;
int CLEAR_GRANTED_URI_PERMISSIONS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 361;
int IS_APP_FOREGROUND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 362;
+ int START_LOCAL_VOICE_INTERACTION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 363;
+ int STOP_LOCAL_VOICE_INTERACTION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 364;
+ int SUPPORTS_LOCAL_VOICE_INTERACTION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 365;
}
diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java
index dc67026..6d64bd0 100644
--- a/core/java/android/app/IApplicationThread.java
+++ b/core/java/android/app/IApplicationThread.java
@@ -154,6 +154,7 @@
void stopBinderTrackingAndDump(FileDescriptor fd) throws RemoteException;
void scheduleMultiWindowModeChanged(IBinder token, boolean multiWindowMode) throws RemoteException;
void schedulePictureInPictureModeChanged(IBinder token, boolean multiWindowMode) throws RemoteException;
+ void scheduleLocalVoiceInteractionStarted(IBinder token, IVoiceInteractor voiceInteractor) throws RemoteException;
String descriptor = "android.app.IApplicationThread";
@@ -216,4 +217,5 @@
int STOP_BINDER_TRACKING_AND_DUMP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+57;
int SCHEDULE_MULTI_WINDOW_MODE_CHANGED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+58;
int SCHEDULE_PICTURE_IN_PICTURE_MODE_CHANGED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+59;
+ int SCHEDULE_LOCAL_VOICE_INTERACTION_STARTED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+60;
}
diff --git a/core/java/android/service/voice/VoiceInteractionManagerInternal.java b/core/java/android/service/voice/VoiceInteractionManagerInternal.java
new file mode 100644
index 0000000..b38067b
--- /dev/null
+++ b/core/java/android/service/voice/VoiceInteractionManagerInternal.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 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.os.Bundle;
+import android.os.IBinder;
+
+
+/**
+ * @hide
+ * Private interface to the VoiceInteractionManagerService for use by ActivityManagerService.
+ */
+public abstract class VoiceInteractionManagerInternal {
+
+ /**
+ * Start a new voice interaction session when requested from within an activity
+ * by Activity.startLocalVoiceInteraction()
+ * @param callingActivity The binder token representing the calling activity.
+ * @param options
+ */
+ public abstract void startLocalVoiceInteraction(IBinder callingActivity, Bundle options);
+
+ /**
+ * Returns whether the currently selected voice interaction service supports local voice
+ * interaction for launching from an Activity.
+ */
+ public abstract boolean supportsLocalVoiceInteraction();
+
+ public abstract void stopLocalVoiceInteraction(IBinder callingActivity);
+}
\ No newline at end of file
diff --git a/core/java/android/service/voice/VoiceInteractionServiceInfo.java b/core/java/android/service/voice/VoiceInteractionServiceInfo.java
index 4f58626..a9db32b 100644
--- a/core/java/android/service/voice/VoiceInteractionServiceInfo.java
+++ b/core/java/android/service/voice/VoiceInteractionServiceInfo.java
@@ -45,6 +45,7 @@
private String mSettingsActivity;
private boolean mSupportsAssist;
private boolean mSupportsLaunchFromKeyguard;
+ private boolean mSupportsLocalInteraction;
public VoiceInteractionServiceInfo(PackageManager pm, ComponentName comp)
throws PackageManager.NameNotFoundException {
@@ -118,6 +119,8 @@
mSupportsLaunchFromKeyguard = array.getBoolean(com.android.internal.
R.styleable.VoiceInteractionService_supportsLaunchVoiceAssistFromKeyguard,
false);
+ mSupportsLocalInteraction = array.getBoolean(com.android.internal.
+ R.styleable.VoiceInteractionService_supportsLocalInteraction, false);
array.recycle();
if (mSessionService == null) {
mParseError = "No sessionService specified";
@@ -172,4 +175,8 @@
public boolean getSupportsLaunchFromKeyguard() {
return mSupportsLaunchFromKeyguard;
}
+
+ public boolean getSupportsLocalInteraction() {
+ return mSupportsLocalInteraction;
+ }
}
diff --git a/core/java/android/service/voice/VoiceInteractionSession.java b/core/java/android/service/voice/VoiceInteractionSession.java
index ec14740..0c6a0c6 100644
--- a/core/java/android/service/voice/VoiceInteractionSession.java
+++ b/core/java/android/service/voice/VoiceInteractionSession.java
@@ -17,6 +17,7 @@
package android.service.voice;
import android.annotation.Nullable;
+import android.app.Activity;
import android.app.Dialog;
import android.app.Instrumentation;
import android.app.VoiceInteractor;
@@ -49,6 +50,7 @@
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;
@@ -75,7 +77,7 @@
*/
public class VoiceInteractionSession implements KeyEvent.Callback, ComponentCallbacks2 {
static final String TAG = "VoiceInteractionSession";
- static final boolean DEBUG = false;
+ static final boolean DEBUG = true;
/**
* Flag received in {@link #onShow}: originator requested that the session be started with
@@ -101,6 +103,13 @@
*/
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;
+
final Context mContext;
final HandlerCaller mHandlerCaller;
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index e0f9eca..80c6060 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -7512,6 +7512,10 @@
<!-- Flag indicating whether this voice interaction service is capable of being launched
from the keyguard. -->
<attr name="supportsLaunchVoiceAssistFromKeyguard" format="boolean" />
+ <!-- Flag indicating whether this voice interaction service can handle local voice
+ interaction requests from an Activity. This flag is new in
+ {@link android.os.Build.VERSION_CODES#N} and not used in previous versions. -->
+ <attr name="supportsLocalInteraction" format="boolean" />
</declare-styleable>
<!-- Use <code>voice-enrollment-application</code>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index c883b1f8..57132ea 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2690,6 +2690,7 @@
<public type="attr" name="tickMarkTintMode" />
<public type="attr" name="canPerformGestures" />
<public type="attr" name="externalService" />
+ <public type="attr" name="supportsLocalInteraction" />
<public type="style" name="Theme.Material.Light.DialogWhenLarge.DarkActionBar" />
<public type="style" name="Widget.Material.SeekBar.Discrete" />
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 8c2090e..25ee38e 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -187,6 +187,7 @@
import android.os.storage.StorageManager;
import android.provider.Settings;
import android.service.voice.IVoiceInteractionSession;
+import android.service.voice.VoiceInteractionManagerInternal;
import android.service.voice.VoiceInteractionSession;
import android.text.format.DateUtils;
import android.text.format.Time;
@@ -2803,16 +2804,21 @@
} else {
r.appTimeTracker = null;
}
+ // TODO: VI Maybe r.task.voiceInteractor || r.voiceInteractor != null
+ // TODO: Probably not, because we don't want to resume voice on switching
+ // back to this activity
if (r.task.voiceInteractor != null) {
startRunningVoiceLocked(r.task.voiceSession, r.info.applicationInfo.uid);
} else {
finishRunningVoiceLocked();
- if (last != null && last.task.voiceSession != null) {
+ IVoiceInteractionSession session;
+ if (last != null && ((session = last.task.voiceSession) != null
+ || (session = last.voiceSession) != null)) {
// We had been in a voice interaction session, but now focused has
// move to something different. Just finish the session, we can't
// return to it and retain the proper state and synchronization with
// the voice interaction service.
- finishVoiceTask(last.task.voiceSession);
+ finishVoiceTask(session);
}
}
if (mStackSupervisor.moveActivityStackToFront(r, reason + " setFocusedActivity")) {
@@ -4256,6 +4262,66 @@
}
@Override
+ public void startLocalVoiceInteraction(IBinder callingActivity, Bundle options)
+ throws RemoteException {
+ Slog.i(TAG, "Activity tried to startVoiceInteraction");
+ synchronized (this) {
+ ActivityRecord activity = getFocusedStack().topActivity();
+ if (ActivityRecord.forTokenLocked(callingActivity) != activity) {
+ throw new SecurityException("Only focused activity can call startVoiceInteraction");
+ }
+ if (mRunningVoice != null || activity.task.voiceSession != null
+ || activity.voiceSession != null) {
+ Slog.w(TAG, "Already in a voice interaction, cannot start new voice interaction");
+ return;
+ }
+ if (activity.pendingVoiceInteractionStart) {
+ Slog.w(TAG, "Pending start of voice interaction already.");
+ return;
+ }
+ activity.pendingVoiceInteractionStart = true;
+ }
+ LocalServices.getService(VoiceInteractionManagerInternal.class)
+ .startLocalVoiceInteraction(callingActivity, options);
+ }
+
+ @Override
+ public void stopLocalVoiceInteraction(IBinder callingActivity) throws RemoteException {
+ LocalServices.getService(VoiceInteractionManagerInternal.class)
+ .stopLocalVoiceInteraction(callingActivity);
+ }
+
+ @Override
+ public boolean supportsLocalVoiceInteraction() throws RemoteException {
+ return LocalServices.getService(VoiceInteractionManagerInternal.class)
+ .supportsLocalVoiceInteraction();
+ }
+
+ void onLocalVoiceInteractionStartedLocked(IBinder activity,
+ IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor) {
+ ActivityRecord activityToCallback = ActivityRecord.forTokenLocked(activity);
+ if (activityToCallback == null) return;
+ activityToCallback.setVoiceSessionLocked(voiceSession);
+
+ // Inform the activity
+ try {
+ activityToCallback.app.thread.scheduleLocalVoiceInteractionStarted(activity,
+ voiceInteractor);
+ long token = Binder.clearCallingIdentity();
+ try {
+ startRunningVoiceLocked(voiceSession, activityToCallback.appInfo.uid);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ // TODO: VI Should we cache the activity so that it's easier to find later
+ // rather than scan through all the stacks and activities?
+ } catch (RemoteException re) {
+ activityToCallback.clearVoiceSessionLocked();
+ // TODO: VI Should this terminate the voice session?
+ }
+ }
+
+ @Override
public void setVoiceKeepAwake(IVoiceInteractionSession session, boolean keepAwake) {
synchronized (this) {
if (mRunningVoice != null && mRunningVoice.asBinder() == session.asBinder()) {
@@ -4747,9 +4813,11 @@
@Override
public void finishVoiceTask(IVoiceInteractionSession session) {
- synchronized(this) {
+ synchronized (this) {
final long origId = Binder.clearCallingIdentity();
try {
+ // TODO: VI Consider treating local voice interactions and voice tasks
+ // differently here
mStackSupervisor.finishVoiceTask(session);
} finally {
Binder.restoreCallingIdentity(origId);
@@ -11107,6 +11175,7 @@
}
void finishRunningVoiceLocked() {
+ Slog.d(TAG, "finishRunningVoiceLocked() >>>>");
if (mRunningVoice != null) {
mRunningVoice = null;
mVoiceWakeLock.release();
@@ -11250,6 +11319,7 @@
}
void startRunningVoiceLocked(IVoiceInteractionSession session, int targetUid) {
+ Slog.d(TAG, "<<< startRunningVoiceLocked()");
mVoiceWakeLock.setWorkSource(new WorkSource(targetUid));
if (mRunningVoice == null || mRunningVoice.asBinder() != session.asBinder()) {
boolean wasRunningVoice = mRunningVoice != null;
@@ -21059,6 +21129,15 @@
ActivityManagerService.this.onUserStoppedLocked(userId);
}
}
+
+ @Override
+ public void onLocalVoiceInteractionStarted(IBinder activity,
+ IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor) {
+ synchronized (ActivityManagerService.this) {
+ ActivityManagerService.this.onLocalVoiceInteractionStartedLocked(activity,
+ voiceSession, voiceInteractor);
+ }
+ }
}
private final class SleepTokenImpl extends SleepToken {
diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java
index b16bd2b..71008a9 100755
--- a/services/core/java/com/android/server/am/ActivityRecord.java
+++ b/services/core/java/com/android/server/am/ActivityRecord.java
@@ -54,6 +54,7 @@
import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
+import android.service.voice.IVoiceInteractionSession;
import android.util.EventLog;
import android.util.Log;
import android.util.Slog;
@@ -209,6 +210,9 @@
private int[] mHorizontalSizeConfigurations;
private int[] mSmallestSizeConfigurations;
+ boolean pendingVoiceInteractionStart; // Waiting for activity-invoked voice session
+ IVoiceInteractionSession voiceSession; // Voice interaction session for this activity
+
void dump(PrintWriter pw, String prefix) {
final long now = SystemClock.uptimeMillis();
pw.print(prefix); pw.print("packageName="); pw.print(packageName);
@@ -1274,6 +1278,16 @@
taskDescription = _taskDescription;
}
+ void setVoiceSessionLocked(IVoiceInteractionSession session) {
+ voiceSession = session;
+ pendingVoiceInteractionStart = false;
+ }
+
+ void clearVoiceSessionLocked() {
+ voiceSession = null;
+ pendingVoiceInteractionStart = false;
+ }
+
void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException {
out.attribute(null, ATTR_ID, String.valueOf(createTime));
out.attribute(null, ATTR_LAUNCHEDFROMUID, String.valueOf(launchedFromUid));
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index fcea625..4bac2d6 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -3093,8 +3093,29 @@
didOne = true;
}
}
+ } else {
+ // Check if any of the activities are using voice
+ for (int activityNdx = tr.mActivities.size() - 1; activityNdx >= 0; --activityNdx) {
+ ActivityRecord r = tr.mActivities.get(activityNdx);
+ if (r.voiceSession != null
+ && r.voiceSession.asBinder() == sessionBinder) {
+ // Inform of cancellation
+ r.clearVoiceSessionLocked();
+ try {
+ r.app.thread.scheduleLocalVoiceInteractionStarted((IBinder) r.appToken,
+ null);
+ } catch (RemoteException re) {
+ // Ok
+ }
+ // TODO: VI This is redundant in some cases
+ mService.finishRunningVoiceLocked();
+ break;
+ }
+ }
}
}
+ Slog.d(TAG, "ActivityStack.finishVoiceTask()");
+
if (didOne) {
mService.updateOomAdjLocked();
}
@@ -4686,6 +4707,7 @@
updateTaskMovement(task, true);
if (!moving && task.mActivities.isEmpty()) {
+ // TODO: VI what about activity?
final boolean isVoiceSession = task.voiceSession != null;
if (isVoiceSession) {
try {
@@ -4790,6 +4812,7 @@
void addConfigOverride(ActivityRecord r, TaskRecord task) {
final Rect bounds = task.updateOverrideConfigurationFromLaunchBounds();
+ // TODO: VI deal with activity
mWindowManager.addAppToken(task.mActivities.indexOf(r), r.appToken,
r.task.taskId, mStackId, r.info.screenOrientation, r.fullscreen,
(r.info.flags & FLAG_SHOW_FOR_ALL_USERS) != 0, r.userId, r.info.configChanges,
diff --git a/services/core/java/com/android/server/am/RecentTasks.java b/services/core/java/com/android/server/am/RecentTasks.java
index 05702af..3f0674d 100644
--- a/services/core/java/com/android/server/am/RecentTasks.java
+++ b/services/core/java/com/android/server/am/RecentTasks.java
@@ -405,6 +405,8 @@
int recentsCount = size();
// Quick case: never add voice sessions.
+ // TODO: VI what about if it's just an activity?
+ // Probably nothing to do here
if (task.voiceSession != null) {
if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
"addRecent: not adding voice interaction " + task);
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index f05e45f..d11da79 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -3517,7 +3517,7 @@
/**
* Checks if the request is from the system or an app that has INTERACT_ACROSS_USERS
* or INTERACT_ACROSS_USERS_FULL permissions, if the userid is not for the caller.
- * @param checkShell TODO(yamasani):
+ * @param checkShell whether to prevent shell from access if there's a debugging restriction
* @param message the message to log on security exception
*/
void enforceCrossUserPermission(int callingUid, int userId, boolean requireFullPermission,
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 8fee91f..2aef109 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -18,6 +18,8 @@
import android.Manifest;
import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.ActivityManagerNative;
import android.app.AppGlobals;
import android.content.ComponentName;
import android.content.ContentResolver;
@@ -45,6 +47,7 @@
import android.provider.Settings;
import android.service.voice.IVoiceInteractionService;
import android.service.voice.IVoiceInteractionSession;
+import android.service.voice.VoiceInteractionManagerInternal;
import android.service.voice.VoiceInteractionService;
import android.service.voice.VoiceInteractionServiceInfo;
import android.service.voice.VoiceInteractionSession;
@@ -71,12 +74,13 @@
*/
public class VoiceInteractionManagerService extends SystemService {
static final String TAG = "VoiceInteractionManagerService";
- static final boolean DEBUG = false;
+ static final boolean DEBUG = true;
final Context mContext;
final ContentResolver mResolver;
final DatabaseHelper mDbHelper;
final SoundTriggerHelper mSoundTriggerHelper;
+ final ActivityManagerInternal mAmInternal;
public VoiceInteractionManagerService(Context context) {
super(context);
@@ -85,6 +89,7 @@
mDbHelper = new DatabaseHelper(context);
mSoundTriggerHelper = new SoundTriggerHelper(context);
mServiceStub = new VoiceInteractionManagerServiceStub();
+ mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
PackageManagerInternal packageManagerInternal = LocalServices.getService(
PackageManagerInternal.class);
@@ -105,6 +110,7 @@
@Override
public void onStart() {
publishBinderService(Context.VOICE_INTERACTION_MANAGER_SERVICE, mServiceStub);
+ publishLocalService(VoiceInteractionManagerInternal.class, new LocalService());
}
@Override
@@ -124,6 +130,31 @@
mServiceStub.switchUser(userHandle);
}
+ class LocalService extends VoiceInteractionManagerInternal {
+ @Override
+ public void startLocalVoiceInteraction(IBinder callingActivity, Bundle options) {
+ if (DEBUG) {
+ Slog.i(TAG, "startLocalVoiceInteraction " + callingActivity);
+ }
+ VoiceInteractionManagerService.this.mServiceStub.startLocalVoiceInteraction(
+ callingActivity, options);
+ }
+
+ @Override
+ public boolean supportsLocalVoiceInteraction() {
+ return VoiceInteractionManagerService.this.mServiceStub.supportsLocalVoiceInteraction();
+ }
+
+ @Override
+ public void stopLocalVoiceInteraction(IBinder callingActivity) {
+ if (DEBUG) {
+ Slog.i(TAG, "stopLocalVoiceInteraction " + callingActivity);
+ }
+ VoiceInteractionManagerService.this.mServiceStub.stopLocalVoiceInteraction(
+ callingActivity);
+ }
+ }
+
// implementation entry point and binder service
private final VoiceInteractionManagerServiceStub mServiceStub;
@@ -139,6 +170,49 @@
mEnableService = shouldEnableService(mContext.getResources());
}
+ // TODO: VI Make sure the caller is the current user or profile
+ void startLocalVoiceInteraction(final IBinder token, Bundle options) {
+ if (mImpl == null) return;
+
+ final long caller = Binder.clearCallingIdentity();
+ try {
+ mImpl.showSessionLocked(options,
+ VoiceInteractionSession.SHOW_SOURCE_ACTIVITY,
+ new IVoiceInteractionSessionShowCallback.Stub() {
+ @Override
+ public void onFailed() {
+ }
+
+ @Override
+ public void onShown() {
+ mAmInternal.onLocalVoiceInteractionStarted(token,
+ mImpl.mActiveSession.mSession,
+ mImpl.mActiveSession.mInteractor);
+ }
+ },
+ token);
+ } finally {
+ Binder.restoreCallingIdentity(caller);
+ }
+ }
+
+ public void stopLocalVoiceInteraction(IBinder callingActivity) {
+ if (mImpl == null) return;
+
+ final long caller = Binder.clearCallingIdentity();
+ try {
+ mImpl.finishLocked(callingActivity, true);
+ } finally {
+ Binder.restoreCallingIdentity(caller);
+ }
+ }
+
+ public boolean supportsLocalVoiceInteraction() {
+ if (mImpl == null) return false;
+
+ return mImpl.supportsLocalVoiceInteraction();
+ }
+
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
@@ -568,7 +642,7 @@
}
final long caller = Binder.clearCallingIdentity();
try {
- mImpl.finishLocked(token);
+ mImpl.finishLocked(token, false);
} finally {
Binder.restoreCallingIdentity(caller);
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 3efd0fb..1544723 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -215,12 +215,12 @@
}
}
- public void finishLocked(IBinder token) {
- if (mActiveSession == null || token != mActiveSession.mToken) {
+ public void finishLocked(IBinder token, boolean finishTask) {
+ if (mActiveSession == null || (!finishTask && token != mActiveSession.mToken)) {
Slog.w(TAG, "finish does not match active session");
return;
}
- mActiveSession.cancelLocked();
+ mActiveSession.cancelLocked(finishTask);
mActiveSession = null;
}
@@ -251,6 +251,10 @@
return mActiveSession != null ? mActiveSession.getUserDisabledShowContextLocked() : 0;
}
+ public boolean supportsLocalVoiceInteraction() {
+ return mInfo.getSupportsLocalInteraction();
+ }
+
public void dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!mValid) {
pw.print(" NOT VALID: ");
@@ -308,7 +312,7 @@
// If there is an active session, cancel it to allow it to clean up its window and other
// state.
if (mActiveSession != null) {
- mActiveSession.cancelLocked();
+ mActiveSession.cancelLocked(false);
mActiveSession = null;
}
try {
@@ -343,7 +347,7 @@
@Override
public void sessionConnectionGone(VoiceInteractionSessionConnection connection) {
synchronized (mLock) {
- finishLocked(connection.mToken);
+ finishLocked(connection.mToken, false);
}
}
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
index 1788e88..e04f312 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
@@ -418,7 +418,7 @@
return false;
}
- public void cancelLocked() {
+ public void cancelLocked(boolean finishTask) {
hideLocked();
mCanceled = true;
if (mBound) {
@@ -429,7 +429,7 @@
Slog.w(TAG, "Voice interation session already dead");
}
}
- if (mSession != null) {
+ if (finishTask && mSession != null) {
try {
mAm.finishVoiceTask(mSession);
} catch (RemoteException e) {
diff --git a/tests/VoiceInteraction/AndroidManifest.xml b/tests/VoiceInteraction/AndroidManifest.xml
index fe17c6e..cbc6c76 100644
--- a/tests/VoiceInteraction/AndroidManifest.xml
+++ b/tests/VoiceInteraction/AndroidManifest.xml
@@ -61,5 +61,13 @@
<category android:name="android.intent.category.VOICE" />
</intent-filter>
</activity>
+ <activity android:name="StartVoiceInteractionActivity" android:label="In-Activity Voice"
+ android:theme="@android:style/Theme.Material.Light">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
</application>
</manifest>
diff --git a/tests/VoiceInteraction/res/layout/local_interaction_app.xml b/tests/VoiceInteraction/res/layout/local_interaction_app.xml
new file mode 100644
index 0000000..9694133
--- /dev/null
+++ b/tests/VoiceInteraction/res/layout/local_interaction_app.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="8dp"
+ >
+
+ <TextView android:id="@+id/log"
+ android:layout_width="match_parent"
+ android:layout_height="0px"
+ android:layout_weight="1"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ />
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:orientation="horizontal">
+
+ <Button android:id="@+id/start"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/startFromActivity"
+ />
+
+ </LinearLayout>
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:orientation="horizontal">
+
+ <Button android:id="@+id/stop"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/stopFromActivity"
+ android:enabled="false"
+ />
+
+ </LinearLayout>
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:orientation="horizontal">
+
+ <Button android:id="@+id/command"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/commandVoice"
+ />
+
+ <Button android:id="@+id/pick"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/pickVoice"
+ />
+
+ </LinearLayout>
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="16dp"
+ android:orientation="horizontal">
+
+ <Button android:id="@+id/cancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/cancelVoice"
+ />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/tests/VoiceInteraction/res/values/strings.xml b/tests/VoiceInteraction/res/values/strings.xml
index c665c23..64f8bc5 100644
--- a/tests/VoiceInteraction/res/values/strings.xml
+++ b/tests/VoiceInteraction/res/values/strings.xml
@@ -31,6 +31,8 @@
<string name="pickVoice">Pick Voice</string>
<string name="cancelVoice">Cancel</string>
<string name="jumpOut">Jump out</string>
+ <string name="startFromActivity">Start voice interaction</string>
+ <string name="stopFromActivity">Stop voice interaction</string>
<string name="largetext">This is a bunch of text that we will use to show how we handle it
when reporting it for assist data. We need many many lines of text, like\n
diff --git a/tests/VoiceInteraction/res/xml/interaction_service.xml b/tests/VoiceInteraction/res/xml/interaction_service.xml
index c015ad2..f0c88a2 100644
--- a/tests/VoiceInteraction/res/xml/interaction_service.xml
+++ b/tests/VoiceInteraction/res/xml/interaction_service.xml
@@ -21,4 +21,5 @@
android:sessionService="com.android.test.voiceinteraction.MainInteractionSessionService"
android:recognitionService="com.android.test.voiceinteraction.MainRecognitionService"
android:settingsActivity="com.android.test.voiceinteraction.SettingsActivity"
- android:supportsAssist="true" />
+ android:supportsAssist="true"
+ android:supportsLocalInteraction="true" />
diff --git a/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionSession.java b/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionSession.java
index 6e3694b..450334c 100644
--- a/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionSession.java
+++ b/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionSession.java
@@ -73,6 +73,7 @@
CharSequence mPendingPrompt;
Request mPendingRequest;
int mCurrentTask = -1;
+ int mShowFlags;
MainInteractionSession(Context context) {
super(context);
@@ -88,6 +89,7 @@
@Override
public void onShow(Bundle args, int showFlags) {
super.onShow(args, showFlags);
+ mShowFlags = showFlags;
Log.i(TAG, "onShow: flags=0x" + Integer.toHexString(showFlags) + " args=" + args);
mState = STATE_IDLE;
mStartIntent = args != null ? (Intent)args.getParcelable("intent") : null;
@@ -311,6 +313,8 @@
if (mState != STATE_IDLE) {
outInsets.contentInsets.top = mBottomContent.getTop();
outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_CONTENT;
+ } else if ((mShowFlags & SHOW_SOURCE_ACTIVITY) != 0) {
+ outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_CONTENT;
}
}
@@ -355,7 +359,7 @@
mPendingPrompt = prompt.getVisualPrompt();
}
}
-
+
@Override
public void onRequestConfirmation(ConfirmationRequest request) {
Log.i(TAG, "onConfirm: prompt=" + request.getVoicePrompt() + " extras="
diff --git a/tests/VoiceInteraction/src/com/android/test/voiceinteraction/StartVoiceInteractionActivity.java b/tests/VoiceInteraction/src/com/android/test/voiceinteraction/StartVoiceInteractionActivity.java
new file mode 100644
index 0000000..41058c9
--- /dev/null
+++ b/tests/VoiceInteraction/src/com/android/test/voiceinteraction/StartVoiceInteractionActivity.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2016 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 com.android.test.voiceinteraction;
+
+import android.app.Activity;
+import android.app.VoiceInteractor;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+public class StartVoiceInteractionActivity extends Activity implements View.OnClickListener {
+ static final String TAG = "LocalVoiceInteractionActivity";
+
+ static final String REQUEST_ABORT = "abort";
+ static final String REQUEST_COMPLETE = "complete";
+ static final String REQUEST_COMMAND = "command";
+ static final String REQUEST_PICK = "pick";
+ static final String REQUEST_CONFIRM = "confirm";
+
+ VoiceInteractor mInteractor;
+ VoiceInteractor.Request mCurrentRequest = null;
+ TextView mLog;
+ Button mCommandButton;
+ Button mPickButton;
+ Button mCancelButton;
+ Button mStartButton;
+ Button mStopButton;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.local_interaction_app);
+
+ mLog = (TextView)findViewById(R.id.log);
+ mCommandButton = (Button)findViewById(R.id.command);
+ mCommandButton.setOnClickListener(this);
+ mPickButton = (Button)findViewById(R.id.pick);
+ mPickButton.setOnClickListener(this);
+ mCancelButton = (Button)findViewById(R.id.cancel);
+ mCancelButton.setOnClickListener(this);
+ mStartButton = (Button) findViewById(R.id.start);
+ mStartButton.setOnClickListener(this);
+ mStopButton = (Button) findViewById(R.id.stop);
+ mStopButton.setOnClickListener(this);
+
+ mLog.append("Local Voice Interaction Supported = " + isLocalVoiceInteractionSupported());
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mCommandButton) {
+ VoiceInteractor.CommandRequest req = new TestCommand("Some arg");
+ mInteractor.submitRequest(req, REQUEST_COMMAND);
+ } else if (v == mPickButton) {
+ VoiceInteractor.PickOptionRequest.Option[] options =
+ new VoiceInteractor.PickOptionRequest.Option[5];
+ options[0] = new VoiceInteractor.PickOptionRequest.Option("One");
+ options[1] = new VoiceInteractor.PickOptionRequest.Option("Two");
+ options[2] = new VoiceInteractor.PickOptionRequest.Option("Three");
+ options[3] = new VoiceInteractor.PickOptionRequest.Option("Four");
+ options[4] = new VoiceInteractor.PickOptionRequest.Option("Five");
+ VoiceInteractor.PickOptionRequest req = new TestPickOption(options);
+ mInteractor.submitRequest(req, REQUEST_PICK);
+ } else if (v == mCancelButton && mCurrentRequest != null) {
+ Log.i(TAG, "Cancel request");
+ mCurrentRequest.cancel();
+ } else if (v == mStartButton) {
+ Bundle args = new Bundle();
+ args.putString("Foo", "Bar");
+ startLocalVoiceInteraction(args);
+ } else if (v == mStopButton) {
+ stopLocalVoiceInteraction();
+ }
+ }
+
+ @Override
+ public void onLocalVoiceInteractionStarted() {
+ mInteractor = getVoiceInteractor();
+ mLog.append("\nLocalVoiceInteraction started!");
+ mStopButton.setEnabled(true);
+ }
+
+ @Override
+ public void onLocalVoiceInteractionStopped() {
+ mInteractor = getVoiceInteractor();
+ mLog.append("\nLocalVoiceInteraction stopped!");
+ mStopButton.setEnabled(false);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ static class TestAbortVoice extends VoiceInteractor.AbortVoiceRequest {
+ public TestAbortVoice() {
+ super(new VoiceInteractor.Prompt("Dammit, we suck :("), null);
+ }
+ @Override public void onCancel() {
+ Log.i(TAG, "Canceled!");
+ ((StartVoiceInteractionActivity)getActivity()).mLog.append("Canceled abort\n");
+ }
+ @Override public void onAbortResult(Bundle result) {
+ Log.i(TAG, "Abort result: result=" + result);
+ ((StartVoiceInteractionActivity)getActivity()).mLog.append(
+ "Abort: result=" + result + "\n");
+ getActivity().finish();
+ }
+ }
+
+ static class TestCompleteVoice extends VoiceInteractor.CompleteVoiceRequest {
+ public TestCompleteVoice() {
+ super(new VoiceInteractor.Prompt("Woohoo, completed!"), null);
+ }
+ @Override public void onCancel() {
+ Log.i(TAG, "Canceled!");
+ ((StartVoiceInteractionActivity)getActivity()).mLog.append("Canceled complete\n");
+ }
+ @Override public void onCompleteResult(Bundle result) {
+ Log.i(TAG, "Complete result: result=" + result);
+ ((StartVoiceInteractionActivity)getActivity()).mLog.append("Complete: result="
+ + result + "\n");
+ getActivity().finish();
+ }
+ }
+
+ static class TestCommand extends VoiceInteractor.CommandRequest {
+ public TestCommand(String arg) {
+ super("com.android.test.voiceinteraction.COMMAND", makeBundle(arg));
+ }
+ @Override public void onCancel() {
+ Log.i(TAG, "Canceled!");
+ ((StartVoiceInteractionActivity)getActivity()).mLog.append("Canceled command\n");
+ }
+ @Override
+ public void onCommandResult(boolean finished, Bundle result) {
+ Log.i(TAG, "Command result: finished=" + finished + " result=" + result);
+ StringBuilder sb = new StringBuilder();
+ if (finished) {
+ sb.append("Command final result: ");
+ } else {
+ sb.append("Command intermediate result: ");
+ }
+ if (result != null) {
+ result.getString("key");
+ }
+ sb.append(result);
+ sb.append("\n");
+ ((StartVoiceInteractionActivity)getActivity()).mLog.append(sb.toString());
+ }
+ static Bundle makeBundle(String arg) {
+ Bundle b = new Bundle();
+ b.putString("key", arg);
+ return b;
+ }
+ }
+
+ static class TestPickOption extends VoiceInteractor.PickOptionRequest {
+ public TestPickOption(Option[] options) {
+ super(new VoiceInteractor.Prompt("Need to pick something"), options, null);
+ }
+ @Override public void onCancel() {
+ Log.i(TAG, "Canceled!");
+ ((StartVoiceInteractionActivity)getActivity()).mLog.append("Canceled pick\n");
+ }
+ @Override
+ public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) {
+ Log.i(TAG, "Pick result: finished=" + finished + " selections=" + selections
+ + " result=" + result);
+ StringBuilder sb = new StringBuilder();
+ if (finished) {
+ sb.append("Pick final result: ");
+ } else {
+ sb.append("Pick intermediate result: ");
+ }
+ for (int i=0; i<selections.length; i++) {
+ if (i >= 1) {
+ sb.append(", ");
+ }
+ sb.append(selections[i].getLabel());
+ }
+ sb.append("\n");
+ ((StartVoiceInteractionActivity)getActivity()).mLog.append(sb.toString());
+ }
+ }
+}