1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
|
/*
* Copyright (C) 2018 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 android.service.contentcapture;
import static android.view.contentcapture.ContentCaptureHelper.sDebug;
import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
import static android.view.contentcapture.ContentCaptureHelper.toList;
import static android.view.contentcapture.ContentCaptureSession.NO_SESSION_ID;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.app.Service;
import android.content.ComponentName;
import android.content.ContentCaptureOptions;
import android.content.Intent;
import android.content.pm.ParceledListSlice;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import android.util.Slog;
import android.util.SparseIntArray;
import android.util.StatsLog;
import android.view.contentcapture.ContentCaptureCondition;
import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureEvent;
import android.view.contentcapture.ContentCaptureManager;
import android.view.contentcapture.ContentCaptureSession;
import android.view.contentcapture.ContentCaptureSessionId;
import android.view.contentcapture.DataRemovalRequest;
import android.view.contentcapture.IContentCaptureDirectManager;
import android.view.contentcapture.MainContentCaptureSession;
import com.android.internal.os.IResultReceiver;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.List;
import java.util.Set;
/**
* A service used to capture the content of the screen to provide contextual data in other areas of
* the system such as Autofill.
*
* @hide
*/
@SystemApi
@TestApi
public abstract class ContentCaptureService extends Service {
private static final String TAG = ContentCaptureService.class.getSimpleName();
/**
* The {@link Intent} that must be declared as handled by the service.
*
* <p>To be supported, the service must also require the
* {@link android.Manifest.permission#BIND_CONTENT_CAPTURE_SERVICE} permission so
* that other applications can not abuse it.
*/
public static final String SERVICE_INTERFACE =
"android.service.contentcapture.ContentCaptureService";
/**
* Name under which a ContentCaptureService component publishes information about itself.
*
* <p>This meta-data should reference an XML resource containing a
* <code><{@link
* android.R.styleable#ContentCaptureService content-capture-service}></code> tag.
*
* <p>Here's an example of how to use it on {@code AndroidManifest.xml}:
*
* <pre>
* <service android:name=".MyContentCaptureService"
* android:permission="android.permission.BIND_CONTENT_CAPTURE_SERVICE">
* <intent-filter>
* <action android:name="android.service.contentcapture.ContentCaptureService" />
* </intent-filter>
*
* <meta-data
* android:name="android.content_capture"
* android:resource="@xml/my_content_capture_service"/>
* </service>
* </pre>
*
* <p>And then on {@code res/xml/my_content_capture_service.xml}:
*
* <pre>
* <content-capture-service xmlns:android="http://schemas.android.com/apk/res/android"
* android:settingsActivity="my.package.MySettingsActivity">
* </content-capture-service>
* </pre>
*/
public static final String SERVICE_META_DATA = "android.content_capture";
private Handler mHandler;
private IContentCaptureServiceCallback mCallback;
private long mCallerMismatchTimeout = 1000;
private long mLastCallerMismatchLog;
/**
* Binder that receives calls from the system server.
*/
private final IContentCaptureService mServerInterface = new IContentCaptureService.Stub() {
@Override
public void onConnected(IBinder callback, boolean verbose, boolean debug) {
sVerbose = verbose;
sDebug = debug;
mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnConnected,
ContentCaptureService.this, callback));
}
@Override
public void onDisconnected() {
mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnDisconnected,
ContentCaptureService.this));
}
@Override
public void onSessionStarted(ContentCaptureContext context, int sessionId, int uid,
IResultReceiver clientReceiver, int initialState) {
mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnCreateSession,
ContentCaptureService.this, context, sessionId, uid, clientReceiver,
initialState));
}
@Override
public void onActivitySnapshot(int sessionId, SnapshotData snapshotData) {
mHandler.sendMessage(
obtainMessage(ContentCaptureService::handleOnActivitySnapshot,
ContentCaptureService.this, sessionId, snapshotData));
}
@Override
public void onSessionFinished(int sessionId) {
mHandler.sendMessage(obtainMessage(ContentCaptureService::handleFinishSession,
ContentCaptureService.this, sessionId));
}
@Override
public void onDataRemovalRequest(DataRemovalRequest request) {
mHandler.sendMessage(
obtainMessage(ContentCaptureService::handleOnDataRemovalRequest,
ContentCaptureService.this, request));
}
@Override
public void onActivityEvent(ActivityEvent event) {
mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnActivityEvent,
ContentCaptureService.this, event));
}
};
/**
* Binder that receives calls from the app.
*/
private final IContentCaptureDirectManager mClientInterface =
new IContentCaptureDirectManager.Stub() {
@Override
public void sendEvents(@SuppressWarnings("rawtypes") ParceledListSlice events, int reason,
ContentCaptureOptions options) {
mHandler.sendMessage(obtainMessage(ContentCaptureService::handleSendEvents,
ContentCaptureService.this, Binder.getCallingUid(), events, reason, options));
}
};
/**
* UIDs associated with each session.
*
* <p>This map is populated when an session is started, which is called by the system server
* and can be trusted. Then subsequent calls made by the app are verified against this map.
*/
private final SparseIntArray mSessionUids = new SparseIntArray();
@CallSuper
@Override
public void onCreate() {
super.onCreate();
mHandler = new Handler(Looper.getMainLooper(), null, true);
}
/** @hide */
@Override
public final IBinder onBind(Intent intent) {
if (SERVICE_INTERFACE.equals(intent.getAction())) {
return mServerInterface.asBinder();
}
Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
return null;
}
/**
* Explicitly limits content capture to the given packages and activities.
*
* <p>To reset the whitelist, call it passing {@code null} to both arguments.
*
* <p>Useful when the service wants to restrict content capture to a category of apps, like
* chat apps. For example, if the service wants to support view captures on all activities of
* app {@code ChatApp1} and just activities {@code act1} and {@code act2} of {@code ChatApp2},
* it would call: {@code setContentCaptureWhitelist(Sets.newArraySet("ChatApp1"),
* Sets.newArraySet(new ComponentName("ChatApp2", "act1"),
* new ComponentName("ChatApp2", "act2")));}
*/
public final void setContentCaptureWhitelist(@Nullable Set<String> packages,
@Nullable Set<ComponentName> activities) {
final IContentCaptureServiceCallback callback = mCallback;
if (callback == null) {
Log.w(TAG, "setContentCaptureWhitelist(): no server callback");
return;
}
try {
callback.setContentCaptureWhitelist(toList(packages), toList(activities));
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Explicitly sets the conditions for which content capture should be available by an app.
*
* <p>Typically used to restrict content capture to a few websites on browser apps. Example:
*
* <code>
* ArraySet<ContentCaptureCondition> conditions = new ArraySet<>(1);
* conditions.add(new ContentCaptureCondition(new LocusId("^https://.*\\.example\\.com$"),
* ContentCaptureCondition.FLAG_IS_REGEX));
* service.setContentCaptureConditions("com.example.browser_app", conditions);
*
* </code>
*
* <p>NOTE: </p> this method doesn't automatically disable content capture for the given
* conditions; it's up to the {@code packageName} implementation to call
* {@link ContentCaptureManager#getContentCaptureConditions()} and disable it accordingly.
*
* @param packageName name of the packages where the restrictions are set.
* @param conditions list of conditions, or {@code null} to reset the conditions for the
* package.
*/
public final void setContentCaptureConditions(@NonNull String packageName,
@Nullable Set<ContentCaptureCondition> conditions) {
final IContentCaptureServiceCallback callback = mCallback;
if (callback == null) {
Log.w(TAG, "setContentCaptureConditions(): no server callback");
return;
}
try {
callback.setContentCaptureConditions(packageName, toList(conditions));
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Called when the Android system connects to service.
*
* <p>You should generally do initialization here rather than in {@link #onCreate}.
*/
public void onConnected() {
Slog.i(TAG, "bound to " + getClass().getName());
}
/**
* Creates a new content capture session.
*
* @param context content capture context
* @param sessionId the session's Id
*/
public void onCreateContentCaptureSession(@NonNull ContentCaptureContext context,
@NonNull ContentCaptureSessionId sessionId) {
if (sVerbose) {
Log.v(TAG, "onCreateContentCaptureSession(id=" + sessionId + ", ctx=" + context + ")");
}
}
/**
* Notifies the service of {@link ContentCaptureEvent events} associated with a content capture
* session.
*
* @param sessionId the session's Id
* @param event the event
*/
public void onContentCaptureEvent(@NonNull ContentCaptureSessionId sessionId,
@NonNull ContentCaptureEvent event) {
if (sVerbose) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")");
}
/**
* Notifies the service that the app requested to remove content capture data.
*
* @param request the content capture data requested to be removed
*/
public void onDataRemovalRequest(@NonNull DataRemovalRequest request) {
if (sVerbose) Log.v(TAG, "onDataRemovalRequest()");
}
/**
* Notifies the service of {@link SnapshotData snapshot data} associated with a session.
*
* @param sessionId the session's Id
* @param snapshotData the data
*/
public void onActivitySnapshot(@NonNull ContentCaptureSessionId sessionId,
@NonNull SnapshotData snapshotData) {
if (sVerbose) Log.v(TAG, "onActivitySnapshot(id=" + sessionId + ")");
}
/**
* Notifies the service of an activity-level event that is not associated with a session.
*
* <p>This method can be used to track some high-level events for all activities, even those
* that are not whitelisted for Content Capture.
*
* @param event high-level activity event
*/
public void onActivityEvent(@NonNull ActivityEvent event) {
if (sVerbose) Log.v(TAG, "onActivityEvent(): " + event);
}
/**
* Destroys the content capture session.
*
* @param sessionId the id of the session to destroy
* */
public void onDestroyContentCaptureSession(@NonNull ContentCaptureSessionId sessionId) {
if (sVerbose) Log.v(TAG, "onDestroyContentCaptureSession(id=" + sessionId + ")");
}
/**
* Disables the Content Capture service for the given user.
*/
public final void disableSelf() {
if (sDebug) Log.d(TAG, "disableSelf()");
final IContentCaptureServiceCallback callback = mCallback;
if (callback == null) {
Log.w(TAG, "disableSelf(): no server callback");
return;
}
try {
callback.disableSelf();
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Called when the Android system disconnects from the service.
*
* <p> At this point this service may no longer be an active {@link ContentCaptureService}.
* It should not make calls on {@link ContentCaptureManager} that requires the caller to be
* the current service.
*/
public void onDisconnected() {
Slog.i(TAG, "unbinding from " + getClass().getName());
}
@Override
@CallSuper
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.print("Debug: "); pw.print(sDebug); pw.print(" Verbose: "); pw.println(sVerbose);
final int size = mSessionUids.size();
pw.print("Number sessions: "); pw.println(size);
if (size > 0) {
final String prefix = " ";
for (int i = 0; i < size; i++) {
pw.print(prefix); pw.print(mSessionUids.keyAt(i));
pw.print(": uid="); pw.println(mSessionUids.valueAt(i));
}
}
}
private void handleOnConnected(@NonNull IBinder callback) {
mCallback = IContentCaptureServiceCallback.Stub.asInterface(callback);
onConnected();
}
private void handleOnDisconnected() {
onDisconnected();
mCallback = null;
}
//TODO(b/111276913): consider caching the InteractionSessionId for the lifetime of the session,
// so we don't need to create a temporary InteractionSessionId for each event.
private void handleOnCreateSession(@NonNull ContentCaptureContext context,
int sessionId, int uid, IResultReceiver clientReceiver, int initialState) {
mSessionUids.put(sessionId, uid);
onCreateContentCaptureSession(context, new ContentCaptureSessionId(sessionId));
final int clientFlags = context.getFlags();
int stateFlags = 0;
if ((clientFlags & ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE) != 0) {
stateFlags |= ContentCaptureSession.STATE_FLAG_SECURE;
}
if ((clientFlags & ContentCaptureContext.FLAG_DISABLED_BY_APP) != 0) {
stateFlags |= ContentCaptureSession.STATE_BY_APP;
}
if (stateFlags == 0) {
stateFlags = initialState;
} else {
stateFlags |= ContentCaptureSession.STATE_DISABLED;
}
setClientState(clientReceiver, stateFlags, mClientInterface.asBinder());
}
private void handleSendEvents(int uid,
@NonNull ParceledListSlice<ContentCaptureEvent> parceledEvents, int reason,
@Nullable ContentCaptureOptions options) {
final List<ContentCaptureEvent> events = parceledEvents.getList();
if (events.isEmpty()) {
Log.w(TAG, "handleSendEvents() received empty list of events");
return;
}
// Metrics.
final FlushMetrics metrics = new FlushMetrics();
ComponentName activityComponent = null;
// Most events belong to the same session, so we can keep a reference to the last one
// to avoid creating too many ContentCaptureSessionId objects
int lastSessionId = NO_SESSION_ID;
ContentCaptureSessionId sessionId = null;
for (int i = 0; i < events.size(); i++) {
final ContentCaptureEvent event = events.get(i);
if (!handleIsRightCallerFor(event, uid)) continue;
int sessionIdInt = event.getSessionId();
if (sessionIdInt != lastSessionId) {
sessionId = new ContentCaptureSessionId(sessionIdInt);
lastSessionId = sessionIdInt;
if (i != 0) {
writeFlushMetrics(lastSessionId, activityComponent, metrics, options, reason);
metrics.reset();
}
}
final ContentCaptureContext clientContext = event.getContentCaptureContext();
if (activityComponent == null && clientContext != null) {
activityComponent = clientContext.getActivityComponent();
}
switch (event.getType()) {
case ContentCaptureEvent.TYPE_SESSION_STARTED:
clientContext.setParentSessionId(event.getParentSessionId());
mSessionUids.put(sessionIdInt, uid);
onCreateContentCaptureSession(clientContext, sessionId);
metrics.sessionStarted++;
break;
case ContentCaptureEvent.TYPE_SESSION_FINISHED:
mSessionUids.delete(sessionIdInt);
onDestroyContentCaptureSession(sessionId);
metrics.sessionFinished++;
break;
case ContentCaptureEvent.TYPE_VIEW_APPEARED:
onContentCaptureEvent(sessionId, event);
metrics.viewAppearedCount++;
break;
case ContentCaptureEvent.TYPE_VIEW_DISAPPEARED:
onContentCaptureEvent(sessionId, event);
metrics.viewDisappearedCount++;
break;
case ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED:
onContentCaptureEvent(sessionId, event);
metrics.viewTextChangedCount++;
break;
default:
onContentCaptureEvent(sessionId, event);
}
}
writeFlushMetrics(lastSessionId, activityComponent, metrics, options, reason);
}
private void handleOnActivitySnapshot(int sessionId, @NonNull SnapshotData snapshotData) {
onActivitySnapshot(new ContentCaptureSessionId(sessionId), snapshotData);
}
private void handleFinishSession(int sessionId) {
mSessionUids.delete(sessionId);
onDestroyContentCaptureSession(new ContentCaptureSessionId(sessionId));
}
private void handleOnDataRemovalRequest(@NonNull DataRemovalRequest request) {
onDataRemovalRequest(request);
}
private void handleOnActivityEvent(@NonNull ActivityEvent event) {
onActivityEvent(event);
}
/**
* Checks if the given {@code uid} owns the session associated with the event.
*/
private boolean handleIsRightCallerFor(@NonNull ContentCaptureEvent event, int uid) {
final int sessionId;
switch (event.getType()) {
case ContentCaptureEvent.TYPE_SESSION_STARTED:
case ContentCaptureEvent.TYPE_SESSION_FINISHED:
sessionId = event.getParentSessionId();
break;
default:
sessionId = event.getSessionId();
}
if (mSessionUids.indexOfKey(sessionId) < 0) {
if (sVerbose) {
Log.v(TAG, "handleIsRightCallerFor(" + event + "): no session for " + sessionId
+ ": " + mSessionUids);
}
// Just ignore, as the session could have been finished already
return false;
}
final int rightUid = mSessionUids.get(sessionId);
if (rightUid != uid) {
Log.e(TAG, "invalid call from UID " + uid + ": session " + sessionId + " belongs to "
+ rightUid);
long now = System.currentTimeMillis();
if (now - mLastCallerMismatchLog > mCallerMismatchTimeout) {
StatsLog.write(StatsLog.CONTENT_CAPTURE_CALLER_MISMATCH_REPORTED,
getPackageManager().getNameForUid(rightUid),
getPackageManager().getNameForUid(uid));
mLastCallerMismatchLog = now;
}
return false;
}
return true;
}
/**
* Sends the state of the {@link ContentCaptureManager} in the client app.
*
* @param clientReceiver receiver in the client app.
* @param sessionState state of the session
* @param binder handle to the {@code IContentCaptureDirectManager} object that resides in the
* service.
* @hide
*/
public static void setClientState(@NonNull IResultReceiver clientReceiver,
int sessionState, @Nullable IBinder binder) {
try {
final Bundle extras;
if (binder != null) {
extras = new Bundle();
extras.putBinder(MainContentCaptureSession.EXTRA_BINDER, binder);
} else {
extras = null;
}
clientReceiver.send(sessionState, extras);
} catch (RemoteException e) {
Slog.w(TAG, "Error async reporting result to client: " + e);
}
}
/**
* Logs the metrics for content capture events flushing.
*/
private void writeFlushMetrics(int sessionId, @Nullable ComponentName app,
@NonNull FlushMetrics flushMetrics, @Nullable ContentCaptureOptions options,
int flushReason) {
if (mCallback == null) {
Log.w(TAG, "writeSessionFlush(): no server callback");
return;
}
try {
mCallback.writeSessionFlush(sessionId, app, flushMetrics, options, flushReason);
} catch (RemoteException e) {
Log.e(TAG, "failed to write flush metrics: " + e);
}
}
}
|