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