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