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