| /* |
| * 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<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); |
| } |
| |
| 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<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(); |
| // 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<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(); |
| 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(); |
| } |
| } |
| }; |
| }; |