1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.contextualsearch; 18 19 import static android.Manifest.permission.ACCESS_CONTEXTUAL_SEARCH; 20 import static android.app.AppOpsManager.OP_ASSIST_SCREENSHOT; 21 import static android.app.AppOpsManager.OP_ASSIST_STRUCTURE; 22 import static android.content.Context.CONTEXTUAL_SEARCH_SERVICE; 23 import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; 24 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 25 import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION; 26 import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; 27 import static android.content.pm.PackageManager.MATCH_FACTORY_ONLY; 28 import static android.content.pm.PackageManager.PERMISSION_GRANTED; 29 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; 30 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; 31 import static android.view.WindowManager.LayoutParams.TYPE_POINTER; 32 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; 33 34 import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_CONTENT; 35 import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUCTURE; 36 37 import android.annotation.NonNull; 38 import android.annotation.Nullable; 39 import android.annotation.RequiresPermission; 40 import android.app.ActivityOptions; 41 import android.app.AppOpsManager; 42 import android.app.admin.DevicePolicyManagerInternal; 43 import android.app.assist.AssistContent; 44 import android.app.assist.AssistStructure; 45 import android.app.contextualsearch.CallbackToken; 46 import android.app.contextualsearch.ContextualSearchManager; 47 import android.app.contextualsearch.ContextualSearchState; 48 import android.app.contextualsearch.IContextualSearchCallback; 49 import android.app.contextualsearch.IContextualSearchManager; 50 import android.app.contextualsearch.flags.Flags; 51 import android.content.ComponentName; 52 import android.content.Context; 53 import android.content.Intent; 54 import android.content.pm.PackageManagerInternal; 55 import android.content.pm.ResolveInfo; 56 import android.graphics.Bitmap; 57 import android.os.Binder; 58 import android.os.Bundle; 59 import android.os.Handler; 60 import android.os.IBinder; 61 import android.os.Looper; 62 import android.os.Message; 63 import android.os.ParcelableException; 64 import android.os.Process; 65 import android.os.RemoteException; 66 import android.os.ResultReceiver; 67 import android.os.ServiceManager; 68 import android.os.ShellCallback; 69 import android.os.SystemClock; 70 import android.provider.Settings; 71 import android.util.Log; 72 import android.util.Slog; 73 import android.view.IWindowManager; 74 import android.window.ScreenCapture; 75 76 import com.android.internal.R; 77 import com.android.internal.annotations.GuardedBy; 78 import com.android.server.LocalServices; 79 import com.android.server.SystemService; 80 import com.android.server.am.AssistDataRequester; 81 import com.android.server.am.AssistDataRequester.AssistDataRequesterCallbacks; 82 import com.android.server.wm.ActivityAssistInfo; 83 import com.android.server.wm.ActivityTaskManagerInternal; 84 import com.android.server.wm.WindowManagerInternal; 85 86 import java.io.FileDescriptor; 87 import java.util.ArrayList; 88 import java.util.List; 89 import java.util.Objects; 90 import java.util.Set; 91 92 public class ContextualSearchManagerService extends SystemService { 93 private static final String TAG = ContextualSearchManagerService.class.getSimpleName(); 94 private static final int MSG_RESET_TEMPORARY_PACKAGE = 0; 95 private static final int MAX_TEMP_PACKAGE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes 96 private static final int MSG_INVALIDATE_TOKEN = 1; 97 private static final int MAX_TOKEN_VALID_DURATION_MS = 1_000 * 60 * 10; // 10 minutes 98 99 private final Context mContext; 100 private final ActivityTaskManagerInternal mAtmInternal; 101 private final PackageManagerInternal mPackageManager; 102 private final WindowManagerInternal mWmInternal; 103 private final DevicePolicyManagerInternal mDpmInternal; 104 private final Object mLock = new Object(); 105 private final AssistDataRequester mAssistDataRequester; 106 107 private final AssistDataRequesterCallbacks mAssistDataCallbacks = 108 new AssistDataRequesterCallbacks() { 109 @Override 110 public boolean canHandleReceivedAssistDataLocked() { 111 synchronized (mLock) { 112 return mStateCallback != null; 113 } 114 } 115 116 @Override 117 public void onAssistDataReceivedLocked( 118 final Bundle data, 119 final int activityIndex, 120 final int activityCount) { 121 final IContextualSearchCallback callback; 122 synchronized (mLock) { 123 callback = mStateCallback; 124 } 125 126 if (callback != null) { 127 try { 128 callback.onResult(new ContextualSearchState( 129 data.getParcelable(ASSIST_KEY_STRUCTURE, AssistStructure.class), 130 data.getParcelable(ASSIST_KEY_CONTENT, AssistContent.class), 131 data)); 132 } catch (RemoteException e) { 133 Log.e(TAG, "Error invoking ContextualSearchCallback", e); 134 } 135 } else { 136 Log.w(TAG, "Callback went away!"); 137 } 138 } 139 140 @Override 141 public void onAssistRequestCompleted() { 142 synchronized (mLock) { 143 mStateCallback = null; 144 } 145 } 146 }; 147 148 @GuardedBy("this") 149 private Handler mTemporaryHandler; 150 @GuardedBy("this") 151 private String mTemporaryPackage = null; 152 @GuardedBy("this") 153 private long mTokenValidDurationMs = MAX_TOKEN_VALID_DURATION_MS; 154 155 @GuardedBy("mLock") 156 private IContextualSearchCallback mStateCallback; 157 ContextualSearchManagerService(@onNull Context context)158 public ContextualSearchManagerService(@NonNull Context context) { 159 super(context); 160 if (DEBUG_USER) Log.d(TAG, "ContextualSearchManagerService created"); 161 mContext = context; 162 mAtmInternal = Objects.requireNonNull( 163 LocalServices.getService(ActivityTaskManagerInternal.class)); 164 mPackageManager = LocalServices.getService(PackageManagerInternal.class); 165 mWmInternal = Objects.requireNonNull(LocalServices.getService(WindowManagerInternal.class)); 166 mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class); 167 mAssistDataRequester = new AssistDataRequester( 168 mContext, 169 IWindowManager.Stub.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE)), 170 mContext.getSystemService(AppOpsManager.class), 171 mAssistDataCallbacks, mLock, OP_ASSIST_STRUCTURE, OP_ASSIST_SCREENSHOT); 172 173 updateSecureSetting(); 174 } 175 176 @Override onStart()177 public void onStart() { 178 publishBinderService(CONTEXTUAL_SEARCH_SERVICE, new ContextualSearchManagerStub()); 179 } 180 updateSecureSetting()181 private void updateSecureSetting() { 182 // Write default package to secure setting every time there is a change. If OEM didn't 183 // supply a new value in their config, then we would write empty string. 184 Settings.Secure.putString( 185 mContext.getContentResolver(), 186 Settings.Secure.CONTEXTUAL_SEARCH_PACKAGE, 187 getContextualSearchPackageName()); 188 } 189 getContextualSearchPackageName()190 private String getContextualSearchPackageName() { 191 synchronized (this) { 192 return mTemporaryPackage != null ? mTemporaryPackage : mContext 193 .getResources().getString(R.string.config_defaultContextualSearchPackageName); 194 } 195 } 196 resetTemporaryPackage()197 void resetTemporaryPackage() { 198 synchronized (this) { 199 enforceOverridingPermission("resetTemporaryPackage"); 200 if (mTemporaryHandler != null) { 201 mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_PACKAGE); 202 mTemporaryHandler = null; 203 } 204 if (DEBUG_USER) Log.d(TAG, "mTemporaryPackage reset."); 205 mTemporaryPackage = null; 206 updateSecureSetting(); 207 } 208 } 209 setTemporaryPackage(@onNull String temporaryPackage, int durationMs)210 void setTemporaryPackage(@NonNull String temporaryPackage, int durationMs) { 211 synchronized (this) { 212 enforceOverridingPermission("setTemporaryPackage"); 213 final int maxDurationMs = MAX_TEMP_PACKAGE_DURATION_MS; 214 if (durationMs > maxDurationMs) { 215 throw new IllegalArgumentException( 216 "Max duration is " + maxDurationMs + " (called with " + durationMs + ")"); 217 } 218 if (mTemporaryHandler == null) { 219 mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) { 220 @Override 221 public void handleMessage(Message msg) { 222 if (msg.what == MSG_RESET_TEMPORARY_PACKAGE) { 223 synchronized (this) { 224 resetTemporaryPackage(); 225 } 226 } else { 227 Slog.wtf(TAG, "invalid handler msg: " + msg); 228 } 229 } 230 }; 231 } else { 232 mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_PACKAGE); 233 } 234 mTemporaryPackage = temporaryPackage; 235 updateSecureSetting(); 236 mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_PACKAGE, durationMs); 237 if (DEBUG_USER) Log.d(TAG, "mTemporaryPackage set to " + mTemporaryPackage); 238 } 239 } 240 resetTokenValidDurationMs()241 void resetTokenValidDurationMs() { 242 setTokenValidDurationMs(MAX_TOKEN_VALID_DURATION_MS); 243 } 244 setTokenValidDurationMs(int durationMs)245 void setTokenValidDurationMs(int durationMs) { 246 synchronized (this) { 247 enforceOverridingPermission("setTokenValidDurationMs"); 248 if (durationMs > MAX_TOKEN_VALID_DURATION_MS) { 249 throw new IllegalArgumentException( 250 "Token max duration is " + MAX_TOKEN_VALID_DURATION_MS + " (called with " 251 + durationMs + ")"); 252 } 253 mTokenValidDurationMs = durationMs; 254 if (DEBUG_USER) Log.d(TAG, "mTokenValidDurationMs set to " + durationMs); 255 } 256 } 257 getTokenValidDurationMs()258 private long getTokenValidDurationMs() { 259 synchronized (this) { 260 return mTokenValidDurationMs; 261 } 262 } 263 getResolvedLaunchIntent()264 private Intent getResolvedLaunchIntent() { 265 synchronized (this) { 266 // If mTemporaryPackage is not null, use it to get the ContextualSearch intent. 267 String csPkgName = getContextualSearchPackageName(); 268 if (csPkgName.isEmpty()) { 269 // Return null if csPackageName is not specified. 270 return null; 271 } 272 Intent launchIntent = new Intent( 273 ContextualSearchManager.ACTION_LAUNCH_CONTEXTUAL_SEARCH); 274 launchIntent.setPackage(csPkgName); 275 ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivity( 276 launchIntent, MATCH_FACTORY_ONLY); 277 if (resolveInfo == null) { 278 return null; 279 } 280 ComponentName componentName = resolveInfo.getComponentInfo().getComponentName(); 281 if (componentName == null) { 282 return null; 283 } 284 launchIntent.setComponent(componentName); 285 return launchIntent; 286 } 287 } 288 getContextualSearchIntent(int entrypoint, CallbackToken mToken)289 private Intent getContextualSearchIntent(int entrypoint, CallbackToken mToken) { 290 final Intent launchIntent = getResolvedLaunchIntent(); 291 if (launchIntent == null) { 292 return null; 293 } 294 295 if (DEBUG_USER) Log.d(TAG, "Launch component: " + launchIntent.getComponent()); 296 launchIntent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NO_ANIMATION 297 | FLAG_ACTIVITY_NO_USER_ACTION | FLAG_ACTIVITY_CLEAR_TASK); 298 launchIntent.putExtra( 299 ContextualSearchManager.EXTRA_INVOCATION_TIME_MS, 300 SystemClock.uptimeMillis()); 301 launchIntent.putExtra(ContextualSearchManager.EXTRA_ENTRYPOINT, entrypoint); 302 launchIntent.putExtra(ContextualSearchManager.EXTRA_TOKEN, mToken); 303 boolean isAssistDataAllowed = mAtmInternal.isAssistDataAllowed(); 304 final List<ActivityAssistInfo> records = mAtmInternal.getTopVisibleActivities(); 305 final List<IBinder> activityTokens = new ArrayList<>(records.size()); 306 ArrayList<String> visiblePackageNames = new ArrayList<>(); 307 boolean isManagedProfileVisible = false; 308 for (ActivityAssistInfo record : records) { 309 // Add the package name to the list only if assist data is allowed. 310 if (isAssistDataAllowed) { 311 visiblePackageNames.add(record.getComponentName().getPackageName()); 312 activityTokens.add(record.getActivityToken()); 313 } 314 if (mDpmInternal != null 315 && mDpmInternal.isUserOrganizationManaged(record.getUserId())) { 316 isManagedProfileVisible = true; 317 } 318 } 319 if (isAssistDataAllowed) { 320 try { 321 final String csPackage = Objects.requireNonNull(launchIntent.getPackage()); 322 final int csUid = mPackageManager.getPackageUid(csPackage, 0, 0); 323 mAssistDataRequester.requestAssistData( 324 activityTokens, 325 /* fetchData */ true, 326 /* fetchScreenshot */ false, 327 /* allowFetchData */ true, 328 /* allowFetchScreenshot */ false, 329 csUid, 330 csPackage, 331 null); 332 } catch (Exception e) { 333 Log.e(TAG, "Could not request assist data", e); 334 } 335 } 336 final ScreenCapture.ScreenshotHardwareBuffer shb; 337 if (mWmInternal != null) { 338 shb = mWmInternal.takeAssistScreenshot(Set.of( 339 TYPE_STATUS_BAR, 340 TYPE_NAVIGATION_BAR, 341 TYPE_NAVIGATION_BAR_PANEL, 342 TYPE_POINTER)); 343 } else { 344 shb = null; 345 } 346 final Bitmap bm = shb != null ? shb.asBitmap() : null; 347 // Now that everything is fetched, putting it in the launchIntent. 348 if (bm != null) { 349 launchIntent.putExtra(ContextualSearchManager.EXTRA_FLAG_SECURE_FOUND, 350 shb.containsSecureLayers()); 351 // Only put the screenshot if assist data is allowed 352 if (isAssistDataAllowed) { 353 launchIntent.putExtra(ContextualSearchManager.EXTRA_SCREENSHOT, bm.asShared()); 354 } 355 } 356 launchIntent.putExtra(ContextualSearchManager.EXTRA_IS_MANAGED_PROFILE_VISIBLE, 357 isManagedProfileVisible); 358 // Only put the list of visible package names if assist data is allowed 359 if (isAssistDataAllowed) { 360 launchIntent.putExtra(ContextualSearchManager.EXTRA_VISIBLE_PACKAGE_NAMES, 361 visiblePackageNames); 362 } 363 return launchIntent; 364 } 365 366 @RequiresPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS) invokeContextualSearchIntent(Intent launchIntent, final int userId)367 private int invokeContextualSearchIntent(Intent launchIntent, final int userId) { 368 // Contextual search starts with a frozen screen - so we launch without 369 // any system animations or starting window. 370 final ActivityOptions opts = ActivityOptions.makeCustomTaskAnimation(mContext, 371 /* enterResId= */ 0, /* exitResId= */ 0, null, null, null); 372 opts.setDisableStartingWindow(true); 373 return mAtmInternal.startActivityWithScreenshot(launchIntent, 374 mContext.getPackageName(), Binder.getCallingUid(), Binder.getCallingPid(), null, 375 opts.toBundle(), userId); 376 } 377 enforcePermission(@onNull final String func)378 private void enforcePermission(@NonNull final String func) { 379 Context ctx = getContext(); 380 if (!(ctx.checkCallingPermission(ACCESS_CONTEXTUAL_SEARCH) == PERMISSION_GRANTED 381 || isCallerTemporary())) { 382 String msg = "Permission Denial: Cannot call " + func + " from pid=" 383 + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid(); 384 throw new SecurityException(msg); 385 } 386 } 387 enforceOverridingPermission(@onNull final String func)388 private void enforceOverridingPermission(@NonNull final String func) { 389 if (!(Binder.getCallingUid() == Process.SHELL_UID 390 || Binder.getCallingUid() == Process.ROOT_UID 391 || Binder.getCallingUid() == Process.SYSTEM_UID)) { 392 String msg = "Permission Denial: Cannot override Contextual Search. Called " + func 393 + " from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid(); 394 throw new SecurityException(msg); 395 } 396 } 397 isCallerTemporary()398 private boolean isCallerTemporary() { 399 synchronized (this) { 400 return mTemporaryPackage != null 401 && mTemporaryPackage.equals( 402 getContext().getPackageManager().getNameForUid(Binder.getCallingUid())); 403 } 404 } 405 406 private class ContextualSearchManagerStub extends IContextualSearchManager.Stub { 407 @GuardedBy("this") 408 private Handler mTokenHandler; 409 private @Nullable CallbackToken mToken; 410 invalidateToken()411 private void invalidateToken() { 412 synchronized (this) { 413 if (mTokenHandler != null) { 414 mTokenHandler.removeMessages(MSG_INVALIDATE_TOKEN); 415 mTokenHandler = null; 416 } 417 if (DEBUG_USER) Log.d(TAG, "mToken invalidated."); 418 mToken = null; 419 } 420 } 421 issueToken()422 private void issueToken() { 423 synchronized (this) { 424 mToken = new CallbackToken(); 425 if (mTokenHandler == null) { 426 mTokenHandler = new Handler(Looper.getMainLooper(), null, true) { 427 @Override 428 public void handleMessage(Message msg) { 429 if (msg.what == MSG_INVALIDATE_TOKEN) { 430 invalidateToken(); 431 } else { 432 Slog.wtf(TAG, "invalid token handler msg: " + msg); 433 } 434 } 435 }; 436 } else { 437 mTokenHandler.removeMessages(MSG_INVALIDATE_TOKEN); 438 } 439 mTokenHandler.sendEmptyMessageDelayed( 440 MSG_INVALIDATE_TOKEN, getTokenValidDurationMs()); 441 } 442 } 443 444 @Override startContextualSearch(int entrypoint)445 public void startContextualSearch(int entrypoint) { 446 synchronized (this) { 447 if (DEBUG_USER) Log.d(TAG, "startContextualSearch"); 448 enforcePermission("startContextualSearch"); 449 final int callingUserId = Binder.getCallingUserHandle().getIdentifier(); 450 451 mAssistDataRequester.cancel(); 452 // Creates a new CallbackToken at mToken and an expiration handler. 453 issueToken(); 454 // We get the launch intent with the system server's identity because the system 455 // server has READ_FRAME_BUFFER permission to get the screenshot and because only 456 // the system server can invoke non-exported activities. 457 Binder.withCleanCallingIdentity(() -> { 458 Intent launchIntent = getContextualSearchIntent(entrypoint, mToken); 459 if (launchIntent != null) { 460 int result = invokeContextualSearchIntent(launchIntent, callingUserId); 461 if (DEBUG_USER) Log.d(TAG, "Launch result: " + result); 462 } 463 }); 464 } 465 } 466 467 @Override getContextualSearchState( @onNull IBinder token, @NonNull IContextualSearchCallback callback)468 public void getContextualSearchState( 469 @NonNull IBinder token, 470 @NonNull IContextualSearchCallback callback) { 471 if (DEBUG_USER) { 472 Log.i(TAG, "getContextualSearchState token: " + token + ", callback: " + callback); 473 } 474 if (mToken == null || !mToken.getToken().equals(token)) { 475 if (DEBUG_USER) { 476 Log.e(TAG, "getContextualSearchState: invalid token, returning error"); 477 } 478 try { 479 callback.onError( 480 new ParcelableException(new IllegalArgumentException("Invalid token"))); 481 } catch (RemoteException e) { 482 Log.e(TAG, "Could not invoke onError callback", e); 483 } 484 return; 485 } 486 invalidateToken(); 487 if (Flags.enableTokenRefresh()) { 488 issueToken(); 489 Bundle bundle = new Bundle(); 490 bundle.putParcelable(ContextualSearchManager.EXTRA_TOKEN, mToken); 491 // We get take the screenshot with the system server's identity because the system 492 // server has READ_FRAME_BUFFER permission to get the screenshot. 493 Binder.withCleanCallingIdentity(() -> { 494 if (mWmInternal != null) { 495 bundle.putParcelable(ContextualSearchManager.EXTRA_SCREENSHOT, 496 mWmInternal.takeAssistScreenshot(Set.of( 497 TYPE_STATUS_BAR, 498 TYPE_NAVIGATION_BAR, 499 TYPE_NAVIGATION_BAR_PANEL, 500 TYPE_POINTER)) 501 .asBitmap().asShared()); 502 } 503 try { 504 callback.onResult( 505 new ContextualSearchState(null, null, bundle)); 506 } catch (RemoteException e) { 507 Log.e(TAG, "Error invoking ContextualSearchCallback", e); 508 } 509 }); 510 } 511 synchronized (mLock) { 512 mStateCallback = callback; 513 } 514 mAssistDataRequester.processPendingAssistData(); 515 } 516 onShellCommand( @ullable FileDescriptor in, @Nullable FileDescriptor out, @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback, @NonNull ResultReceiver resultReceiver)517 public void onShellCommand( 518 @Nullable FileDescriptor in, 519 @Nullable FileDescriptor out, 520 @Nullable FileDescriptor err, 521 @NonNull String[] args, 522 @Nullable ShellCallback callback, 523 @NonNull ResultReceiver resultReceiver) { 524 new ContextualSearchManagerShellCommand(ContextualSearchManagerService.this) 525 .exec(this, in, out, err, args, callback, resultReceiver); 526 } 527 } 528 } 529