1 /*
2  * Copyright 2020 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.core.content;
18 
19 import android.content.Intent;
20 import android.content.LocusId;
21 import android.os.Build;
22 
23 import androidx.annotation.RequiresApi;
24 import androidx.core.util.Preconditions;
25 
26 import org.jspecify.annotations.NonNull;
27 import org.jspecify.annotations.Nullable;
28 
29 /**
30  * An identifier for an unique state (locus) in the application. Should be stable across reboots and
31  * backup / restore.
32  *
33  * <p>Locus is a new concept introduced on
34  * {@link android.os.Build.VERSION_CODES#Q Android Q} and it lets the intelligence service provided
35  * by the Android system correlate state between different subsystems such as content capture,
36  * shortcuts, and notifications.
37  *
38  * <p>For example, if your app provides an activity representing a chat between 2 users
39  * (say {@code A} and {@code B}, this chat state could be represented by:
40  *
41  * <pre><code>
42  * LocusIdCompat chatId = new LocusIdCompat("Chat_A_B");
43  * </code></pre>
44  *
45  * <p>And then you should use that {@code chatId} by:
46  *
47  * <ul>
48  *   <li>Setting it in the chat notification (through
49  *   {@link androidx.core.app.NotificationCompat.Builder#setLocusId(LocusIdCompat)
50  *   NotificationCompat.Builder.setLocusId(chatId)}).
51  *   <li>Setting it into the {@link androidx.core.content.pm.ShortcutInfoCompat} (through
52  *   {@link androidx.core.content.pm.ShortcutInfoCompat.Builder#setLocusId(LocusIdCompat)
53  *   ShortcutInfoCompat.Builder.setLocusId(chatId)}), if you provide a launcher shortcut for that
54  *   chat conversation.
55  *   <li>Associating it with the {@link android.view.contentcapture.ContentCaptureContext} of the
56  *   root view of the chat conversation activity (through
57  *   {@link android.view.View#getContentCaptureSession()}, then
58  *   {@link android.view.contentcapture.ContentCaptureContext.Builder
59  *   new ContentCaptureContext.Builder(chatId).build()} and
60  *   {@link android.view.contentcapture.ContentCaptureSession#setContentCaptureContext(
61  *   android.view.contentcapture.ContentCaptureContext)} - see
62  *   {@link android.view.contentcapture.ContentCaptureManager} for more info about content capture).
63  *   <li>Configuring your app to launch the chat conversation through the
64  *   {@link Intent#ACTION_VIEW_LOCUS} intent.
65  * </ul>
66  *
67  * NOTE: The LocusId is only used by a on-device intelligence service provided by the Android
68  * System, see {@link android.view.contentcapture.ContentCaptureManager} for more details.
69  */
70 public final class LocusIdCompat {
71 
72     private final String mId;
73     // Only guaranteed to be non-null on SDK_INT >= 29.
74     private final LocusId mWrapped;
75 
76     /**
77      * Construct a new LocusIdCompat with the specified id.
78      *
79      * @throws IllegalArgumentException if {@code id} is empty or {@code null}.
80      */
LocusIdCompat(@onNull String id)81     public LocusIdCompat(@NonNull String id) {
82         mId = Preconditions.checkStringNotEmpty(id, "id cannot be empty");
83         if (Build.VERSION.SDK_INT >= 29) {
84             mWrapped = Api29Impl.create(id);
85         } else {
86             mWrapped = null;
87         }
88     }
89 
90     /**
91      * Gets the canonical {@code id} associated with the locus.
92      */
getId()93     public @NonNull String getId() {
94         return mId;
95     }
96 
97     @Override
hashCode()98     public int hashCode() {
99         final int prime = 31;
100         int result = 1;
101         result = prime * result + ((mId == null) ? 0 : mId.hashCode());
102         return result;
103     }
104 
105     @Override
equals(final @Nullable Object obj)106     public boolean equals(final @Nullable Object obj) {
107         if (this == obj) return true;
108         if (obj == null) return false;
109         if (getClass() != obj.getClass()) return false;
110         final LocusIdCompat other = (LocusIdCompat) obj;
111         if (mId == null) {
112             return other.mId == null;
113         } else {
114             return mId.equals(other.mId);
115         }
116     }
117 
118     @Override
toString()119     public @NonNull String toString() {
120         return "LocusIdCompat[" + getSanitizedId() + "]";
121     }
122 
123     /**
124      * @return {@link LocusId} object from this compat object.
125      */
126     @RequiresApi(29)
toLocusId()127     public @NonNull LocusId toLocusId() {
128         return mWrapped;
129     }
130 
131     /**
132      * Returns an instance of LocusIdCompat from given {@link LocusId}.
133      */
134     @RequiresApi(29)
toLocusIdCompat(final @NonNull LocusId locusId)135     public static @NonNull LocusIdCompat toLocusIdCompat(final @NonNull LocusId locusId) {
136         Preconditions.checkNotNull(locusId, "locusId cannot be null");
137         return new LocusIdCompat(Preconditions.checkStringNotEmpty(Api29Impl.getId(locusId),
138                 "id cannot be empty"));
139     }
140 
getSanitizedId()141     private @NonNull String getSanitizedId() {
142         final int size = mId.length();
143         return size + "_chars";
144     }
145 
146     // Inner class required to avoid VFY errors during class init.
147     @RequiresApi(29)
148     private static class Api29Impl {
149 
150         /**
151          * @return {@link LocusId} object from this compat object.
152          */
create(final @NonNull String id)153         static @NonNull LocusId create(final @NonNull String id) {
154             return new LocusId(id);
155         }
156 
157         /**
158          * @return {@code id} from the LocusId object.
159          */
getId(final @NonNull LocusId obj)160         static @NonNull String getId(final @NonNull LocusId obj) {
161             return obj.getId();
162         }
163     }
164 }
165