| /* |
| * 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 static android.app.AppOpsManager.OP_ASSIST_SCREENSHOT; |
| import static android.app.AppOpsManager.OP_ASSIST_STRUCTURE; |
| import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; |
| import static android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION; |
| import static android.view.Display.DEFAULT_DISPLAY; |
| import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION; |
| |
| import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_ACTIVITY_ID; |
| import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_CONTENT; |
| import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_DATA; |
| import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUCTURE; |
| import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_TASK_ID; |
| |
| import android.app.ActivityManager; |
| import android.app.ActivityTaskManager; |
| import android.app.AppOpsManager; |
| import android.app.IActivityManager; |
| import android.app.UriGrantsManager; |
| 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 com.android.internal.app.AssistUtils; |
| import com.android.internal.app.IVoiceInteractionSessionShowCallback; |
| import com.android.internal.app.IVoiceInteractor; |
| import com.android.server.LocalServices; |
| import com.android.server.am.AssistDataRequester; |
| import com.android.server.am.AssistDataRequester.AssistDataRequesterCallbacks; |
| import com.android.server.statusbar.StatusBarManagerInternal; |
| import com.android.server.uri.UriGrantsManagerInternal; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| final class VoiceInteractionSessionConnection implements ServiceConnection, |
| AssistDataRequesterCallbacks { |
| |
| final static String TAG = "VoiceInteractionServiceManager"; |
| |
| 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 UriGrantsManagerInternal mUgmInternal; |
| 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; |
| ArrayList<IVoiceInteractionSessionShowCallback> mPendingShowCallbacks = new ArrayList<>(); |
| AssistDataRequester mAssistDataRequester; |
| |
| 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) { |
| } |
| }; |
| |
| 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(); |
| mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class); |
| mIWindowManager = IWindowManager.Stub.asInterface( |
| ServiceManager.getService(Context.WINDOW_SERVICE)); |
| mAppOps = context.getSystemService(AppOpsManager.class); |
| mAssistDataRequester = new AssistDataRequester(mContext, mIWindowManager, |
| (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE), |
| this, mLock, OP_ASSIST_STRUCTURE, OP_ASSIST_SCREENSHOT); |
| final IBinder permOwner = mUgmInternal.newUriPermissionOwner("voicesession:" |
| + component.flattenToShortString()); |
| 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 |
| | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS, 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, List<IBinder> topActivities) { |
| if (mBound) { |
| if (!mFullyBound) { |
| mFullyBound = mContext.bindServiceAsUser(mBindIntent, mFullConnection, |
| Context.BIND_AUTO_CREATE | Context.BIND_TREAT_LIKE_ACTIVITY |
| | Context.BIND_SCHEDULE_LIKE_TOP_APP |
| | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS, |
| new UserHandle(mUser)); |
| } |
| |
| mShown = true; |
| mShowArgs = args; |
| mShowFlags = flags; |
| |
| disabledContext |= getUserDisabledShowContextLocked(); |
| mAssistDataRequester.requestAssistData(topActivities, |
| (flags & VoiceInteractionSession.SHOW_WITH_ASSIST) != 0, |
| (flags & VoiceInteractionSession.SHOW_WITH_SCREENSHOT) != 0, |
| (disabledContext & VoiceInteractionSession.SHOW_WITH_ASSIST) == 0, |
| (disabledContext & VoiceInteractionSession.SHOW_WITH_SCREENSHOT) == 0, |
| mCallingUid, mSessionComponentName.getPackageName()); |
| |
| boolean needDisclosure = mAssistDataRequester.getPendingDataCount() > 0 |
| || mAssistDataRequester.getPendingScreenshotCount() > 0; |
| 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) { |
| } |
| mAssistDataRequester.processPendingAssistData(); |
| } else if (showCallback != null) { |
| mPendingShowCallbacks.add(showCallback); |
| } |
| mCallback.onSessionShown(this); |
| return true; |
| } |
| if (showCallback != null) { |
| try { |
| showCallback.onFailed(); |
| } catch (RemoteException e) { |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean canHandleReceivedAssistDataLocked() { |
| return mSession != null; |
| } |
| |
| @Override |
| public void onAssistDataReceivedLocked(Bundle data, int activityIndex, int activityCount) { |
| // Return early if we have no session |
| if (mSession == null) { |
| return; |
| } |
| |
| if (data == null) { |
| try { |
| mSession.handleAssist(-1, null, null, null, null, 0, 0); |
| } catch (RemoteException e) { |
| // Ignore |
| } |
| } else { |
| final int taskId = data.getInt(ASSIST_TASK_ID); |
| final IBinder activityId = data.getBinder(ASSIST_ACTIVITY_ID); |
| final Bundle assistData = data.getBundle(ASSIST_KEY_DATA); |
| final AssistStructure structure = data.getParcelable(ASSIST_KEY_STRUCTURE); |
| final AssistContent content = data.getParcelable(ASSIST_KEY_CONTENT); |
| int uid = -1; |
| if (assistData != null) { |
| uid = assistData.getInt(Intent.EXTRA_ASSIST_UID, -1); |
| } |
| if (uid >= 0 && content != null) { |
| Intent intent = content.getIntent(); |
| if (intent != null) { |
| ClipData clipData = intent.getClipData(); |
| if (clipData != null && Intent.isAccessUriMode(intent.getFlags())) { |
| grantClipDataPermissions(clipData, intent.getFlags(), uid, |
| mCallingUid, mSessionComponentName.getPackageName()); |
| } |
| } |
| ClipData clipData = content.getClipData(); |
| if (clipData != null) { |
| grantClipDataPermissions(clipData, FLAG_GRANT_READ_URI_PERMISSION, |
| uid, mCallingUid, mSessionComponentName.getPackageName()); |
| } |
| } |
| try { |
| mSession.handleAssist(taskId, activityId, assistData, structure, |
| content, activityIndex, activityCount); |
| } catch (RemoteException e) { |
| // Ignore |
| } |
| } |
| } |
| |
| @Override |
| public void onAssistScreenshotReceivedLocked(Bitmap screenshot) { |
| // Return early if we have no session |
| if (mSession == null) { |
| return; |
| } |
| |
| try { |
| mSession.handleScreenshot(screenshot); |
| } catch (RemoteException e) { |
| // Ignore |
| } |
| } |
| |
| 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. |
| mUgmInternal.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); |
| UriGrantsManager.getService().grantUriPermissionFromOwner(mPermissionOwner, srcUid, |
| destPkg, uri, 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); |
| } |
| } |
| |
| public boolean hideLocked() { |
| if (mBound) { |
| if (mShown) { |
| mShown = false; |
| mShowArgs = null; |
| mShowFlags = 0; |
| mAssistDataRequester.cancel(); |
| mPendingShowCallbacks.clear(); |
| if (mSession != null) { |
| try { |
| mSession.hide(); |
| } catch (RemoteException e) { |
| } |
| } |
| mUgmInternal.revokeUriPermissionFromOwner(mPermissionOwner, null, |
| FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION, mUser); |
| if (mSession != null) { |
| try { |
| ActivityTaskManager.getService().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 { |
| ActivityTaskManager.getService().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) { |
| } |
| mAssistDataRequester.processPendingAssistData(); |
| } |
| 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); |
| } |
| mAssistDataRequester.dump(prefix, pw); |
| } |
| |
| private Runnable mShowAssistDisclosureRunnable = new Runnable() { |
| @Override |
| public void run() { |
| StatusBarManagerInternal statusBarInternal = LocalServices.getService( |
| StatusBarManagerInternal.class); |
| if (statusBarInternal != null) { |
| statusBarInternal.showAssistDisclosure(); |
| } |
| } |
| }; |
| }; |