blob: d394d6311ba5ef9dac5316ac9f68ed4cce0fd77e [file] [log] [blame]
/*
* Copyright (C) 2015 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.server.voiceinteraction;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.IActivityManager;
import android.app.assist.AssistContent;
import android.app.assist.AssistStructure;
import android.content.ClipData;
import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.voice.IVoiceInteractionSession;
import android.service.voice.IVoiceInteractionSessionService;
import android.service.voice.VoiceInteractionService;
import android.service.voice.VoiceInteractionSession;
import android.util.Slog;
import android.view.IWindowManager;
import android.view.WindowManager;
import com.android.internal.app.AssistUtils;
import com.android.internal.app.IAssistScreenshotReceiver;
import com.android.internal.app.IVoiceInteractionSessionShowCallback;
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.os.IResultReceiver;
import com.android.server.LocalServices;
import com.android.server.statusbar.StatusBarManagerInternal;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
final class VoiceInteractionSessionConnection implements ServiceConnection {
final static String TAG = "VoiceInteractionServiceManager";
private static final String KEY_RECEIVER_EXTRA_COUNT = "count";
private static final String KEY_RECEIVER_EXTRA_INDEX = "index";
final IBinder mToken = new Binder();
final Object mLock;
final ComponentName mSessionComponentName;
final Intent mBindIntent;
final int mUser;
final Context mContext;
final Callback mCallback;
final int mCallingUid;
final Handler mHandler;
final IActivityManager mAm;
final IWindowManager mIWindowManager;
final AppOpsManager mAppOps;
final IBinder mPermissionOwner;
boolean mShown;
Bundle mShowArgs;
int mShowFlags;
boolean mBound;
boolean mFullyBound;
boolean mCanceled;
IVoiceInteractionSessionService mService;
IVoiceInteractionSession mSession;
IVoiceInteractor mInteractor;
boolean mHaveAssistData;
int mPendingAssistDataCount;
ArrayList<AssistDataForActivity> mAssistData = new ArrayList<>();
boolean mHaveScreenshot;
Bitmap mScreenshot;
ArrayList<IVoiceInteractionSessionShowCallback> mPendingShowCallbacks = new ArrayList<>();
static class AssistDataForActivity {
int activityIndex;
int activityCount;
Bundle data;
public AssistDataForActivity(Bundle data) {
this.data = data;
Bundle receiverExtras = data.getBundle(VoiceInteractionSession.KEY_RECEIVER_EXTRAS);
if (receiverExtras != null) {
activityIndex = receiverExtras.getInt(KEY_RECEIVER_EXTRA_INDEX);
activityCount = receiverExtras.getInt(KEY_RECEIVER_EXTRA_COUNT);
}
}
}
IVoiceInteractionSessionShowCallback mShowCallback =
new IVoiceInteractionSessionShowCallback.Stub() {
@Override
public void onFailed() throws RemoteException {
synchronized (mLock) {
notifyPendingShowCallbacksFailedLocked();
}
}
@Override
public void onShown() throws RemoteException {
synchronized (mLock) {
// TODO: Figure out whether this is good enough or whether we need to hook into
// Window manager to actually wait for the window to be drawn.
notifyPendingShowCallbacksShownLocked();
}
}
};
public interface Callback {
public void sessionConnectionGone(VoiceInteractionSessionConnection connection);
public void onSessionShown(VoiceInteractionSessionConnection connection);
public void onSessionHidden(VoiceInteractionSessionConnection connection);
}
final ServiceConnection mFullConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
final IResultReceiver mAssistReceiver = new IResultReceiver.Stub() {
@Override
public void send(int resultCode, Bundle resultData) throws RemoteException {
synchronized (mLock) {
if (mShown) {
mHaveAssistData = true;
mAssistData.add(new AssistDataForActivity(resultData));
deliverSessionDataLocked();
}
}
}
};
final IAssistScreenshotReceiver mScreenshotReceiver = new IAssistScreenshotReceiver.Stub() {
@Override
public void send(Bitmap screenshot) throws RemoteException {
synchronized (mLock) {
if (mShown) {
mHaveScreenshot = true;
mScreenshot = screenshot;
deliverSessionDataLocked();
}
}
}
};
public VoiceInteractionSessionConnection(Object lock, ComponentName component, int user,
Context context, Callback callback, int callingUid, Handler handler) {
mLock = lock;
mSessionComponentName = component;
mUser = user;
mContext = context;
mCallback = callback;
mCallingUid = callingUid;
mHandler = handler;
mAm = ActivityManager.getService();
mIWindowManager = IWindowManager.Stub.asInterface(
ServiceManager.getService(Context.WINDOW_SERVICE));
mAppOps = context.getSystemService(AppOpsManager.class);
IBinder permOwner = null;
try {
permOwner = mAm.newUriPermissionOwner("voicesession:"
+ component.flattenToShortString());
} catch (RemoteException e) {
Slog.w("voicesession", "AM dead", e);
}
mPermissionOwner = permOwner;
mBindIntent = new Intent(VoiceInteractionService.SERVICE_INTERFACE);
mBindIntent.setComponent(mSessionComponentName);
mBound = mContext.bindServiceAsUser(mBindIntent, this,
Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY
| Context.BIND_ALLOW_OOM_MANAGEMENT, new UserHandle(mUser));
if (mBound) {
try {
mIWindowManager.addWindowToken(mToken, TYPE_VOICE_INTERACTION, DEFAULT_DISPLAY);
} catch (RemoteException e) {
Slog.w(TAG, "Failed adding window token", e);
}
} else {
Slog.w(TAG, "Failed binding to voice interaction session service "
+ mSessionComponentName);
}
}
public int getUserDisabledShowContextLocked() {
int flags = 0;
if (Settings.Secure.getIntForUser(mContext.getContentResolver(),
Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1, mUser) == 0) {
flags |= VoiceInteractionSession.SHOW_WITH_ASSIST;
}
if (Settings.Secure.getIntForUser(mContext.getContentResolver(),
Settings.Secure.ASSIST_SCREENSHOT_ENABLED, 1, mUser) == 0) {
flags |= VoiceInteractionSession.SHOW_WITH_SCREENSHOT;
}
return flags;
}
public boolean showLocked(Bundle args, int flags, int disabledContext,
IVoiceInteractionSessionShowCallback showCallback, IBinder activityToken,
List<IBinder> topActivities) {
if (mBound) {
if (!mFullyBound) {
mFullyBound = mContext.bindServiceAsUser(mBindIntent, mFullConnection,
Context.BIND_AUTO_CREATE | Context.BIND_TREAT_LIKE_ACTIVITY
| Context.BIND_FOREGROUND_SERVICE,
new UserHandle(mUser));
}
mShown = true;
boolean isAssistDataAllowed = true;
try {
isAssistDataAllowed = mAm.isAssistDataAllowedOnCurrentActivity();
} catch (RemoteException e) {
}
disabledContext |= getUserDisabledShowContextLocked();
boolean structureEnabled = isAssistDataAllowed
&& (disabledContext&VoiceInteractionSession.SHOW_WITH_ASSIST) == 0;
boolean screenshotEnabled = isAssistDataAllowed && structureEnabled
&& (disabledContext&VoiceInteractionSession.SHOW_WITH_SCREENSHOT) == 0;
mShowArgs = args;
mShowFlags = flags;
mHaveAssistData = false;
mPendingAssistDataCount = 0;
boolean needDisclosure = false;
if ((flags&VoiceInteractionSession.SHOW_WITH_ASSIST) != 0) {
if (mAppOps.noteOpNoThrow(AppOpsManager.OP_ASSIST_STRUCTURE, mCallingUid,
mSessionComponentName.getPackageName()) == AppOpsManager.MODE_ALLOWED
&& structureEnabled) {
mAssistData.clear();
final int count = activityToken != null ? 1 : topActivities.size();
for (int i = 0; i < count; i++) {
IBinder topActivity = count == 1 ? activityToken : topActivities.get(i);
try {
MetricsLogger.count(mContext, "assist_with_context", 1);
Bundle receiverExtras = new Bundle();
receiverExtras.putInt(KEY_RECEIVER_EXTRA_INDEX, i);
receiverExtras.putInt(KEY_RECEIVER_EXTRA_COUNT, count);
if (mAm.requestAssistContextExtras(ActivityManager.ASSIST_CONTEXT_FULL,
mAssistReceiver, receiverExtras, topActivity,
/* focused= */ i == 0, /* newSessionId= */ i == 0)) {
needDisclosure = true;
mPendingAssistDataCount++;
} else if (i == 0) {
// Wasn't allowed... given that, let's not do the screenshot either.
mHaveAssistData = true;
mAssistData.clear();
screenshotEnabled = false;
break;
}
} catch (RemoteException e) {
}
}
} else {
mHaveAssistData = true;
mAssistData.clear();
}
} else {
mAssistData.clear();
}
mHaveScreenshot = false;
if ((flags&VoiceInteractionSession.SHOW_WITH_SCREENSHOT) != 0) {
if (mAppOps.noteOpNoThrow(AppOpsManager.OP_ASSIST_SCREENSHOT, mCallingUid,
mSessionComponentName.getPackageName()) == AppOpsManager.MODE_ALLOWED
&& screenshotEnabled) {
try {
MetricsLogger.count(mContext, "assist_with_screen", 1);
needDisclosure = true;
mIWindowManager.requestAssistScreenshot(mScreenshotReceiver);
} catch (RemoteException e) {
}
} else {
mHaveScreenshot = true;
mScreenshot = null;
}
} else {
mScreenshot = null;
}
if (needDisclosure && AssistUtils.shouldDisclose(mContext, mSessionComponentName)) {
mHandler.post(mShowAssistDisclosureRunnable);
}
if (mSession != null) {
try {
mSession.show(mShowArgs, mShowFlags, showCallback);
mShowArgs = null;
mShowFlags = 0;
} catch (RemoteException e) {
}
deliverSessionDataLocked();
} else if (showCallback != null) {
mPendingShowCallbacks.add(showCallback);
}
mCallback.onSessionShown(this);
return true;
}
if (showCallback != null) {
try {
showCallback.onFailed();
} catch (RemoteException e) {
}
}
return false;
}
void grantUriPermission(Uri uri, int mode, int srcUid, int destUid, String destPkg) {
if (!"content".equals(uri.getScheme())) {
return;
}
long ident = Binder.clearCallingIdentity();
try {
// This will throw SecurityException for us.
mAm.checkGrantUriPermission(srcUid, null, ContentProvider.getUriWithoutUserId(uri),
mode, ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(srcUid)));
// No security exception, do the grant.
int sourceUserId = ContentProvider.getUserIdFromUri(uri, mUser);
uri = ContentProvider.getUriWithoutUserId(uri);
mAm.grantUriPermissionFromOwner(mPermissionOwner, srcUid, destPkg,
uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, sourceUserId, mUser);
} catch (RemoteException e) {
} catch (SecurityException e) {
Slog.w(TAG, "Can't propagate permission", e);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
void grantClipDataItemPermission(ClipData.Item item, int mode, int srcUid, int destUid,
String destPkg) {
if (item.getUri() != null) {
grantUriPermission(item.getUri(), mode, srcUid, destUid, destPkg);
}
Intent intent = item.getIntent();
if (intent != null && intent.getData() != null) {
grantUriPermission(intent.getData(), mode, srcUid, destUid, destPkg);
}
}
void grantClipDataPermissions(ClipData data, int mode, int srcUid, int destUid,
String destPkg) {
final int N = data.getItemCount();
for (int i=0; i<N; i++) {
grantClipDataItemPermission(data.getItemAt(i), mode, srcUid, destUid, destPkg);
}
}
void deliverSessionDataLocked() {
if (mSession == null) {
return;
}
if (mHaveAssistData) {
AssistDataForActivity assistData;
if (mAssistData.isEmpty()) {
// We're not actually going to get any data, deliver some nothing
try {
mSession.handleAssist(null, null, null, 0, 0);
} catch (RemoteException e) {
}
} else {
while (!mAssistData.isEmpty()) {
if (mPendingAssistDataCount <= 0) {
Slog.e(TAG, "mPendingAssistDataCount is " + mPendingAssistDataCount);
}
mPendingAssistDataCount--;
assistData = mAssistData.remove(0);
if (assistData.data == null) {
try {
mSession.handleAssist(null, null, null, assistData.activityIndex,
assistData.activityCount);
} catch (RemoteException e) {
}
} else {
deliverSessionDataLocked(assistData);
}
}
}
if (mPendingAssistDataCount <= 0) {
mHaveAssistData = false;
} // else, more to come
}
if (mHaveScreenshot) {
try {
mSession.handleScreenshot(mScreenshot);
} catch (RemoteException e) {
}
mScreenshot = null;
mHaveScreenshot = false;
}
}
private void deliverSessionDataLocked(AssistDataForActivity assistDataForActivity) {
Bundle assistData = assistDataForActivity.data.getBundle(
VoiceInteractionSession.KEY_DATA);
AssistStructure structure = assistDataForActivity.data.getParcelable(
VoiceInteractionSession.KEY_STRUCTURE);
AssistContent content = assistDataForActivity.data.getParcelable(
VoiceInteractionSession.KEY_CONTENT);
int uid = assistDataForActivity.data.getInt(Intent.EXTRA_ASSIST_UID, -1);
if (uid >= 0 && content != null) {
Intent intent = content.getIntent();
if (intent != null) {
ClipData data = intent.getClipData();
if (data != null && Intent.isAccessUriMode(intent.getFlags())) {
grantClipDataPermissions(data, intent.getFlags(), uid,
mCallingUid, mSessionComponentName.getPackageName());
}
}
ClipData data = content.getClipData();
if (data != null) {
grantClipDataPermissions(data,
Intent.FLAG_GRANT_READ_URI_PERMISSION,
uid, mCallingUid, mSessionComponentName.getPackageName());
}
}
try {
mSession.handleAssist(assistData, structure, content,
assistDataForActivity.activityIndex, assistDataForActivity.activityCount);
} catch (RemoteException e) {
}
}
public boolean hideLocked() {
if (mBound) {
if (mShown) {
mShown = false;
mShowArgs = null;
mShowFlags = 0;
mHaveAssistData = false;
mAssistData.clear();
mPendingShowCallbacks.clear();
if (mSession != null) {
try {
mSession.hide();
} catch (RemoteException e) {
}
}
try {
mAm.revokeUriPermissionFromOwner(mPermissionOwner, null,
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
mUser);
} catch (RemoteException e) {
}
if (mSession != null) {
try {
mAm.finishVoiceTask(mSession);
} catch (RemoteException e) {
}
}
mCallback.onSessionHidden(this);
}
if (mFullyBound) {
mContext.unbindService(mFullConnection);
mFullyBound = false;
}
return true;
}
return false;
}
public void cancelLocked(boolean finishTask) {
hideLocked();
mCanceled = true;
if (mBound) {
if (mSession != null) {
try {
mSession.destroy();
} catch (RemoteException e) {
Slog.w(TAG, "Voice interation session already dead");
}
}
if (finishTask && mSession != null) {
try {
mAm.finishVoiceTask(mSession);
} catch (RemoteException e) {
}
}
mContext.unbindService(this);
try {
mIWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
} catch (RemoteException e) {
Slog.w(TAG, "Failed removing window token", e);
}
mBound = false;
mService = null;
mSession = null;
mInteractor = null;
}
if (mFullyBound) {
mContext.unbindService(mFullConnection);
mFullyBound = false;
}
}
public boolean deliverNewSessionLocked(IVoiceInteractionSession session,
IVoiceInteractor interactor) {
mSession = session;
mInteractor = interactor;
if (mShown) {
try {
session.show(mShowArgs, mShowFlags, mShowCallback);
mShowArgs = null;
mShowFlags = 0;
} catch (RemoteException e) {
}
deliverSessionDataLocked();
}
return true;
}
private void notifyPendingShowCallbacksShownLocked() {
for (int i = 0; i < mPendingShowCallbacks.size(); i++) {
try {
mPendingShowCallbacks.get(i).onShown();
} catch (RemoteException e) {
}
}
mPendingShowCallbacks.clear();
}
private void notifyPendingShowCallbacksFailedLocked() {
for (int i = 0; i < mPendingShowCallbacks.size(); i++) {
try {
mPendingShowCallbacks.get(i).onFailed();
} catch (RemoteException e) {
}
}
mPendingShowCallbacks.clear();
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mLock) {
mService = IVoiceInteractionSessionService.Stub.asInterface(service);
if (!mCanceled) {
try {
mService.newSession(mToken, mShowArgs, mShowFlags);
} catch (RemoteException e) {
Slog.w(TAG, "Failed adding window token", e);
}
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
mCallback.sessionConnectionGone(this);
synchronized (mLock) {
mService = null;
}
}
public void dump(String prefix, PrintWriter pw) {
pw.print(prefix); pw.print("mToken="); pw.println(mToken);
pw.print(prefix); pw.print("mShown="); pw.println(mShown);
pw.print(prefix); pw.print("mShowArgs="); pw.println(mShowArgs);
pw.print(prefix); pw.print("mShowFlags=0x"); pw.println(Integer.toHexString(mShowFlags));
pw.print(prefix); pw.print("mBound="); pw.println(mBound);
if (mBound) {
pw.print(prefix); pw.print("mService="); pw.println(mService);
pw.print(prefix); pw.print("mSession="); pw.println(mSession);
pw.print(prefix); pw.print("mInteractor="); pw.println(mInteractor);
}
pw.print(prefix); pw.print("mHaveAssistData="); pw.println(mHaveAssistData);
if (mHaveAssistData) {
pw.print(prefix); pw.print("mAssistData="); pw.println(mAssistData);
}
}
private Runnable mShowAssistDisclosureRunnable = new Runnable() {
@Override
public void run() {
StatusBarManagerInternal statusBarInternal = LocalServices.getService(
StatusBarManagerInternal.class);
if (statusBarInternal != null) {
statusBarInternal.showAssistDisclosure();
}
}
};
};