1 /*
2  * Copyright 2022 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.builtintypes;
18 
19 import android.content.Intent;
20 import android.net.Uri;
21 
22 import androidx.annotation.OptIn;
23 import androidx.appsearch.annotation.Document;
24 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
25 import androidx.appsearch.app.ExperimentalAppSearchApi;
26 import androidx.core.util.Preconditions;
27 
28 import org.jspecify.annotations.NonNull;
29 import org.jspecify.annotations.Nullable;
30 
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.List;
34 
35 /**
36  * AppSearch document representing a <a href="http://schema.org/Thing">Thing</a>, the most generic
37  * type of an item.
38  */
39 @Document(name = "builtin:Thing")
40 public class Thing {
41     @Document.Namespace
42     private final String mNamespace;
43 
44     @Document.Id
45     private final String mId;
46 
47     @Document.Score
48     private final int mDocumentScore;
49 
50     @Document.CreationTimestampMillis
51     private final long mCreationTimestampMillis;
52 
53     @Document.TtlMillis
54     private final long mDocumentTtlMillis;
55 
56     @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
57     private final String mName;
58 
59     @Document.StringProperty
60     private final List<String> mAlternateNames;
61 
62     @Document.StringProperty
63     private final String mDescription;
64 
65     @Document.StringProperty
66     private final String mImage;
67 
68     @Document.StringProperty
69     private final String mUrl;
70 
71     @Document.DocumentProperty
72     @OptIn(markerClass = ExperimentalAppSearchApi.class)
73     private final List<PotentialAction> mPotentialActions;
74 
75     @OptIn(markerClass = ExperimentalAppSearchApi.class)
Thing(@onNull String namespace, @NonNull String id, int documentScore, long creationTimestampMillis, long documentTtlMillis, @Nullable String name, @Nullable List<String> alternateNames, @Nullable String description, @Nullable String image, @Nullable String url, @Nullable List<PotentialAction> potentialActions)76     Thing(@NonNull String namespace, @NonNull String id, int documentScore,
77             long creationTimestampMillis, long documentTtlMillis, @Nullable String name,
78             @Nullable List<String> alternateNames, @Nullable String description,
79             @Nullable String image, @Nullable String url,
80             @Nullable List<PotentialAction> potentialActions) {
81         mNamespace = Preconditions.checkNotNull(namespace);
82         mId = Preconditions.checkNotNull(id);
83         mDocumentScore = documentScore;
84         mCreationTimestampMillis = creationTimestampMillis;
85         mDocumentTtlMillis = documentTtlMillis;
86         mName = name;
87         // If an old schema does not define the alternateNames field, AppSearch may attempt to
88         // pass in null when converting its GenericDocument to the java class.
89         if (alternateNames == null) {
90             mAlternateNames = Collections.emptyList();
91         } else {
92             mAlternateNames = Collections.unmodifiableList(alternateNames);
93         }
94         mDescription = description;
95         mImage = image;
96         mUrl = url;
97         // AppSearch may pass null if old schema lacks the potentialActions field during
98         // GenericDocument to Java class conversion.
99         if (potentialActions == null) {
100             mPotentialActions = Collections.emptyList();
101         } else {
102             mPotentialActions = Collections.unmodifiableList(potentialActions);
103         }
104     }
105 
106     /** Returns the namespace (or logical grouping) for this item. */
getNamespace()107     public @NonNull String getNamespace() {
108         return mNamespace;
109     }
110 
111     /** Returns the unique identifier for this item. */
getId()112     public @NonNull String getId() {
113         return mId;
114     }
115 
116     /** Returns the intrinsic score (or importance) of this item. */
getDocumentScore()117     public int getDocumentScore() {
118         return mDocumentScore;
119     }
120 
121     /** Returns the creation timestamp, in milliseconds since Unix epoch, of this item. */
getCreationTimestampMillis()122     public long getCreationTimestampMillis() {
123         return mCreationTimestampMillis;
124     }
125 
126     /**
127      * Returns the time-to-live timestamp, in milliseconds since
128      * {@link #getCreationTimestampMillis()}, for this item.
129      */
getDocumentTtlMillis()130     public long getDocumentTtlMillis() {
131         return mDocumentTtlMillis;
132     }
133 
134     /** Returns the name of this item. */
getName()135     public @Nullable String getName() {
136         return mName;
137     }
138 
139     /** Returns an unmodifiable list of aliases, if any, for this item. */
getAlternateNames()140     public @NonNull List<String> getAlternateNames() {
141         return mAlternateNames;
142     }
143 
144     /** Returns a description of this item. */
getDescription()145     public @Nullable String getDescription() {
146         return mDescription;
147     }
148 
149     /** Returns the URL for an image of this item. */
150     // TODO(b/328672505): Discuss with other API reviewers or council whether this API should be
151     //  changed to return ImageObject instead for improved flexibility.
152     @ExperimentalAppSearchApi
getImage()153     public @Nullable String getImage() {
154         return mImage;
155     }
156 
157     /**
158      * Returns the deeplink URL of this item.
159      *
160      * <p>This item can be opened (or viewed) by creating an {@link Intent#ACTION_VIEW} intent
161      * with this URL as the {@link Intent#setData(Uri)} uri.
162      *
163      * @see <a href="//reference/android/content/Intent#intent-structure">Intent Structure</a>
164      */
getUrl()165     public @Nullable String getUrl() {
166         return mUrl;
167     }
168 
169     /** Returns the actions that can be taken on this object. */
170     @ExperimentalAppSearchApi
getPotentialActions()171     public @NonNull List<PotentialAction> getPotentialActions() {
172         return mPotentialActions;
173     }
174 
175     /** Builder for {@link Thing}. */
176     @Document.BuilderProducer
177     public static final class Builder extends BuilderImpl<Builder> {
178         /** Constructs {@link Thing.Builder} with given {@code namespace} and {@code id} */
Builder(@onNull String namespace, @NonNull String id)179         public Builder(@NonNull String namespace, @NonNull String id) {
180             super(namespace, id);
181         }
182 
183         /** Constructs {@link Thing.Builder} from existing values in given {@link Thing}. */
Builder(@onNull Thing thing)184         public Builder(@NonNull Thing thing) {
185             super(thing);
186         }
187     }
188 
189     @SuppressWarnings("unchecked")
190     // TODO: currently this can only be extends by classes in this package. Make this publicly
191     //  extensible.
192     static class BuilderImpl<T extends BuilderImpl<T>> {
193         protected final String mNamespace;
194         protected final String mId;
195         protected int mDocumentScore;
196         protected long mCreationTimestampMillis;
197         protected long mDocumentTtlMillis;
198         protected String mName;
199         protected List<String> mAlternateNames = new ArrayList<>();
200         protected String mDescription;
201         protected String mImage;
202         protected String mUrl;
203         @OptIn(markerClass = ExperimentalAppSearchApi.class)
204         protected List<PotentialAction> mPotentialActions = new ArrayList<>();
205         private boolean mBuilt = false;
206 
BuilderImpl(@onNull String namespace, @NonNull String id)207         BuilderImpl(@NonNull String namespace, @NonNull String id) {
208             mNamespace = Preconditions.checkNotNull(namespace);
209             mId = Preconditions.checkNotNull(id);
210 
211             // Default for unset creationTimestampMillis. AppSearch will internally convert this
212             // to current time when creating the GenericDocument.
213             mCreationTimestampMillis = -1;
214         }
215 
216         @OptIn(markerClass = ExperimentalAppSearchApi.class)
BuilderImpl(@onNull Thing thing)217         BuilderImpl(@NonNull Thing thing) {
218             this(thing.getNamespace(), thing.getId());
219             mDocumentScore = thing.getDocumentScore();
220             mCreationTimestampMillis = thing.getCreationTimestampMillis();
221             mDocumentTtlMillis = thing.getDocumentTtlMillis();
222             mName = thing.getName();
223             mAlternateNames = new ArrayList<>(thing.getAlternateNames());
224             mDescription = thing.getDescription();
225             mImage = thing.getImage();
226             mUrl = thing.getUrl();
227             mPotentialActions = new ArrayList<>(thing.getPotentialActions());
228         }
229 
230         /**
231          * Sets the user-provided opaque document score of the current AppSearch document, which can
232          * be used for ranking using
233          * {@link androidx.appsearch.app.SearchSpec.RankingStrategy#RANKING_STRATEGY_DOCUMENT_SCORE}.
234          *
235          * <p>See {@link androidx.appsearch.annotation.Document.Score} for more information on
236          * score.
237          */
238         @SuppressWarnings("unchecked")
setDocumentScore(int documentScore)239         public @NonNull T setDocumentScore(int documentScore) {
240             resetIfBuilt();
241             mDocumentScore = documentScore;
242             return (T) this;
243         }
244 
245         /**
246          * Sets the creation timestamp for the current AppSearch entity, in milliseconds using the
247          * {@link System#currentTimeMillis()} time base.
248          *
249          * <p>This timestamp refers to the creation time of the AppSearch entity, not when the
250          * document is written into AppSearch.
251          *
252          * <p>If not set, then the current timestamp will be used.
253          *
254          * <p>See {@link androidx.appsearch.annotation.Document.CreationTimestampMillis} for more
255          * information on creation timestamp.
256          */
257         @SuppressWarnings("unchecked")
setCreationTimestampMillis(long creationTimestampMillis)258         public @NonNull T setCreationTimestampMillis(long creationTimestampMillis) {
259             resetIfBuilt();
260             mCreationTimestampMillis = creationTimestampMillis;
261             return (T) this;
262         }
263 
264         /**
265          * Sets the time-to-live (TTL) for the current AppSearch document as a duration in
266          * milliseconds.
267          *
268          * <p>The document will be automatically deleted when the TTL expires.
269          *
270          * <p>If not set, then the document will never expire.
271          *
272          * <p>See {@link androidx.appsearch.annotation.Document.TtlMillis} for more information on
273          * TTL.
274          */
275         @SuppressWarnings("unchecked")
setDocumentTtlMillis(long documentTtlMillis)276         public @NonNull T setDocumentTtlMillis(long documentTtlMillis) {
277             resetIfBuilt();
278             mDocumentTtlMillis = documentTtlMillis;
279             return (T) this;
280         }
281 
282         /** Sets the name of the item. */
setName(@ullable String name)283         public @NonNull T setName(@Nullable String name) {
284             resetIfBuilt();
285             mName = name;
286             return (T) this;
287         }
288 
289         /** Adds an alias for the item. */
addAlternateName(@onNull String alternateName)290         public @NonNull T addAlternateName(@NonNull String alternateName) {
291             resetIfBuilt();
292             Preconditions.checkNotNull(alternateName);
293             mAlternateNames.add(alternateName);
294             return (T) this;
295         }
296 
297         /** Sets a list of aliases for the item. */
setAlternateNames(@ullable List<String> alternateNames)298         public @NonNull T setAlternateNames(@Nullable List<String> alternateNames) {
299             resetIfBuilt();
300             clearAlternateNames();
301             if (alternateNames != null) {
302                 mAlternateNames.addAll(alternateNames);
303             }
304             return (T) this;
305         }
306 
307         /** Clears the aliases, if any, for the item. */
clearAlternateNames()308         public @NonNull T clearAlternateNames() {
309             resetIfBuilt();
310             mAlternateNames.clear();
311             return (T) this;
312         }
313 
314         /** Sets the description for the item. */
setDescription(@ullable String description)315         public @NonNull T setDescription(@Nullable String description) {
316             resetIfBuilt();
317             mDescription = description;
318             return (T) this;
319         }
320 
321         /** Sets the URL for an image of the item. */
322         @ExperimentalAppSearchApi
setImage(@ullable String image)323         public @NonNull T setImage(@Nullable String image) {
324             resetIfBuilt();
325             mImage = image;
326             return (T) this;
327         }
328 
329         /**
330          * Sets the deeplink URL of the item.
331          *
332          * <p>If this item can be displayed by any system UI surface, or can be read by another
333          * Android package, through one of the
334          * {@link androidx.appsearch.app.SetSchemaRequest.Builder} methods, this {@code url}
335          * should act as a deeplink into the activity that can open it. Callers should be able to
336          * construct an {@link Intent#ACTION_VIEW} intent with the {@code url} as the
337          * {@link Intent#setData(Uri)} to view the item inside your application.
338          *
339          * <p>See <a href="//training/basics/intents/filters">Allowing Other Apps to Start Your
340          * Activity</a> for more details on how to make activities in your app open for use by other
341          * apps by defining intent filters.
342          */
setUrl(@ullable String url)343         public @NonNull T setUrl(@Nullable String url) {
344             resetIfBuilt();
345             mUrl = url;
346             return (T) this;
347         }
348         /**
349          * Add a new action to the list of potential actions for this document.
350          */
351         @ExperimentalAppSearchApi
addPotentialAction(@onNull PotentialAction newPotentialAction)352         public @NonNull T addPotentialAction(@NonNull PotentialAction newPotentialAction) {
353             resetIfBuilt();
354             Preconditions.checkNotNull(newPotentialAction);
355             mPotentialActions.add(newPotentialAction);
356             return (T) this;
357         }
358 
359         /** Sets a list of potential actions for this document. */
360         @ExperimentalAppSearchApi
setPotentialActions(@ullable List<PotentialAction> newPotentialActions)361         public @NonNull T setPotentialActions(@Nullable List<PotentialAction> newPotentialActions) {
362             resetIfBuilt();
363             clearPotentialActions();
364             if (newPotentialActions != null) {
365                 mPotentialActions.addAll(newPotentialActions);
366             }
367             return (T) this;
368         }
369 
370         /**
371          * Clear all the potential actions for this document.
372          */
373         @ExperimentalAppSearchApi
clearPotentialActions()374         public @NonNull T clearPotentialActions() {
375             resetIfBuilt();
376             mPotentialActions.clear();
377             return (T) this;
378         }
379 
380         /**
381          * If built, make a copy of previous data for every field so that the builder can be reused.
382          */
resetIfBuilt()383         private void resetIfBuilt() {
384             if (mBuilt) {
385                 mAlternateNames = new ArrayList<>(mAlternateNames);
386                 mPotentialActions = new ArrayList<>(mPotentialActions);
387                 mBuilt = false;
388             }
389         }
390 
391         /** Builds a {@link Thing} object. */
build()392         public @NonNull Thing build() {
393             mBuilt = true;
394             return new Thing(mNamespace, mId, mDocumentScore, mCreationTimestampMillis,
395                     mDocumentTtlMillis, mName, mAlternateNames, mDescription, mImage, mUrl,
396                     mPotentialActions);
397         }
398     }
399 }
400