1 /*
2  * Copyright (C) 2018 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 package android.view.contentcapture;
17 
18 import static android.view.contentcapture.ContentCaptureManager.NO_SESSION_ID;
19 
20 import android.annotation.IntDef;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.SystemApi;
24 import android.app.TaskInfo;
25 import android.app.assist.ActivityId;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.LocusId;
29 import android.os.Bundle;
30 import android.os.IBinder;
31 import android.os.Parcel;
32 import android.os.Parcelable;
33 import android.view.Display;
34 import android.view.View;
35 
36 import com.android.internal.util.Preconditions;
37 
38 import java.io.PrintWriter;
39 import java.lang.annotation.Retention;
40 import java.lang.annotation.RetentionPolicy;
41 import java.util.Objects;
42 
43 /**
44  * Context associated with a {@link ContentCaptureSession} - see {@link ContentCaptureManager} for
45  * more info.
46  */
47 public final class ContentCaptureContext implements Parcelable {
48 
49     /*
50      * IMPLEMENTATION NOTICE:
51      *
52      * This object contains both the info that's explicitly added by apps (hence it's public), but
53      * it also contains info injected by the server (and are accessible through @SystemApi methods).
54      */
55 
56     /**
57      * Flag used to indicate that the app explicitly disabled content capture for the activity
58      * (using {@link ContentCaptureManager#setContentCaptureEnabled(boolean)}),
59      * in which case the service will just receive activity-level events.
60      *
61      * @hide
62      */
63     @SystemApi
64     public static final int FLAG_DISABLED_BY_APP = 0x1;
65 
66     /**
67      * Flag used to indicate that the activity's window is tagged with
68      * {@link android.view.Display#FLAG_SECURE}, in which case the service will just receive
69      * activity-level events.
70      *
71      * @hide
72      */
73     @SystemApi
74     public static final int FLAG_DISABLED_BY_FLAG_SECURE = 0x2;
75 
76     /**
77      * Flag used when the event is sent because the Android System reconnected to the service (for
78      * example, after its process died).
79      *
80      * @hide
81      */
82     @SystemApi
83     public static final int FLAG_RECONNECTED = 0x4;
84 
85     /**
86      * Flag used to disable flush when receiving a VIEW_TREE_APPEARING event.
87      *
88      * @hide
89      */
90     public static final int FLAG_DISABLED_FLUSH_FOR_VIEW_TREE_APPEARING = 1 << 3;
91 
92     /** @hide */
93     @IntDef(flag = true, prefix = { "FLAG_" }, value = {
94             FLAG_DISABLED_BY_APP,
95             FLAG_DISABLED_BY_FLAG_SECURE,
96             FLAG_RECONNECTED,
97             FLAG_DISABLED_FLUSH_FOR_VIEW_TREE_APPEARING
98     })
99     @Retention(RetentionPolicy.SOURCE)
100     @interface ContextCreationFlags{}
101 
102     /**
103      * Flag indicating if this object has the app-provided context (which is set on
104      * {@link ContentCaptureSession#createContentCaptureSession(ContentCaptureContext)}).
105      */
106     private final boolean mHasClientContext;
107 
108     // Fields below are set by app on Builder
109     private final @Nullable Bundle mExtras;
110     private final @Nullable LocusId mId;
111 
112     // Fields below are set by server when the session starts
113     private final @Nullable ComponentName mComponentName;
114     private final int mFlags;
115     private final int mDisplayId;
116     private final ActivityId mActivityId;
117     private final IBinder mWindowToken;
118 
119     // Fields below are set by the service upon "delivery" and are not marshalled in the parcel
120     private int mParentSessionId = NO_SESSION_ID;
121 
122     /** @hide */
ContentCaptureContext(@ullable ContentCaptureContext clientContext, @NonNull ActivityId activityId, @NonNull ComponentName componentName, int displayId, IBinder windowToken, int flags)123     public ContentCaptureContext(@Nullable ContentCaptureContext clientContext,
124             @NonNull ActivityId activityId, @NonNull ComponentName componentName, int displayId,
125             IBinder windowToken, int flags) {
126         if (clientContext != null) {
127             mHasClientContext = true;
128             mExtras = clientContext.mExtras;
129             mId = clientContext.mId;
130         } else {
131             mHasClientContext = false;
132             mExtras = null;
133             mId = null;
134         }
135         mComponentName = Objects.requireNonNull(componentName);
136         mFlags = flags;
137         mDisplayId = displayId;
138         mActivityId = activityId;
139         mWindowToken = windowToken;
140     }
141 
ContentCaptureContext(@onNull Builder builder)142     private ContentCaptureContext(@NonNull Builder builder) {
143         mHasClientContext = true;
144         mExtras = builder.mExtras;
145         mId = builder.mId;
146 
147         mComponentName  = null;
148         mFlags = 0;
149         mDisplayId = Display.INVALID_DISPLAY;
150         mActivityId = null;
151         mWindowToken = null;
152     }
153 
154     /** @hide */
ContentCaptureContext(@ullable ContentCaptureContext original, int extraFlags)155     public ContentCaptureContext(@Nullable ContentCaptureContext original, int extraFlags) {
156         mHasClientContext = original.mHasClientContext;
157         mExtras = original.mExtras;
158         mId = original.mId;
159         mComponentName = original.mComponentName;
160         mFlags = original.mFlags | extraFlags;
161         mDisplayId = original.mDisplayId;
162         mActivityId = original.mActivityId;
163         mWindowToken = original.mWindowToken;
164     }
165 
166     /**
167      * Gets the (optional) extras set by the app (through {@link Builder#setExtras(Bundle)}).
168      *
169      * <p>It can be used to provide vendor-specific data that can be modified and examined.
170      */
171     @Nullable
getExtras()172     public Bundle getExtras() {
173         return mExtras;
174     }
175 
176     /**
177      * Gets the context id.
178      */
179     @Nullable
getLocusId()180     public LocusId getLocusId() {
181         return mId;
182     }
183 
184     /**
185      * Gets the id of the {@link TaskInfo task} associated with this context.
186      *
187      * @hide
188      */
189     @SystemApi
getTaskId()190     public int getTaskId() {
191         return mHasClientContext ? 0 : mActivityId.getTaskId();
192     }
193 
194     /**
195      * Gets the activity associated with this context, or {@code null} when it is a child session.
196      *
197      * @hide
198      */
199     @SystemApi
getActivityComponent()200     public @Nullable ComponentName getActivityComponent() {
201         return mComponentName;
202     }
203 
204     /**
205      * Gets the Activity id information associated with this context, or {@code null} when it is a
206      * child session.
207      *
208      * @hide
209      */
210     @SystemApi
211     @Nullable
getActivityId()212     public ActivityId getActivityId() {
213         return mHasClientContext ? null : mActivityId;
214     }
215 
216     /**
217      * Gets the id of the session that originated this session (through
218      * {@link ContentCaptureSession#createContentCaptureSession(ContentCaptureContext)}),
219      * or {@code null} if this is the main session associated with the Activity's {@link Context}.
220      *
221      * @hide
222      */
223     @SystemApi
getParentSessionId()224     public @Nullable ContentCaptureSessionId getParentSessionId() {
225         return mParentSessionId == NO_SESSION_ID ? null
226                 : new ContentCaptureSessionId(mParentSessionId);
227     }
228 
229     /** @hide */
setParentSessionId(int parentSessionId)230     public void setParentSessionId(int parentSessionId) {
231         mParentSessionId = parentSessionId;
232     }
233 
234     /**
235      * Gets the ID of the display associated with this context, as defined by
236      * {G android.hardware.display.DisplayManager#getDisplay(int) DisplayManager.getDisplay()}.
237      *
238      * @hide
239      */
240     @SystemApi
getDisplayId()241     public int getDisplayId() {
242         return mDisplayId;
243     }
244 
245     /**
246      * Gets the window token of the activity associated with this context.
247      *
248      * <p>The token can be used to attach relevant overlay views to the activity's window. This can
249      * be done through {@link android.view.WindowManager.LayoutParams#token}.
250      *
251      * @hide
252      */
253     @SystemApi
254     @Nullable
getWindowToken()255     public IBinder getWindowToken() {
256         return mWindowToken;
257     }
258 
259     /**
260      * Gets the flags associated with this context.
261      *
262      * @return any combination of {@link #FLAG_DISABLED_BY_FLAG_SECURE},
263      * {@link #FLAG_DISABLED_BY_APP}, {@link #FLAG_RECONNECTED} and {@link
264      * #FLAG_DISABLED_FLUSH_FOR_VIEW_TREE_APPEARING}.
265      *
266      * @hide
267      */
268     @SystemApi
getFlags()269     public @ContextCreationFlags int getFlags() {
270         return mFlags;
271     }
272 
273     /**
274      * Helper that creates a {@link ContentCaptureContext} associated with the given {@code id}.
275      */
276     @NonNull
forLocusId(@onNull String id)277     public static ContentCaptureContext forLocusId(@NonNull String id) {
278         return new Builder(new LocusId(id)).build();
279     }
280 
281     /**
282      * Builder for {@link ContentCaptureContext} objects.
283      */
284     public static final class Builder {
285         private Bundle mExtras;
286         private final LocusId mId;
287         private boolean mDestroyed;
288 
289         /**
290          * Creates a new builder.
291          *
292          * <p>The context must have an id, which is usually one of the following:
293          *
294          * <ul>
295          *   <li>A URL representing a web page (or {@code IFRAME}) that's being rendered by the
296          *   activity (See {@link View#setContentCaptureSession(ContentCaptureSession)} for an
297          *   example).
298          *   <li>A unique identifier of the application state (for example, a conversation between
299          *   2 users in a chat app).
300          * </ul>
301          *
302          * <p>See {@link ContentCaptureManager} for more info about the content capture context.
303          *
304          * @param id id associated with this context.
305          */
Builder(@onNull LocusId id)306         public Builder(@NonNull LocusId id) {
307             mId = Objects.requireNonNull(id);
308         }
309 
310         /**
311          * Sets extra options associated with this context.
312          *
313          * <p>It can be used to provide vendor-specific data that can be modified and examined.
314          *
315          * @param extras extra options.
316          * @return this builder.
317          *
318          * @throws IllegalStateException if {@link #build()} was already called.
319          */
320         @NonNull
setExtras(@onNull Bundle extras)321         public Builder setExtras(@NonNull Bundle extras) {
322             mExtras =  Objects.requireNonNull(extras);
323             throwIfDestroyed();
324             return this;
325         }
326 
327         /**
328          * Builds the {@link ContentCaptureContext}.
329          *
330          * @throws IllegalStateException if {@link #build()} was already called.
331          *
332          * @return the built {@code ContentCaptureContext}
333          */
334         @NonNull
build()335         public ContentCaptureContext build() {
336             throwIfDestroyed();
337             mDestroyed = true;
338             return new ContentCaptureContext(this);
339         }
340 
throwIfDestroyed()341         private void throwIfDestroyed() {
342             Preconditions.checkState(!mDestroyed, "Already called #build()");
343         }
344     }
345 
346     /**
347      * @hide
348      */
349     // TODO(b/111276913): dump to proto as well
dump(PrintWriter pw)350     public void dump(PrintWriter pw) {
351         if (mComponentName != null) {
352             pw.print("activity="); pw.print(mComponentName.flattenToShortString());
353         }
354         if (mId != null) {
355             pw.print(", id="); mId.dump(pw);
356         }
357         pw.print(", activityId="); pw.print(mActivityId);
358         pw.print(", displayId="); pw.print(mDisplayId);
359         pw.print(", windowToken="); pw.print(mWindowToken);
360         if (mParentSessionId != NO_SESSION_ID) {
361             pw.print(", parentId="); pw.print(mParentSessionId);
362         }
363         if (mFlags > 0) {
364             pw.print(", flags="); pw.print(mFlags);
365         }
366         if (mExtras != null) {
367             // NOTE: cannot dump because it could contain PII
368             pw.print(", hasExtras");
369         }
370     }
371 
fromServer()372     private boolean fromServer() {
373         return mComponentName != null;
374     }
375 
376     @Override
toString()377     public String toString() {
378         final StringBuilder builder = new StringBuilder("Context[");
379 
380         if (fromServer()) {
381             builder.append("act=").append(ComponentName.flattenToShortString(mComponentName))
382                 .append(", activityId=").append(mActivityId)
383                 .append(", displayId=").append(mDisplayId)
384                 .append(", windowToken=").append(mWindowToken)
385                 .append(", flags=").append(mFlags);
386         } else {
387             builder.append("id=").append(mId);
388             if (mExtras != null) {
389                 // NOTE: cannot print because it could contain PII
390                 builder.append(", hasExtras");
391             }
392         }
393         if (mParentSessionId != NO_SESSION_ID) {
394             builder.append(", parentId=").append(mParentSessionId);
395         }
396         return builder.append(']').toString();
397     }
398 
399     @Override
describeContents()400     public int describeContents() {
401         return 0;
402     }
403 
404     @Override
writeToParcel(Parcel parcel, int flags)405     public void writeToParcel(Parcel parcel, int flags) {
406         parcel.writeInt(mHasClientContext ? 1 : 0);
407         if (mHasClientContext) {
408             parcel.writeParcelable(mId, flags);
409             parcel.writeBundle(mExtras);
410         }
411         parcel.writeParcelable(mComponentName, flags);
412         if (fromServer()) {
413             parcel.writeInt(mDisplayId);
414             parcel.writeStrongBinder(mWindowToken);
415             parcel.writeInt(mFlags);
416             mActivityId.writeToParcel(parcel, flags);
417         }
418     }
419 
420     public static final @android.annotation.NonNull Parcelable.Creator<ContentCaptureContext> CREATOR =
421             new Parcelable.Creator<ContentCaptureContext>() {
422 
423         @Override
424         @NonNull
425         public ContentCaptureContext createFromParcel(Parcel parcel) {
426             final boolean hasClientContext = parcel.readInt() == 1;
427 
428             final ContentCaptureContext clientContext;
429             if (hasClientContext) {
430                 // Must reconstruct the client context using the Builder API
431                 final LocusId id = parcel.readParcelable(null, android.content.LocusId.class);
432                 final Bundle extras = parcel.readBundle();
433                 final Builder builder = new Builder(id);
434                 if (extras != null) builder.setExtras(extras);
435                 clientContext = new ContentCaptureContext(builder);
436             } else {
437                 clientContext = null;
438             }
439             final ComponentName componentName = parcel.readParcelable(null, android.content.ComponentName.class);
440             if (componentName == null) {
441                 // Client-state only
442                 return clientContext;
443             } else {
444                 final int displayId = parcel.readInt();
445                 final IBinder windowToken = parcel.readStrongBinder();
446                 final int flags = parcel.readInt();
447                 final ActivityId activityId = new ActivityId(parcel);
448 
449                 return new ContentCaptureContext(clientContext, activityId, componentName,
450                         displayId, windowToken, flags);
451             }
452         }
453 
454         @Override
455         @NonNull
456         public ContentCaptureContext[] newArray(int size) {
457             return new ContentCaptureContext[size];
458         }
459     };
460 }
461