Add voice interaction support to ResolverActivity/ChooserActivity
All options are sent to the VoiceInteractor once ChooserTargetServices
have reported in. We don't perform explicit progressive refinement or
filtering, but an explicit option picked will be invoked.
Also fix a lingering bug around being able to nested-fling the
resolver drawer closed.
Bug 21516866
Change-Id: I6b141f5fa87d74dccec9dcb88110630696e9c38e
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 678e92b..1b55557 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -100,6 +100,10 @@
mChooserListAdapter.addServiceResults(sri.originalTarget, sri.resultTargets);
unbindService(sri.connection);
mServiceConnections.remove(sri.connection);
+ if (mServiceConnections.isEmpty()) {
+ mChooserHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT);
+ sendVoiceChoicesIfNeeded();
+ }
break;
case CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT:
@@ -107,6 +111,7 @@
Log.d(TAG, "CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT; unbinding services");
}
unbindRemainingServices();
+ sendVoiceChoicesIfNeeded();
break;
default:
@@ -384,6 +389,8 @@
+ WATCHDOG_TIMEOUT_MILLIS + "ms");
mChooserHandler.sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT,
WATCHDOG_TIMEOUT_MILLIS);
+ } else {
+ sendVoiceChoicesIfNeeded();
}
}
@@ -418,6 +425,10 @@
mChooserHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT);
}
+ void onSetupVoiceInteraction() {
+ // Do nothing. We'll send the voice stuff ourselves.
+ }
+
void onRefinementResult(TargetInfo selectedTarget, Intent matchingIntent) {
if (mRefinementResultReceiver != null) {
mRefinementResultReceiver.destroy();
@@ -956,6 +967,10 @@
if (DEBUG) Log.d(TAG, "onServiceDisconnected: " + name);
unbindService(this);
mServiceConnections.remove(this);
+ if (mServiceConnections.isEmpty()) {
+ mChooserHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT);
+ sendVoiceChoicesIfNeeded();
+ }
}
@Override
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index e14f058..fe3ab9e 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -16,10 +16,17 @@
package com.android.internal.app;
+import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityThread;
+import android.app.VoiceInteractor;
+import android.app.VoiceInteractor.PickOptionRequest;
+import android.app.VoiceInteractor.PickOptionRequest.Option;
+import android.app.VoiceInteractor.Prompt;
+import android.app.VoiceInteractor.Request;
import android.os.AsyncTask;
import android.provider.Settings;
+import android.service.chooser.ChooserTarget;
import android.text.TextUtils;
import android.util.Slog;
import android.widget.AbsListView;
@@ -96,6 +103,7 @@
private int mProfileSwitchMessageId = -1;
private final ArrayList<Intent> mIntents = new ArrayList<>();
private ResolverComparator mResolverComparator;
+ private PickTargetOptionRequest mPickOptionRequest;
private boolean mRegistered;
private final PackageMonitor mPackageMonitor = new PackageMonitor() {
@@ -242,6 +250,9 @@
finish();
}
});
+ if (isVoiceInteraction()) {
+ rdl.setCollapsed(false);
+ }
}
if (title == null) {
@@ -313,6 +324,39 @@
});
bindProfileView();
}
+
+ if (isVoiceInteraction()) {
+ onSetupVoiceInteraction();
+ }
+ }
+
+ /**
+ * Perform any initialization needed for voice interaction.
+ */
+ void onSetupVoiceInteraction() {
+ // Do it right now. Subclasses may delay this and send it later.
+ sendVoiceChoicesIfNeeded();
+ }
+
+ void sendVoiceChoicesIfNeeded() {
+ if (!isVoiceInteraction()) {
+ // Clearly not needed.
+ return;
+ }
+
+
+ final Option[] options = new Option[mAdapter.getCount()];
+ for (int i = 0, N = options.length; i < N; i++) {
+ options[i] = optionForChooserTarget(mAdapter.getItem(i), i);
+ }
+
+ mPickOptionRequest = new PickTargetOptionRequest(
+ new Prompt(getTitle()), options, null);
+ getVoiceInteractor().submitRequest(mPickOptionRequest);
+ }
+
+ Option optionForChooserTarget(TargetInfo target, int index) {
+ return new Option(target.getDisplayLabel(), index);
}
protected final void setAdditionalTargets(Intent[] intents) {
@@ -473,6 +517,14 @@
}
@Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (!isChangingConfigurations() && mPickOptionRequest != null) {
+ mPickOptionRequest.cancel();
+ }
+ }
+
+ @Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (mAlwaysUseOption) {
@@ -510,16 +562,12 @@
try {
ApplicationInfo appInfo = getPackageManager().getApplicationInfo(
resolveInfo.activityInfo.packageName, 0 /* default flags */);
- return versionNumberAtLeastL(appInfo.targetSdkVersion);
+ return appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP;
} catch (NameNotFoundException e) {
return false;
}
}
- private boolean versionNumberAtLeastL(int versionNumber) {
- return versionNumber >= Build.VERSION_CODES.LOLLIPOP;
- }
-
private void setAlwaysButtonEnabled(boolean hasValidSelection, int checkedPos,
boolean filtered) {
boolean enabled = false;
@@ -1644,4 +1692,39 @@
&& match <= IntentFilter.MATCH_CATEGORY_PATH;
}
+ static class PickTargetOptionRequest extends PickOptionRequest {
+ public PickTargetOptionRequest(@Nullable Prompt prompt, Option[] options,
+ @Nullable Bundle extras) {
+ super(prompt, options, extras);
+ }
+
+ @Override
+ public void onCancel() {
+ super.onCancel();
+ final ResolverActivity ra = (ResolverActivity) getActivity();
+ if (ra != null) {
+ ra.mPickOptionRequest = null;
+ ra.finish();
+ }
+ }
+
+ @Override
+ public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) {
+ super.onPickOptionResult(finished, selections, result);
+ if (selections.length != 1) {
+ // TODO In a better world we would filter the UI presented here and let the
+ // user refine. Maybe later.
+ return;
+ }
+
+ final ResolverActivity ra = (ResolverActivity) getActivity();
+ if (ra != null) {
+ final TargetInfo ti = ra.mAdapter.getItem(selections[0].getIndex());
+ if (ra.onTargetSelected(ti, false)) {
+ ra.mPickOptionRequest = null;
+ ra.finish();
+ }
+ }
+ }
+ }
}
diff --git a/core/java/com/android/internal/widget/ResolverDrawerLayout.java b/core/java/com/android/internal/widget/ResolverDrawerLayout.java
index 585cbc9..1071e12 100644
--- a/core/java/com/android/internal/widget/ResolverDrawerLayout.java
+++ b/core/java/com/android/internal/widget/ResolverDrawerLayout.java
@@ -144,6 +144,14 @@
return mCollapseOffset > 0;
}
+ public void setCollapsed(boolean collapsed) {
+ if (!isLaidOut()) {
+ mOpenOnLayout = collapsed;
+ } else {
+ smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0);
+ }
+ }
+
private boolean isMoving() {
return mIsDragging || !mScroller.isFinished();
}
@@ -575,7 +583,13 @@
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) {
- smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
+ if (mOnDismissedListener != null
+ && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
+ smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY);
+ mDismissOnScrollerFinished = true;
+ } else {
+ smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
+ }
return true;
}
return false;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b1b772a..f9b41a93 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2457,6 +2457,7 @@
<intent-filter>
<action android:name="android.intent.action.CHOOSER" />
<category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.VOICE" />
</intent-filter>
</activity>
<activity android:name="com.android.internal.app.IntentForwarderActivity"