/* * 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.ActivityManagerNative; 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.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; 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 mAssistData = new ArrayList<>(); boolean mHaveScreenshot; Bitmap mScreenshot; ArrayList 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); } 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 = ActivityManagerNative.getDefault(); 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, WindowManager.LayoutParams.TYPE_VOICE_INTERACTION); } 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 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(); // Temp workaround for bug: 28348867 Revert after DP3 for (int i = 0; i < count && i < 1; 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) { 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); } 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= 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(); 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) { } } } 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); } 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); 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(); } } }; };