1 /*
2  * Copyright 2023 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 androidx.appsearch.app;
18 
19 import android.os.Parcel;
20 import android.os.Parcelable;
21 
22 import androidx.annotation.NonNull;
23 import androidx.annotation.Nullable;
24 import androidx.annotation.RestrictTo;
25 import androidx.appsearch.annotation.CanIgnoreReturnValue;
26 import androidx.appsearch.flags.FlaggedApi;
27 import androidx.appsearch.flags.Flags;
28 import androidx.appsearch.safeparcel.AbstractSafeParcelable;
29 import androidx.appsearch.safeparcel.PackageIdentifierParcel;
30 import androidx.appsearch.safeparcel.SafeParcelable;
31 import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityConfigCreator;
32 import androidx.collection.ArraySet;
33 
34 import java.util.ArrayList;
35 import java.util.List;
36 import java.util.Objects;
37 import java.util.Set;
38 
39 /**
40  * A class to hold a all necessary Visibility information corresponding to the same schema. This
41  * pattern allows for easier association of these documents.
42  *
43  * <p> This does not correspond to any schema, the properties held in this class are kept in two
44  * separate schemas, VisibilityConfig and PublicAclOverlay.
45  */
46 @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
47 @SafeParcelable.Class(creator = "VisibilityConfigCreator")
48 // TODO(b/384721898): Switch to JSpecify annotations
49 @SuppressWarnings({"HiddenSuperclass", "JSpecifyNullness"})
50 public final class SchemaVisibilityConfig extends AbstractSafeParcelable {
51     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
52     public static final @NonNull Parcelable.Creator<SchemaVisibilityConfig> CREATOR =
53             new VisibilityConfigCreator();
54 
55     @Field(id = 1)
56     final @NonNull List<PackageIdentifierParcel> mAllowedPackages;
57 
58     @Field(id = 2)
59     final @NonNull List<VisibilityPermissionConfig> mRequiredPermissions;
60 
61     @Field(id = 3)
62     final @Nullable PackageIdentifierParcel mPubliclyVisibleTargetPackage;
63 
64     private @Nullable Integer mHashCode;
65     private @Nullable List<PackageIdentifier> mAllowedPackagesCached;
66     private @Nullable Set<Set<Integer>> mRequiredPermissionsCached;
67 
68     @Constructor
SchemaVisibilityConfig( @aramid = 1) @onNull List<PackageIdentifierParcel> allowedPackages, @Param(id = 2) @NonNull List<VisibilityPermissionConfig> requiredPermissions, @Param(id = 3) @Nullable PackageIdentifierParcel publiclyVisibleTargetPackage)69     SchemaVisibilityConfig(
70             @Param(id = 1) @NonNull List<PackageIdentifierParcel> allowedPackages,
71             @Param(id = 2) @NonNull List<VisibilityPermissionConfig> requiredPermissions,
72             @Param(id = 3) @Nullable PackageIdentifierParcel publiclyVisibleTargetPackage) {
73         mAllowedPackages = Objects.requireNonNull(allowedPackages);
74         mRequiredPermissions = Objects.requireNonNull(requiredPermissions);
75         mPubliclyVisibleTargetPackage = publiclyVisibleTargetPackage;
76     }
77 
78      /** Returns a list of {@link PackageIdentifier}s of packages that can access this schema. */
getAllowedPackages()79     public @NonNull List<PackageIdentifier> getAllowedPackages() {
80         if (mAllowedPackagesCached == null) {
81             mAllowedPackagesCached = new ArrayList<>(mAllowedPackages.size());
82             for (int i = 0; i < mAllowedPackages.size(); i++) {
83                 mAllowedPackagesCached.add(new PackageIdentifier(mAllowedPackages.get(i)));
84             }
85         }
86         return mAllowedPackagesCached;
87     }
88 
89     /**
90      * Returns an array of Integers representing Android Permissions that the caller must hold to
91      * access the schema this {@link SchemaVisibilityConfig} represents.
92      * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set)
93      */
getRequiredPermissions()94     public @NonNull Set<Set<Integer>> getRequiredPermissions() {
95         if (mRequiredPermissionsCached == null) {
96             mRequiredPermissionsCached = new ArraySet<>(mRequiredPermissions.size());
97             for (int i = 0; i < mRequiredPermissions.size(); i++) {
98                 VisibilityPermissionConfig permissionConfig = mRequiredPermissions.get(i);
99                 Set<Integer> requiredPermissions = permissionConfig.getAllRequiredPermissions();
100                 if (mRequiredPermissionsCached != null && requiredPermissions != null) {
101                     mRequiredPermissionsCached.add(requiredPermissions);
102                 }
103             }
104         }
105         // Added for nullness checker as it is @Nullable, we initialize it above if it is null.
106         return Objects.requireNonNull(mRequiredPermissionsCached);
107     }
108 
109     /**
110      * Returns the {@link PackageIdentifier} of the package that will be used as the target package
111      * in a call to {@link android.content.pm.PackageManager#canPackageQuery} to determine which
112      * packages can access this publicly visible schema. Returns null if the schema is not publicly
113      * visible.
114      */
getPubliclyVisibleTargetPackage()115     public @Nullable PackageIdentifier getPubliclyVisibleTargetPackage() {
116         if (mPubliclyVisibleTargetPackage == null) {
117             return null;
118         }
119         return new PackageIdentifier(mPubliclyVisibleTargetPackage);
120     }
121 
122     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
123     @Override
writeToParcel(@onNull Parcel dest, int flags)124     public void writeToParcel(@NonNull Parcel dest, int flags) {
125         VisibilityConfigCreator.writeToParcel(this, dest, flags);
126     }
127 
128     @Override
equals(@ullable Object o)129     public boolean equals(@Nullable Object o) {
130         if (this == o) {
131             return true;
132         }
133         if (o == null) {
134             return false;
135         }
136         if (!(o instanceof SchemaVisibilityConfig)) {
137             return false;
138         }
139         SchemaVisibilityConfig that = (SchemaVisibilityConfig) o;
140         return Objects.equals(mAllowedPackages, that.mAllowedPackages)
141                 && Objects.equals(mRequiredPermissions, that.mRequiredPermissions)
142                 && Objects.equals(
143                         mPubliclyVisibleTargetPackage, that.mPubliclyVisibleTargetPackage);
144     }
145 
146     @Override
hashCode()147     public int hashCode() {
148         if (mHashCode == null) {
149             mHashCode = Objects.hash(
150                     mAllowedPackages,
151                     mRequiredPermissions,
152                     mPubliclyVisibleTargetPackage);
153         }
154         return mHashCode;
155     }
156 
157     /** The builder class of {@link SchemaVisibilityConfig}. */
158     @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
159     public static final class Builder {
160         private List<PackageIdentifierParcel> mAllowedPackages = new ArrayList<>();
161         private List<VisibilityPermissionConfig> mRequiredPermissions = new ArrayList<>();
162         private @Nullable PackageIdentifierParcel mPubliclyVisibleTargetPackage;
163         private boolean mBuilt;
164 
165         /** Creates a {@link Builder} for a {@link SchemaVisibilityConfig}. */
Builder()166         public Builder() {}
167 
168         /**
169          * Creates a {@link Builder} copying the values from an existing
170          * {@link SchemaVisibilityConfig}.
171          *
172          * @exportToFramework:hide
173          */
174         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Builder(@onNull SchemaVisibilityConfig schemaVisibilityConfig)175         public Builder(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) {
176             Objects.requireNonNull(schemaVisibilityConfig);
177             mAllowedPackages = new ArrayList<>(schemaVisibilityConfig.mAllowedPackages);
178             mRequiredPermissions = new ArrayList<>(schemaVisibilityConfig.mRequiredPermissions);
179             mPubliclyVisibleTargetPackage = schemaVisibilityConfig.mPubliclyVisibleTargetPackage;
180         }
181 
182         /** Add {@link PackageIdentifier} of packages which has access to this schema. */
183         @CanIgnoreReturnValue
addAllowedPackage(@onNull PackageIdentifier packageIdentifier)184         public @NonNull Builder addAllowedPackage(@NonNull PackageIdentifier packageIdentifier) {
185             Objects.requireNonNull(packageIdentifier);
186             resetIfBuilt();
187             mAllowedPackages.add(packageIdentifier.getPackageIdentifierParcel());
188             return this;
189         }
190 
191         /** Clears the list of packages which have access to this schema. */
192         @CanIgnoreReturnValue
clearAllowedPackages()193         public @NonNull Builder clearAllowedPackages() {
194             resetIfBuilt();
195             mAllowedPackages.clear();
196             return this;
197         }
198 
199         /**
200          * Adds a set of required Android {@link android.Manifest.permission} combination a
201          * package needs to hold to access the schema this {@link SchemaVisibilityConfig}
202          * represents.
203          *
204          * <p> If the querier holds ALL of the required permissions in this combination, they will
205          * have access to read {@link GenericDocument} objects of the given schema type.
206          *
207          * <p> You can call this method repeatedly to add multiple permission combinations, and the
208          * querier will have access if they holds ANY of the combinations.
209          *
210          * <p>Merged Set available from {@link #getRequiredPermissions()}.
211          *
212          * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility for
213          * supported Permissions.
214          */
215         @SuppressWarnings("RequiresPermission")  // No permission required to call this method
216         @CanIgnoreReturnValue
addRequiredPermissions(@onNull Set<Integer> visibleToPermissions)217         public @NonNull Builder addRequiredPermissions(@NonNull Set<Integer> visibleToPermissions) {
218             Objects.requireNonNull(visibleToPermissions);
219             resetIfBuilt();
220             mRequiredPermissions.add(new VisibilityPermissionConfig(visibleToPermissions));
221             return this;
222         }
223 
224         /**
225          * Clears all required permissions combinations set to this {@link SchemaVisibilityConfig}.
226          */
227         @CanIgnoreReturnValue
clearRequiredPermissions()228         public @NonNull Builder clearRequiredPermissions() {
229             resetIfBuilt();
230             mRequiredPermissions.clear();
231             return this;
232         }
233 
234         /**
235          * Specify that this schema should be publicly available, to the same packages that have
236          * visibility to the package passed as a parameter. This visibility is determined by the
237          * result of {@link android.content.pm.PackageManager#canPackageQuery}.
238          *
239          * <p> It is possible for the packageIdentifier parameter to be different from the
240          * package performing the indexing. This might happen in the case of an on-device indexer
241          * processing information about various packages. The visibility will be the same
242          * regardless of which package indexes the document, as the visibility is based on the
243          * packageIdentifier parameter.
244          *
245          * <p> Calling this with packageIdentifier set to null is valid, and will remove public
246          * visibility for the schema.
247          *
248          * @param packageIdentifier the {@link PackageIdentifier} of the package that will be used
249          *                          as the target package in a call to {@link
250          *                          android.content.pm.PackageManager#canPackageQuery} to determine
251          *                          which packages can access this publicly visible schema.
252          */
253         @CanIgnoreReturnValue
setPubliclyVisibleTargetPackage( @ullable PackageIdentifier packageIdentifier)254         public @NonNull Builder setPubliclyVisibleTargetPackage(
255                 @Nullable PackageIdentifier packageIdentifier) {
256             resetIfBuilt();
257             if (packageIdentifier == null) {
258                 mPubliclyVisibleTargetPackage = null;
259             } else {
260                 mPubliclyVisibleTargetPackage = packageIdentifier.getPackageIdentifierParcel();
261             }
262             return this;
263         }
264 
resetIfBuilt()265         private void resetIfBuilt() {
266             if (mBuilt) {
267                 mAllowedPackages = new ArrayList<>(mAllowedPackages);
268                 mRequiredPermissions = new ArrayList<>(mRequiredPermissions);
269                 mBuilt = false;
270             }
271         }
272 
273         /** Build a {@link SchemaVisibilityConfig} */
build()274         public @NonNull SchemaVisibilityConfig build() {
275             mBuilt = true;
276             return new SchemaVisibilityConfig(
277                     mAllowedPackages,
278                     mRequiredPermissions,
279                     mPubliclyVisibleTargetPackage);
280         }
281     }
282 }
283