@@ -23,11 +23,13 @@
import android.os.Bundle; import android.text.TextUtils;+import androidx.annotation.DoNotInline; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.appsearch.exceptions.AppSearchException;+import androidx.appsearch.safeparcel.GenericDocumentParcel; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.util.Preconditions;@@ -61,6 +63,9 @@
+ "Please use androidx.appsearch.app.ShortcutAdapter.DEFAULT_NAMESPACE as the " + "namespace of the document if it will be used to create a shortcut.";+ private static final String APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE =+ "appsearch_generic_doc_parcel";+ /** * Converts given document to a {@link ShortcutInfoCompat.Builder}, which can be used to * construct a shortcut for donation through@@ -117,16 +122,19 @@
throw new IllegalArgumentException(NAMESPACE_CHECK_ERROR_MESSAGE); } final String name = doc.getPropertyString(FIELD_NAME);+ final Bundle extras = new Bundle();+ extras.putParcelable(APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE, doc.getDocumentParcel()); return new ShortcutInfoCompat.Builder(context, doc.getId()) .setShortLabel(!TextUtils.isEmpty(name) ? name : doc.getId()) .setIntent(new Intent(Intent.ACTION_VIEW, getDocumentUri(doc))) .setExcludedFromSurfaces(ShortcutInfoCompat.SURFACE_LAUNCHER)- .setTransientExtras(doc.getBundle());+ .setTransientExtras(extras); } /** * Extracts {@link GenericDocument} from given {@link ShortcutInfoCompat} if applicable. * Returns null if document cannot be found in the given shortcut.+ * * @exportToFramework:hide */ @Nullable@@ -137,7 +145,21 @@
if (extras == null) { return null; }- return new GenericDocument(extras);++ GenericDocumentParcel genericDocParcel;+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {+ genericDocParcel = Api33Impl.getParcelableFromBundle(extras,+ APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE, GenericDocumentParcel.class);+ } else {+ @SuppressWarnings("deprecation")+ GenericDocumentParcel tmp = (GenericDocumentParcel) extras.getParcelable(+ APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE);+ genericDocParcel = tmp;+ }+ if (genericDocParcel == null) {+ return null;+ }+ return new GenericDocument(genericDocParcel); } /**@@ -177,4 +199,21 @@
.path(DEFAULT_NAMESPACE + "/" + id) .build(); }+ @RequiresApi(33)+ static class Api33Impl {+ private Api33Impl() {+ // This class is not instantiable.+ }++ @DoNotInline+ static T getParcelableFromBundle(+ @NonNull Bundle bundle,+ @NonNull String key,+ @NonNull Class clazz) {+ Preconditions.checkNotNull(bundle);+ Preconditions.checkNotNull(key);+ Preconditions.checkNotNull(clazz);+ return bundle.getParcelable(key, clazz);+ }+ } }
@@ -0,0 +1,43 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.localstorage.stats;++import static com.google.common.truth.Truth.assertThat;++import org.junit.Test;++public class ClickStatsTest {+ @Test+ public void testBuilder() {+ long timestampMillis = 1L;+ long timeStayOnResultMillis = 2L;+ int resultRankInBlock = 3;+ int resultRankGlobal = 4;++ final ClickStats clickStats = new ClickStats.Builder()+ .setTimestampMillis(timestampMillis)+ .setTimeStayOnResultMillis(timeStayOnResultMillis)+ .setResultRankInBlock(resultRankInBlock)+ .setResultRankGlobal(resultRankGlobal)+ .build();++ assertThat(clickStats.getTimestampMillis()).isEqualTo(timestampMillis);+ assertThat(clickStats.getTimeStayOnResultMillis()).isEqualTo(timeStayOnResultMillis);+ assertThat(clickStats.getResultRankInBlock()).isEqualTo(resultRankInBlock);+ assertThat(clickStats.getResultRankGlobal()).isEqualTo(resultRankGlobal);+ }+}
@@ -0,0 +1,224 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.localstorage.stats;++import static com.google.common.truth.Truth.assertThat;++import com.google.common.collect.ImmutableSet;++import org.junit.Test;++import java.util.Set;++public class SearchIntentStatsTest {+ static final String TEST_PACKAGE_NAME = "package.test";+ static final String TEST_DATA_BASE = "testDataBase";++ @Test+ public void testBuilder() {+ String prevQuery = "prev";+ String currQuery = "curr";+ long searchIntentTimestampMillis = 1L;+ int numResultsFetched = 2;+ int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;++ // Clicks associated with the search intent.+ final ClickStats clickStats0 = new ClickStats.Builder()+ .setTimestampMillis(10L)+ .setTimeStayOnResultMillis(20L)+ .setResultRankInBlock(30)+ .setResultRankGlobal(40)+ .build();+ final ClickStats clickStats1 = new ClickStats.Builder()+ .setTimestampMillis(11L)+ .setTimeStayOnResultMillis(21L)+ .setResultRankInBlock(31)+ .setResultRankGlobal(41)+ .build();++ final SearchIntentStats searchIntentStats = new SearchIntentStats.Builder(TEST_PACKAGE_NAME)+ .setDatabase(TEST_DATA_BASE)+ .setPrevQuery(prevQuery)+ .setCurrQuery(currQuery)+ .setTimestampMillis(searchIntentTimestampMillis)+ .setNumResultsFetched(numResultsFetched)+ .setQueryCorrectionType(queryCorrectionType)+ .addClicksStats(clickStats0, clickStats1)+ .build();++ assertThat(searchIntentStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);+ assertThat(searchIntentStats.getDatabase()).isEqualTo(TEST_DATA_BASE);+ assertThat(searchIntentStats.getPrevQuery()).isEqualTo(prevQuery);+ assertThat(searchIntentStats.getCurrQuery()).isEqualTo(currQuery);+ assertThat(searchIntentStats.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);+ assertThat(searchIntentStats.getNumResultsFetched()).isEqualTo(numResultsFetched);+ assertThat(searchIntentStats.getQueryCorrectionType()).isEqualTo(queryCorrectionType);+ assertThat(searchIntentStats.getClicksStats()).containsExactly(clickStats0, clickStats1);+ }++ @Test+ public void testBuilder_addClicksStats_byCollection() {+ final ClickStats clickStats0 = new ClickStats.Builder()+ .setTimestampMillis(10L)+ .setTimeStayOnResultMillis(20L)+ .setResultRankInBlock(30)+ .setResultRankGlobal(40)+ .build();+ final ClickStats clickStats1 = new ClickStats.Builder()+ .setTimestampMillis(11L)+ .setTimeStayOnResultMillis(21L)+ .setResultRankInBlock(31)+ .setResultRankGlobal(41)+ .build();+ Set clicksStats = ImmutableSet.of(clickStats0, clickStats1);++ final SearchIntentStats searchIntentStats = new SearchIntentStats.Builder(TEST_PACKAGE_NAME)+ .setDatabase(TEST_DATA_BASE)+ .addClicksStats(clicksStats)+ .build();++ assertThat(searchIntentStats.getClicksStats()).containsExactlyElementsIn(clicksStats);+ }++ @Test+ public void testBuilder_builderReuse() {+ String prevQuery = "prev";+ String currQuery = "curr";+ long searchIntentTimestampMillis = 1;+ int numResultsFetched = 2;+ int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;++ final ClickStats clickStats0 = new ClickStats.Builder()+ .setTimestampMillis(10L)+ .setTimeStayOnResultMillis(20L)+ .setResultRankInBlock(30)+ .setResultRankGlobal(40)+ .build();+ final ClickStats clickStats1 = new ClickStats.Builder()+ .setTimestampMillis(11L)+ .setTimeStayOnResultMillis(21L)+ .setResultRankInBlock(31)+ .setResultRankGlobal(41)+ .build();++ SearchIntentStats.Builder builder = new SearchIntentStats.Builder(TEST_PACKAGE_NAME)+ .setDatabase(TEST_DATA_BASE)+ .setPrevQuery(prevQuery)+ .setCurrQuery(currQuery)+ .setTimestampMillis(searchIntentTimestampMillis)+ .setNumResultsFetched(numResultsFetched)+ .setQueryCorrectionType(queryCorrectionType)+ .addClicksStats(clickStats0, clickStats1);++ final SearchIntentStats searchIntentStats0 = builder.build();++ final ClickStats clickStats2 = new ClickStats.Builder()+ .setTimestampMillis(12L)+ .setTimeStayOnResultMillis(22L)+ .setResultRankInBlock(32)+ .setResultRankGlobal(42)+ .build();+ builder.addClicksStats(clickStats2);++ final SearchIntentStats searchIntentStats1 = builder.build();++ // Check that searchIntentStats0 wasn't altered.+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);+ assertThat(searchIntentStats0.getPrevQuery()).isEqualTo(prevQuery);+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo(currQuery);+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(numResultsFetched);+ assertThat(searchIntentStats0.getQueryCorrectionType()).isEqualTo(queryCorrectionType);+ assertThat(searchIntentStats0.getClicksStats()).containsExactly(clickStats0, clickStats1);++ // Check that searchIntentStats1 has the new values.+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo(prevQuery);+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo(currQuery);+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(numResultsFetched);+ assertThat(searchIntentStats1.getQueryCorrectionType()).isEqualTo(queryCorrectionType);+ assertThat(searchIntentStats1.getClicksStats()).containsExactly(+ clickStats0, clickStats1, clickStats2);+ }++ @Test+ public void testBuilder_builderReuse_byCollection() {+ String prevQuery = "prev";+ String currQuery = "curr";+ long searchIntentTimestampMillis = 1;+ int numResultsFetched = 2;+ int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;++ final ClickStats clickStats0 = new ClickStats.Builder()+ .setTimestampMillis(10L)+ .setTimeStayOnResultMillis(20L)+ .setResultRankInBlock(30)+ .setResultRankGlobal(40)+ .build();+ final ClickStats clickStats1 = new ClickStats.Builder()+ .setTimestampMillis(11L)+ .setTimeStayOnResultMillis(21L)+ .setResultRankInBlock(31)+ .setResultRankGlobal(41)+ .build();++ SearchIntentStats.Builder builder = new SearchIntentStats.Builder(TEST_PACKAGE_NAME)+ .setDatabase(TEST_DATA_BASE)+ .setPrevQuery(prevQuery)+ .setCurrQuery(currQuery)+ .setTimestampMillis(searchIntentTimestampMillis)+ .setNumResultsFetched(numResultsFetched)+ .setQueryCorrectionType(queryCorrectionType)+ .addClicksStats(ImmutableSet.of(clickStats0, clickStats1));++ final SearchIntentStats searchIntentStats0 = builder.build();++ final ClickStats clickStats2 = new ClickStats.Builder()+ .setTimestampMillis(12L)+ .setTimeStayOnResultMillis(22L)+ .setResultRankInBlock(32)+ .setResultRankGlobal(42)+ .build();+ builder.addClicksStats(ImmutableSet.of(clickStats2));++ final SearchIntentStats searchIntentStats1 = builder.build();++ // Check that searchIntentStats0 wasn't altered.+ assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);+ assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);+ assertThat(searchIntentStats0.getPrevQuery()).isEqualTo(prevQuery);+ assertThat(searchIntentStats0.getCurrQuery()).isEqualTo(currQuery);+ assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);+ assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(numResultsFetched);+ assertThat(searchIntentStats0.getQueryCorrectionType()).isEqualTo(queryCorrectionType);+ assertThat(searchIntentStats0.getClicksStats()).containsExactly(clickStats0, clickStats1);++ // Check that searchIntentStats1 has the new values.+ assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);+ assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);+ assertThat(searchIntentStats1.getPrevQuery()).isEqualTo(prevQuery);+ assertThat(searchIntentStats1.getCurrQuery()).isEqualTo(currQuery);+ assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);+ assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(numResultsFetched);+ assertThat(searchIntentStats1.getQueryCorrectionType()).isEqualTo(queryCorrectionType);+ assertThat(searchIntentStats1.getClicksStats()).containsExactly(+ clickStats0, clickStats1, clickStats2);+ }+}
@@ -0,0 +1,135 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.localstorage.usagereporting;++import static com.google.common.truth.Truth.assertThat;++import static org.junit.Assert.assertThrows;++import androidx.appsearch.app.GenericDocument;+import androidx.appsearch.usagereporting.ActionConstants;+import androidx.appsearch.usagereporting.ClickAction;++import org.junit.Test;++public class ClickActionGenericDocumentTest {+ @Test+ public void testBuild() {+ ClickActionGenericDocument clickActionGenericDocument =+ new ClickActionGenericDocument.Builder("namespace", "click", "builtin:ClickAction")+ .setCreationTimestampMillis(1000)+ .setQuery("body")+ .setResultRankInBlock(12)+ .setResultRankGlobal(34)+ .setTimeStayOnResultMillis(2000)+ .build();++ assertThat(clickActionGenericDocument.getNamespace()).isEqualTo("namespace");+ assertThat(clickActionGenericDocument.getId()).isEqualTo("click");+ assertThat(clickActionGenericDocument.getSchemaType()).isEqualTo("builtin:ClickAction");+ assertThat(clickActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);+ assertThat(clickActionGenericDocument.getActionType())+ .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);+ assertThat(clickActionGenericDocument.getQuery()).isEqualTo("body");+ assertThat(clickActionGenericDocument.getResultRankInBlock()).isEqualTo(12);+ assertThat(clickActionGenericDocument.getResultRankGlobal()).isEqualTo(34);+ assertThat(clickActionGenericDocument.getTimeStayOnResultMillis()).isEqualTo(2000);+ }++ @Test+ public void testBuild_fromGenericDocument() {+ GenericDocument document =+ new GenericDocument.Builder<>("namespace", "click", "builtin:ClickAction")+ .setCreationTimestampMillis(1000)+ .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_CLICK)+ .setPropertyString("query", "body")+ .setPropertyLong("resultRankInBlock", 12)+ .setPropertyLong("resultRankGlobal", 34)+ .setPropertyLong("timeStayOnResultMillis", 2000)+ .build();+ ClickActionGenericDocument clickActionGenericDocument =+ new ClickActionGenericDocument.Builder(document).build();++ assertThat(clickActionGenericDocument.getNamespace()).isEqualTo("namespace");+ assertThat(clickActionGenericDocument.getId()).isEqualTo("click");+ assertThat(clickActionGenericDocument.getSchemaType()).isEqualTo("builtin:ClickAction");+ assertThat(clickActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);+ assertThat(clickActionGenericDocument.getActionType())+ .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);+ assertThat(clickActionGenericDocument.getQuery()).isEqualTo("body");+ assertThat(clickActionGenericDocument.getResultRankInBlock()).isEqualTo(12);+ assertThat(clickActionGenericDocument.getResultRankGlobal()).isEqualTo(34);+ assertThat(clickActionGenericDocument.getTimeStayOnResultMillis()).isEqualTo(2000);+ }++// @exportToFramework:startStrip()+ @Test+ public void testBuild_fromDocumentClass() throws Exception {+ ClickAction clickAction =+ new ClickAction.Builder("namespace", "click", /* actionTimestampMillis= */1000)+ .setQuery("body")+ .setReferencedQualifiedId("pkg$db/ns#doc")+ .setResultRankInBlock(12)+ .setResultRankGlobal(34)+ .setTimeStayOnResultMillis(2000)+ .build();+ ClickActionGenericDocument clickActionGenericDocument =+ new ClickActionGenericDocument.Builder(+ GenericDocument.fromDocumentClass(clickAction)).build();++ assertThat(clickActionGenericDocument.getNamespace()).isEqualTo("namespace");+ assertThat(clickActionGenericDocument.getId()).isEqualTo("click");+ assertThat(clickActionGenericDocument.getSchemaType()).isEqualTo("builtin:ClickAction");+ assertThat(clickActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);+ assertThat(clickActionGenericDocument.getActionType())+ .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);+ assertThat(clickActionGenericDocument.getQuery()).isEqualTo("body");+ assertThat(clickActionGenericDocument.getResultRankInBlock()).isEqualTo(12);+ assertThat(clickActionGenericDocument.getResultRankGlobal()).isEqualTo(34);+ assertThat(clickActionGenericDocument.getTimeStayOnResultMillis()).isEqualTo(2000);+ }+// @exportToFramework:endStrip()++ @Test+ public void testBuild_invalidActionTypeThrowsException() {+ GenericDocument documentWithoutActionType =+ new GenericDocument.Builder<>("namespace", "search", "builtin:ClickAction")+ .build();+ IllegalArgumentException e1 = assertThrows(IllegalArgumentException.class,+ () -> new ClickActionGenericDocument.Builder(documentWithoutActionType));+ assertThat(e1.getMessage())+ .isEqualTo("Invalid action type for ClickActionGenericDocument");++ GenericDocument documentWithUnknownActionType =+ new GenericDocument.Builder<>("namespace", "search", "builtin:ClickAction")+ .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_UNKNOWN)+ .build();+ IllegalArgumentException e2 = assertThrows(IllegalArgumentException.class,+ () -> new ClickActionGenericDocument.Builder(documentWithUnknownActionType));+ assertThat(e2.getMessage())+ .isEqualTo("Invalid action type for ClickActionGenericDocument");++ GenericDocument documentWithIncorrectActionType =+ new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")+ .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_SEARCH)+ .build();+ IllegalArgumentException e3 = assertThrows(IllegalArgumentException.class,+ () -> new ClickActionGenericDocument.Builder(documentWithIncorrectActionType));+ assertThat(e3.getMessage())+ .isEqualTo("Invalid action type for ClickActionGenericDocument");+ }+}
@@ -0,0 +1,122 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.localstorage.usagereporting;++import static com.google.common.truth.Truth.assertThat;++import static org.junit.Assert.assertThrows;++import androidx.appsearch.app.GenericDocument;+import androidx.appsearch.usagereporting.ActionConstants;+import androidx.appsearch.usagereporting.SearchAction;++import org.junit.Test;++public class SearchActionGenericDocumentTest {+ @Test+ public void testBuild() {+ SearchActionGenericDocument searchActionGenericDocument =+ new SearchActionGenericDocument.Builder(+ "namespace", "search", "builtin:SearchAction")+ .setCreationTimestampMillis(1000)+ .setQuery("body")+ .setFetchedResultCount(123)+ .build();++ assertThat(searchActionGenericDocument.getNamespace()).isEqualTo("namespace");+ assertThat(searchActionGenericDocument.getId()).isEqualTo("search");+ assertThat(searchActionGenericDocument.getSchemaType()).isEqualTo("builtin:SearchAction");+ assertThat(searchActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);+ assertThat(searchActionGenericDocument.getActionType())+ .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);+ assertThat(searchActionGenericDocument.getQuery()).isEqualTo("body");+ assertThat(searchActionGenericDocument.getFetchedResultCount()).isEqualTo(123);+ }++ @Test+ public void testBuild_fromGenericDocument() {+ GenericDocument document =+ new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")+ .setCreationTimestampMillis(1000)+ .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_SEARCH)+ .setPropertyString("query", "body")+ .setPropertyLong("fetchedResultCount", 123)+ .build();+ SearchActionGenericDocument searchActionGenericDocument =+ new SearchActionGenericDocument(document);++ assertThat(searchActionGenericDocument.getNamespace()).isEqualTo("namespace");+ assertThat(searchActionGenericDocument.getId()).isEqualTo("search");+ assertThat(searchActionGenericDocument.getSchemaType()).isEqualTo("builtin:SearchAction");+ assertThat(searchActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);+ assertThat(searchActionGenericDocument.getActionType())+ .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);+ assertThat(searchActionGenericDocument.getQuery()).isEqualTo("body");+ assertThat(searchActionGenericDocument.getFetchedResultCount()).isEqualTo(123);+ }++// @exportToFramework:startStrip()+ @Test+ public void testBuild_fromDocumentClass() throws Exception {+ SearchAction searchAction =+ new SearchAction.Builder("namespace", "search", /* actionTimestampMillis= */1000)+ .setQuery("body")+ .setFetchedResultCount(123)+ .build();+ SearchActionGenericDocument searchActionGenericDocument =+ new SearchActionGenericDocument(GenericDocument.fromDocumentClass(searchAction));++ assertThat(searchActionGenericDocument.getNamespace()).isEqualTo("namespace");+ assertThat(searchActionGenericDocument.getId()).isEqualTo("search");+ assertThat(searchActionGenericDocument.getSchemaType()).isEqualTo("builtin:SearchAction");+ assertThat(searchActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);+ assertThat(searchActionGenericDocument.getActionType())+ .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);+ assertThat(searchActionGenericDocument.getQuery()).isEqualTo("body");+ assertThat(searchActionGenericDocument.getFetchedResultCount()).isEqualTo(123);+ }+// @exportToFramework:endStrip()++ @Test+ public void testBuild_invalidActionTypeThrowsException() {+ GenericDocument documentWithoutActionType =+ new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")+ .build();+ IllegalArgumentException e1 = assertThrows(IllegalArgumentException.class,+ () -> new SearchActionGenericDocument.Builder(documentWithoutActionType));+ assertThat(e1.getMessage())+ .isEqualTo("Invalid action type for SearchActionGenericDocument");++ GenericDocument documentWithUnknownActionType =+ new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")+ .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_UNKNOWN)+ .build();+ IllegalArgumentException e2 = assertThrows(IllegalArgumentException.class,+ () -> new SearchActionGenericDocument.Builder(documentWithUnknownActionType));+ assertThat(e2.getMessage())+ .isEqualTo("Invalid action type for SearchActionGenericDocument");++ GenericDocument documentWithIncorrectActionType =+ new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")+ .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_CLICK)+ .build();+ IllegalArgumentException e3 = assertThrows(IllegalArgumentException.class,+ () -> new SearchActionGenericDocument.Builder(documentWithIncorrectActionType));+ assertThat(e3.getMessage())+ .isEqualTo("Invalid action type for SearchActionGenericDocument");+ }+}
@@ -0,0 +1,215 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.localstorage.visibilitystore;+++import static com.google.common.truth.Truth.assertThat;++import static org.junit.Assert.assertThrows;++import androidx.appsearch.app.AppSearchSchema;+import androidx.appsearch.app.GenericDocument;+import androidx.appsearch.app.GetSchemaResponse;+import androidx.appsearch.app.InternalSetSchemaResponse;+import androidx.appsearch.app.InternalVisibilityConfig;+import androidx.appsearch.app.PackageIdentifier;+import androidx.appsearch.app.SetSchemaRequest;+import androidx.appsearch.app.VisibilityPermissionConfig;+import androidx.appsearch.exceptions.AppSearchException;+import androidx.appsearch.localstorage.AppSearchConfigImpl;+import androidx.appsearch.localstorage.AppSearchImpl;+import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;+import androidx.appsearch.localstorage.OptimizeStrategy;+import androidx.appsearch.localstorage.UnlimitedLimitConfig;+import androidx.appsearch.localstorage.util.PrefixUtil;++import com.google.common.collect.ImmutableList;+import com.google.common.collect.ImmutableSet;++import org.junit.Before;+import org.junit.Rule;+import org.junit.Test;+import org.junit.rules.TemporaryFolder;++import java.io.File;+import java.util.Collections;++// "V2 schema" refers to V2 of the VisibilityDocument schema, but no Visibility overlay schema+// present. Simulates backwards compatibility situations.+public class VisibilityStoreMigrationFromV2Test {++ /**+ * Always trigger optimize in this class. OptimizeStrategy will be tested in its own test class.+ */+ private static final OptimizeStrategy ALWAYS_OPTIMIZE = optimizeInfo -> true;++ @Rule+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();+ private File mFile;++ @Before+ public void setUp() throws Exception {+ // Give ourselves global query permissions+ mFile = mTemporaryFolder.newFolder();+ }++ @Test+ public void testVisibilityMigration_from2() throws Exception {+ // As such, we can treat V2 documents as V3 documents when upgrading, but we need to test+ // this.++ // Values for a "foo" client+ String packageNameFoo = "packageFoo";+ byte[] sha256CertFoo = new byte[32];+ PackageIdentifier packageIdentifierFoo =+ new PackageIdentifier(packageNameFoo, sha256CertFoo);++ // Values for a "bar" client+ String packageNameBar = "packageBar";+ byte[] sha256CertBar = new byte[32];+ PackageIdentifier packageIdentifierBar =+ new PackageIdentifier(packageNameBar, sha256CertBar);++ // Create AppSearchImpl with visibility document version 2;+ AppSearchImpl appSearchImplInV2 = AppSearchImpl.create(mFile,+ new AppSearchConfigImpl(new UnlimitedLimitConfig(),+ new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,+ /*visibilityChecker=*/ null,+ ALWAYS_OPTIMIZE);++ // Erase overlay schemas since it doesn't exist in released V2 schema.+ InternalSetSchemaResponse internalSetAndroidVSchemaResponse = appSearchImplInV2.setSchema(+ VisibilityStore.VISIBILITY_PACKAGE_NAME,+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,+ // no overlay schema+ ImmutableList.of(),+ /*prefixedVisibilityBundles=*/ Collections.emptyList(),+ /*forceOverride=*/ true, // force push the old version into disk+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,+ /*setSchemaStatsBuilder=*/ null);+ assertThat(internalSetAndroidVSchemaResponse.isSuccess()).isTrue();++ GetSchemaResponse getSchemaResponse = appSearchImplInV2.getSchema(+ VisibilityStore.VISIBILITY_PACKAGE_NAME,+ VisibilityStore.VISIBILITY_DATABASE_NAME,+ new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));+ assertThat(getSchemaResponse.getSchemas()).containsExactly(+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,+ VisibilityPermissionConfig.SCHEMA);+ GetSchemaResponse getAndroidVOverlaySchemaResponse = appSearchImplInV2.getSchema(+ VisibilityStore.VISIBILITY_PACKAGE_NAME,+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,+ new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));+ assertThat(getAndroidVOverlaySchemaResponse.getSchemas()).isEmpty();++ // Build deprecated visibility documents in version 2+ String prefix = PrefixUtil.createPrefix("package", "database");+ InternalVisibilityConfig visibilityConfigV2 = new InternalVisibilityConfig.Builder(+ prefix + "Schema")+ .setNotDisplayedBySystem(true)+ .addVisibleToPackage(+ new PackageIdentifier(packageNameFoo, sha256CertFoo))+ .addVisibleToPackage(+ new PackageIdentifier(packageNameBar, sha256CertBar))+ .addVisibleToPermissions(+ ImmutableSet.of(SetSchemaRequest.READ_SMS,+ SetSchemaRequest.READ_CALENDAR))+ .addVisibleToPermissions(+ ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA))+ .addVisibleToPermissions(+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))+ .build();+ GenericDocument visibilityDocumentV2 =+ VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfigV2);++ // Set client schema into AppSearchImpl with empty VisibilityDocument since we need to+ // directly put old version of VisibilityDocument.+ InternalSetSchemaResponse internalSetSchemaResponse = appSearchImplInV2.setSchema(+ "package",+ "database",+ ImmutableList.of(+ new AppSearchSchema.Builder("Schema").build()),+ /*visibilityDocuments=*/ Collections.emptyList(),+ /*forceOverride=*/ false,+ /*schemaVersion=*/ 0,+ /*setSchemaStatsBuilder=*/ null);+ assertThat(internalSetSchemaResponse.isSuccess()).isTrue();++ // Put deprecated visibility documents in version 2 to AppSearchImpl+ appSearchImplInV2.putDocument(+ VisibilityStore.VISIBILITY_PACKAGE_NAME,+ VisibilityStore.VISIBILITY_DATABASE_NAME,+ visibilityDocumentV2,+ /*sendChangeNotifications=*/ false,+ /*logger=*/null);++ // Persist to disk and re-open the AppSearchImpl+ appSearchImplInV2.close();+ AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,+ new AppSearchConfigImpl(new UnlimitedLimitConfig(),+ new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,+ /*visibilityChecker=*/ null,+ ALWAYS_OPTIMIZE);++ InternalVisibilityConfig actualConfig =+ VisibilityToDocumentConverter.createInternalVisibilityConfig(+ appSearchImpl.getDocument(+ VisibilityStore.VISIBILITY_PACKAGE_NAME,+ VisibilityStore.VISIBILITY_DATABASE_NAME,+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,+ /*id=*/ prefix + "Schema",+ /*typePropertyPaths=*/ Collections.emptyMap()),+ /*androidVOverlayDocument=*/null);++ assertThat(actualConfig.isNotDisplayedBySystem()).isTrue();+ assertThat(actualConfig.getVisibilityConfig().getAllowedPackages())+ .containsExactly(packageIdentifierFoo, packageIdentifierBar);+ assertThat(actualConfig.getVisibilityConfig().getRequiredPermissions())+ .containsExactlyElementsIn(ImmutableSet.of(+ ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),+ ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA),+ ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA)));++ // Check that the visibility overlay schema was added.+ getSchemaResponse = appSearchImpl.getSchema(+ VisibilityStore.VISIBILITY_PACKAGE_NAME,+ VisibilityStore.VISIBILITY_DATABASE_NAME,+ new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));+ assertThat(getSchemaResponse.getSchemas()).containsExactly(+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,+ VisibilityPermissionConfig.SCHEMA);+ getAndroidVOverlaySchemaResponse = appSearchImpl.getSchema(+ VisibilityStore.VISIBILITY_PACKAGE_NAME,+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,+ new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));+ assertThat(getAndroidVOverlaySchemaResponse.getSchemas()).containsExactly(+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA);++ // But no overlay document was created.+ AppSearchException e = assertThrows(AppSearchException.class,+ () -> appSearchImpl.getDocument(+ VisibilityStore.VISIBILITY_PACKAGE_NAME,+ VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,+ /*id=*/ prefix + "Schema",+ /*typePropertyPaths=*/ Collections.emptyMap()));+ assertThat(e).hasMessageThat().contains("not found");++ appSearchImpl.close();+ }+}+
@@ -0,0 +1,58 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.localstorage.visibilitystore;++import static com.google.common.truth.Truth.assertThat;++import org.junit.Test;++public class VisibilityUtilTest {+ @Test+ public void testIsSchemaSearchableByCaller_selfAccessDefaultAllowed() {+ CallerAccess callerAccess = new CallerAccess("package1");+ assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,+ /*targetPackageName=*/ "package1",+ /*prefixedSchema=*/ "schema",+ /*visibilityStore=*/ null,+ /*visibilityChecker=*/ null)).isTrue();+ assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,+ /*targetPackageName=*/ "package2",+ /*prefixedSchema=*/ "schema",+ /*visibilityStore=*/ null,+ /*visibilityChecker=*/ null)).isFalse();+ }++ @Test+ public void testIsSchemaSearchableByCaller_selfAccessNotAllowed() {+ CallerAccess callerAccess = new CallerAccess("package1") {+ @Override+ public boolean doesCallerHaveSelfAccess() {+ return false;+ }+ };+ assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,+ /*targetPackageName=*/ "package1",+ /*prefixedSchema=*/ "schema",+ /*visibilityStore=*/ null,+ /*visibilityChecker=*/ null)).isFalse();+ assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,+ /*targetPackageName=*/ "package2",+ /*prefixedSchema=*/ "schema",+ /*visibilityStore=*/ null,+ /*visibilityChecker=*/ null)).isFalse();+ }+}
@@ -48,6 +48,12 @@
// fall through case Features.LIST_FILTER_QUERY_LANGUAGE: // fall through+ case Features.LIST_FILTER_HAS_PROPERTY_FUNCTION:+ // fall through+ case Features.LIST_FILTER_TOKENIZE_FUNCTION:+ // fall through+ case Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG:+ // fall through case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA: // fall through case Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH:@@ -60,15 +66,23 @@
// fall through case Features.SEARCH_SUGGESTION: // fall through- case Features.SCHEMA_SET_DELETION_PROPAGATION:- // fall through case Features.SET_SCHEMA_CIRCULAR_REFERENCES: // fall through case Features.SCHEMA_ADD_PARENT_TYPE: // fall through+ case Features.SCHEMA_SET_DESCRIPTION:+ // fall through case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES: // fall through case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:+ // fall through+ case Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG:+ // fall through+ case Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE:+ // fall through+ case Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG:+ // fall through+ case Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS: return true; default: return false;
Instead, logger instance needs to be passed to each individual method, like create, query
* and putDocument. *- * @param initStatsBuilder collects stats for initialization if provided.+ * @param initStatsBuilder collects stats for initialization if provided. * @param visibilityChecker The {@link VisibilityChecker} that check whether the caller has * access to aa specific schema. Pass null will lost that ability and * global querier could only get their own data.@@ -269,8 +268,8 @@
@NonNull File icingDir, @NonNull AppSearchConfig config, @Nullable InitializeStats.Builder initStatsBuilder,- @NonNull OptimizeStrategy optimizeStrategy,- @Nullable VisibilityChecker visibilityChecker)+ @Nullable VisibilityChecker visibilityChecker,+ @NonNull OptimizeStrategy optimizeStrategy) throws AppSearchException { return new AppSearchImpl(icingDir, config, initStatsBuilder, optimizeStrategy, visibilityChecker);@@ -463,10 +462,10 @@
* @param packageName The package name that owns the schemas. * @param databaseName The name of the database where this schema lives. * @param schemas Schemas to set for this app.- * @param visibilityDocuments {@link VisibilityDocument}s that contain all+ * @param visibilityConfigs {@link InternalVisibilityConfig}s that contain all * visibility setting information for those schemas * has user custom settings. Other schemas in the list- * that don't has a {@link VisibilityDocument}+ * that don't has a {@link InternalVisibilityConfig} * will be treated as having the default visibility, * which is accessible by the system and no other packages. * @param forceOverride Whether to force-apply the schema even if it is@@ -490,7 +489,7 @@
@NonNull String packageName, @NonNull String databaseName, @NonNull List schemas,- @NonNull List visibilityDocuments,+ @NonNull List visibilityConfigs, boolean forceOverride, int version, @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {@@ -508,7 +507,7 @@
packageName, databaseName, schemas,- visibilityDocuments,+ visibilityConfigs, forceOverride, version, setSchemaStatsBuilder);@@ -517,7 +516,7 @@
packageName, databaseName, schemas,- visibilityDocuments,+ visibilityConfigs, forceOverride, version, setSchemaStatsBuilder);@@ -539,7 +538,7 @@
@NonNull String packageName, @NonNull String databaseName, @NonNull List schemas,- @NonNull List visibilityDocuments,+ @NonNull List visibilityConfigs, boolean forceOverride, int version, @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {@@ -588,7 +587,7 @@
packageName, databaseName, schemas,- visibilityDocuments,+ visibilityConfigs, forceOverride, version, setSchemaStatsBuilder);@@ -717,7 +716,7 @@
@NonNull String packageName, @NonNull String databaseName, @NonNull List schemas,- @NonNull List visibilityDocuments,+ @NonNull List visibilityConfigs, boolean forceOverride, int version, @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {@@ -801,32 +800,34 @@
// store before we have already created it. if (mVisibilityStoreLocked != null) { // Add prefix to all visibility documents.- List prefixedVisibilityDocuments =- new ArrayList<>(visibilityDocuments.size()); // Find out which Visibility document is deleted or changed to all-default settings. // We need to remove them from Visibility Store. Set deprecatedVisibilityDocuments = new ArraySet<>(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet());- for (int i = 0; i < visibilityDocuments.size(); i++) {- VisibilityDocument unPrefixedDocument = visibilityDocuments.get(i);- // The VisibilityDocument is controlled by the client and it's untrusted but we+ List prefixedVisibilityConfigs =+ new ArrayList<>(visibilityConfigs.size());+ for (int i = 0; i < visibilityConfigs.size(); i++) {+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);+ // The VisibilityConfig is controlled by the client and it's untrusted but we // make it safe by appending a prefix. // We must control the package-database prefix. Therefore even if the client // fake the id, they can only mess their own app. That's totally allowed and // they can do this via the public API too.- String prefixedSchemaType = prefix + unPrefixedDocument.getId();- prefixedVisibilityDocuments.add(- new VisibilityDocument.Builder(- unPrefixedDocument).setId(prefixedSchemaType).build());+ // TODO(b/275592563): Move prefixing into VisibilityConfig.createVisibilityDocument+ // and createVisibilityOverlay+ String schemaType = visibilityConfig.getSchemaType();+ String prefixedSchemaType = prefix + schemaType;+ prefixedVisibilityConfigs.add(new InternalVisibilityConfig.Builder(visibilityConfig)+ .setSchemaType(prefixedSchemaType).build()); // This schema has visibility settings. We should keep it from the removal list.- deprecatedVisibilityDocuments.remove(prefixedSchemaType);+ deprecatedVisibilityDocuments.remove(visibilityConfig.getSchemaType()); } // Now deprecatedVisibilityDocuments contains those existing schemas that has // all-default visibility settings, add deleted schemas. That's all we need to // remove. deprecatedVisibilityDocuments.addAll(rewrittenSchemaResults.mDeletedPrefixedTypes); mVisibilityStoreLocked.removeVisibility(deprecatedVisibilityDocuments);- mVisibilityStoreLocked.setVisibility(prefixedVisibilityDocuments);+ mVisibilityStoreLocked.setVisibility(prefixedVisibilityConfigs); } long saveVisibilitySettingEndTimeMillis = SystemClock.elapsedRealtime(); if (setSchemaStatsBuilder != null) {@@ -904,33 +905,44 @@
// schema. Avoid call visibility store before we have already created it. if (mVisibilityStoreLocked != null) { String typeName = typeConfig.getSchemaType().substring(typePrefix.length());- VisibilityDocument visibilityDocument =+ InternalVisibilityConfig visibilityConfig = mVisibilityStoreLocked.getVisibility(prefixedSchemaType);- if (visibilityDocument != null) {- if (visibilityDocument.isNotDisplayedBySystem()) {- responseBuilder- .addSchemaTypeNotDisplayedBySystem(typeName);+ if (visibilityConfig != null) {+ if (visibilityConfig.isNotDisplayedBySystem()) {+ responseBuilder.addSchemaTypeNotDisplayedBySystem(typeName); }- String[] packageNames = visibilityDocument.getPackageNames();- byte[][] sha256Certs = visibilityDocument.getSha256Certs();- if (packageNames.length != sha256Certs.length) {- throw new AppSearchException(RESULT_INTERNAL_ERROR,- "The length of package names and sha256Crets are different!");- }- if (packageNames.length != 0) {- Set packageIdentifier = new ArraySet<>();- for (int j = 0; j < packageNames.length; j++) {- packageIdentifier.add(new PackageIdentifier(- packageNames[j], sha256Certs[j]));- }+ List packageIdentifiers =+ visibilityConfig.getVisibilityConfig().getAllowedPackages();+ if (!packageIdentifiers.isEmpty()) { responseBuilder.setSchemaTypeVisibleToPackages(typeName,- packageIdentifier);+ new ArraySet<>(packageIdentifiers)); } Set> visibleToPermissions =- visibilityDocument.getVisibleToPermissions();- if (visibleToPermissions != null) {- responseBuilder.setRequiredPermissionsForSchemaTypeVisibility(- typeName, visibleToPermissions);+ visibilityConfig.getVisibilityConfig().getRequiredPermissions();+ if (!visibleToPermissions.isEmpty()) {+ Set> visibleToPermissionsSet =+ new ArraySet<>(visibleToPermissions.size());+ for (Set permissionList : visibleToPermissions) {+ visibleToPermissionsSet.add(new ArraySet<>(permissionList));+ }++ responseBuilder.setRequiredPermissionsForSchemaTypeVisibility(typeName,+ visibleToPermissionsSet);+ }++ // Check for Visibility properties from the overlay+ PackageIdentifier publiclyVisibleFromPackage =+ visibilityConfig.getVisibilityConfig()+ .getPubliclyVisibleTargetPackage();+ if (publiclyVisibleFromPackage != null) {+ responseBuilder.setPubliclyVisibleSchema(+ typeName, publiclyVisibleFromPackage);+ }+ Set visibleToConfigs =+ visibilityConfig.getVisibleToConfigs();+ if (!visibleToConfigs.isEmpty()) {+ responseBuilder.setSchemaTypeVisibleToConfigs(+ typeName, visibleToConfigs); } } }@@ -1254,6 +1266,8 @@
*/ @NonNull @GuardedBy("mReadWriteLock")+ // We only log getResultProto.toString() in fullPii trace for debugging.+ @SuppressWarnings("LiteProtoToString") private DocumentProto getDocumentProtoByIdLocked( @NonNull String packageName, @NonNull String databaseName,@@ -1317,8 +1331,9 @@
SearchStats.Builder sStatsBuilder = null; if (logger != null) { sStatsBuilder =- new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL,- packageName).setDatabase(databaseName);+ new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL, packageName)+ .setDatabase(databaseName)+ .setSearchSourceLogTag(searchSpec.getSearchSourceLogTag()); } long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime();@@ -1338,7 +1353,7 @@
if (sStatsBuilder != null && logger != null) { sStatsBuilder.setStatusCode(AppSearchResult.RESULT_SECURITY_ERROR); }- return new SearchResultPage(Bundle.EMPTY);+ return new SearchResultPage(); } String prefix = createPrefix(packageName, databaseName);@@ -1349,7 +1364,7 @@
if (searchSpecToProtoConverter.hasNothingToSearch()) { // there is nothing to search over given their search filters, so we can return an // empty SearchResult and skip sending request to Icing.- return new SearchResultPage(Bundle.EMPTY);+ return new SearchResultPage(); } SearchResultPage searchResultPage =@@ -1394,7 +1409,8 @@
sStatsBuilder = new SearchStats.Builder( SearchStats.VISIBILITY_SCOPE_GLOBAL,- callerAccess.getCallingPackageName());+ callerAccess.getCallingPackageName())+ .setSearchSourceLogTag(searchSpec.getSearchSourceLogTag()); } long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime();@@ -1417,13 +1433,13 @@
// SearchSpec that wants to query every visible package. Set packageFilters = new ArraySet<>(); if (!searchSpec.getFilterPackageNames().isEmpty()) {- if (searchSpec.getJoinSpec() == null) {+ JoinSpec joinSpec = searchSpec.getJoinSpec();+ if (joinSpec == null) { packageFilters.addAll(searchSpec.getFilterPackageNames());- } else if (!searchSpec.getJoinSpec().getNestedSearchSpec()+ } else if (!joinSpec.getNestedSearchSpec() .getFilterPackageNames().isEmpty()) { packageFilters.addAll(searchSpec.getFilterPackageNames());- packageFilters.addAll(- searchSpec.getJoinSpec().getNestedSearchSpec().getFilterPackageNames());+ packageFilters.addAll(joinSpec.getNestedSearchSpec().getFilterPackageNames()); } }@@ -1452,7 +1468,7 @@
if (searchSpecToProtoConverter.hasNothingToSearch()) { // there is nothing to search over given their search filters, so we can return an // empty SearchResult and skip sending request to Icing.- return new SearchResultPage(Bundle.EMPTY);+ return new SearchResultPage(); } if (sStatsBuilder != null) { sStatsBuilder.setAclCheckLatencyMillis(@@ -1510,6 +1526,8 @@
} @GuardedBy("mReadWriteLock")+ // We only log searchSpec, scoringSpec and resultSpec in fullPii trace for debugging.+ @SuppressWarnings("LiteProtoToString") private SearchResultProto searchInIcingLocked( @NonNull SearchSpecProto searchSpec, @NonNull ResultSpecProto resultSpec,@@ -2758,22 +2776,26 @@
} if (LogUtil.DEBUG) { if (Log.isLoggable(icingTag, Log.VERBOSE)) {- IcingSearchEngine.setLoggingLevel(LogSeverity.Code.VERBOSE, /*verbosity=*/- (short) 1);+ boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.VERBOSE,+ /*verbosity=*/ (short) 1); return; } else if (Log.isLoggable(icingTag, Log.DEBUG)) {- IcingSearchEngine.setLoggingLevel(LogSeverity.Code.DBG);+ boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.DBG); return; } }- if (Log.isLoggable(icingTag, Log.INFO)) {- IcingSearchEngine.setLoggingLevel(LogSeverity.Code.INFO);- } else if (Log.isLoggable(icingTag, Log.WARN)) {- IcingSearchEngine.setLoggingLevel(LogSeverity.Code.WARNING);+ if (LogUtil.INFO) {+ if (Log.isLoggable(icingTag, Log.INFO)) {+ boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.INFO);+ return;+ }+ }+ if (Log.isLoggable(icingTag, Log.WARN)) {+ boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.WARNING); } else if (Log.isLoggable(icingTag, Log.ERROR)) {- IcingSearchEngine.setLoggingLevel(LogSeverity.Code.ERROR);+ boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.ERROR); } else {- IcingSearchEngine.setLoggingLevel(LogSeverity.Code.FATAL);+ boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.FATAL); } }@@ -2814,7 +2836,7 @@
* {@link AppSearchResult} code. * @return {@link AppSearchResult} error code */- private static @AppSearchResult.ResultCode int statusProtoToResultCode(+ @AppSearchResult.ResultCode private static int statusProtoToResultCode( @NonNull StatusProto statusProto) { return ResultCodeToProtoConverter.toResultCode(statusProto.getCode()); }
@@ -36,7 +36,7 @@
boolean DEFAULT_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT = false;- float DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD = 0.0f;+ float DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD = 0.9f; /** * The default compression level in IcingSearchEngineOptions proto matches the@@ -62,7 +62,7 @@
*/ int DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD = 65536;- boolean DEFAULT_LITE_INDEX_SORT_AT_INDEXING = false;+ boolean DEFAULT_LITE_INDEX_SORT_AT_INDEXING = true; /** * The default sort threshold for the lite index when sort at indexing is enabled.
@@ -26,6 +26,7 @@
import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; import androidx.appsearch.annotation.Document;+import androidx.appsearch.app.AppSearchEnvironmentFactory; import androidx.appsearch.app.AppSearchSession; import androidx.appsearch.app.GlobalSearchSession; import androidx.appsearch.exceptions.AppSearchException;@@ -39,7 +40,6 @@
import java.io.File; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService;-import java.util.concurrent.Executors; /** * An AppSearch storage system which stores data locally in the app's storage space using a bundled@@ -56,8 +56,6 @@
public class LocalStorage { private static final String TAG = "AppSearchLocalStorage";- private static final String ICING_LIB_ROOT_DIR = "appsearch";- /** Contains information about how to create the search session. */ public static final class SearchContext { final Context mContext;@@ -249,7 +247,8 @@
// AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all // mutate requests will need to gain write lock and query requests need to gain read lock.- static final Executor EXECUTOR = Executors.newCachedThreadPool();+ static final Executor EXECUTOR = AppSearchEnvironmentFactory.getEnvironmentInstance()+ .createCachedThreadPoolExecutor(); private static volatile LocalStorage sInstance;@@ -326,7 +325,8 @@
@Nullable AppSearchLogger logger) throws AppSearchException { Preconditions.checkNotNull(context);- File icingDir = new File(context.getFilesDir(), ICING_LIB_ROOT_DIR);+ File icingDir = AppSearchEnvironmentFactory.getEnvironmentInstance()+ .getAppSearchDir(context, /* userHandle= */ null); long totalLatencyStartMillis = SystemClock.elapsedRealtime(); InitializeStats.Builder initStatsBuilder = null;@@ -346,8 +346,8 @@
/* shouldRetrieveParentInfo= */ true ), initStatsBuilder,- new JetpackOptimizeStrategy(),- /*visibilityChecker=*/null);+ /*visibilityChecker=*/ null,+ new JetpackOptimizeStrategy()); if (logger != null) { initStatsBuilder.setTotalLatencyMillis(
@@ -19,6 +19,7 @@
import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.appsearch.app.AppSearchSchema;+import androidx.appsearch.app.EmbeddingVector; import androidx.appsearch.app.GenericDocument; import androidx.appsearch.exceptions.AppSearchException; import androidx.appsearch.localstorage.AppSearchConfig;@@ -54,6 +55,8 @@
private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0]; private static final byte[][] EMPTY_BYTES_ARRAY = new byte[0][0]; private static final GenericDocument[] EMPTY_DOCUMENT_ARRAY = new GenericDocument[0];+ private static final EmbeddingVector[] EMPTY_EMBEDDING_ARRAY =+ new EmbeddingVector[0]; private GenericDocumentToProtoConverter() { }@@ -109,6 +112,12 @@
DocumentProto proto = toDocumentProto(documentValues[j]); propertyProto.addDocumentValues(proto); }+ } else if (property instanceof EmbeddingVector[]) {+ EmbeddingVector[] embeddingValues = (EmbeddingVector[]) property;+ for (int j = 0; j < embeddingValues.length; j++) {+ propertyProto.addVectorValues(+ embeddingVectorToVectorProto(embeddingValues[j]));+ } } else if (property == null) { throw new IllegalStateException( String.format("Property \"%s\" doesn't have any value!", name));@@ -205,6 +214,13 @@
schemaTypeMap, config); } documentBuilder.setPropertyDocument(name, values);+ } else if (property.getVectorValuesCount() > 0) {+ EmbeddingVector[] values =+ new EmbeddingVector[property.getVectorValuesCount()];+ for (int j = 0; j < values.length; j++) {+ values[j] = vectorProtoToEmbeddingVector(property.getVectorValues(j));+ }+ documentBuilder.setPropertyEmbedding(name, values); } else { // TODO(b/184966497): Optimize by caching PropertyConfigProto SchemaTypeConfigProto schema =@@ -216,6 +232,37 @@
} /**+ * Converts a {@link PropertyProto.VectorProto} into an {@link EmbeddingVector}.+ */+ @NonNull+ public static EmbeddingVector vectorProtoToEmbeddingVector(+ @NonNull PropertyProto.VectorProto vectorProto) {+ Preconditions.checkNotNull(vectorProto);++ float[] values = new float[vectorProto.getValuesCount()];+ for (int i = 0; i < vectorProto.getValuesCount(); i++) {+ values[i] = vectorProto.getValues(i);+ }+ return new EmbeddingVector(values, vectorProto.getModelSignature());+ }++ /**+ * Converts an {@link EmbeddingVector} into a {@link PropertyProto.VectorProto}.+ */+ @NonNull+ public static PropertyProto.VectorProto embeddingVectorToVectorProto(+ @NonNull EmbeddingVector embedding) {+ Preconditions.checkNotNull(embedding);++ PropertyProto.VectorProto.Builder builder = PropertyProto.VectorProto.newBuilder();+ for (int i = 0; i < embedding.getValues().length; i++) {+ builder.addValues(embedding.getValues()[i]);+ }+ builder.setModelSignature(embedding.getModelSignature());+ return builder.build();+ }++ /** * Get the list of unprefixed parent type names of {@code prefixedSchemaType}. * *
It's guaranteed that child types always appear before parent types in the list.
@@ -305,6 +352,9 @@
case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT: documentBuilder.setPropertyDocument(propertyName, EMPTY_DOCUMENT_ARRAY); break;+ case AppSearchSchema.PropertyConfig.DATA_TYPE_EMBEDDING:+ documentBuilder.setPropertyEmbedding(propertyName, EMPTY_EMBEDDING_ARRAY);+ break; default: throw new IllegalStateException("Unknown type of value: " + propertyName); }
@@ -35,7 +35,7 @@
private ResultCodeToProtoConverter() {} /** Converts an {@link StatusProto.Code} into a {@link AppSearchResult.ResultCode}. */- public static @AppSearchResult.ResultCode int toResultCode(+ @AppSearchResult.ResultCode public static int toResultCode( @NonNull StatusProto.Code statusCode) { switch (statusCode) { case OK:
@@ -26,6 +26,8 @@
import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo;+import androidx.appsearch.app.EmbeddingVector;+import androidx.appsearch.app.FeatureConstants; import androidx.appsearch.app.JoinSpec; import androidx.appsearch.app.SearchResult; import androidx.appsearch.app.SearchSpec;@@ -190,12 +192,12 @@
} /**- * @return whether this search's target filters are empty. If any target filter is empty, we+ * Returns whether this search's target filters are empty. If any target filter is empty, we * should skip send request to Icing. *- *
The nestedConverter is not checked as {@link SearchResult}s from the nested query have
- * to be joined to a {@link SearchResult} from the parent query. If the parent query has- * nothing to search, then so does the child query.+ *
The nestedConverter is not checked as {@link SearchResult}s from the nested query have to
+ * be joined to a {@link SearchResult} from the parent query. If the parent query has nothing to+ * search, then so does the child query. */ public boolean hasNothingToSearch() { return mTargetPrefixedNamespaceFilters.isEmpty() || mTargetPrefixedSchemaFilters.isEmpty();@@ -291,6 +293,13 @@
.addAllSchemaTypeFilters(mTargetPrefixedSchemaFilters) .setUseReadOnlySearch(mIcingOptionsConfig.getUseReadOnlySearch());+ List searchEmbeddings = mSearchSpec.getSearchEmbeddings();+ for (int i = 0; i < searchEmbeddings.size(); i++) {+ protoBuilder.addEmbeddingQueryVectors(+ GenericDocumentToProtoConverter.embeddingVectorToVectorProto(+ searchEmbeddings.get(i)));+ }+ // Convert type property filter map into type property mask proto. for (Map.Entry> entry : mSearchSpec.getFilterProperties().entrySet()) {@@ -319,6 +328,17 @@
} protoBuilder.setTermMatchType(termMatchCodeProto);+ @SearchSpec.EmbeddingSearchMetricType int embeddingSearchMetricType =+ mSearchSpec.getDefaultEmbeddingSearchMetricType();+ SearchSpecProto.EmbeddingQueryMetricType.Code embeddingSearchMetricTypeProto =+ SearchSpecProto.EmbeddingQueryMetricType.Code.forNumber(embeddingSearchMetricType);+ if (embeddingSearchMetricTypeProto == null || embeddingSearchMetricTypeProto.equals(+ SearchSpecProto.EmbeddingQueryMetricType.Code.UNKNOWN)) {+ throw new IllegalArgumentException(+ "Invalid embedding search metric type: " + embeddingSearchMetricType);+ }+ protoBuilder.setEmbeddingQueryMetricType(embeddingSearchMetricTypeProto);+ if (mNestedConverter != null && !mNestedConverter.hasNothingToSearch()) { JoinSpecProto.NestedSpecProto nestedSpec = JoinSpecProto.NestedSpecProto.newBuilder()@@ -342,18 +362,18 @@
protoBuilder.setJoinSpec(joinSpecProtoBuilder); }- // TODO(b/208654892) Remove this field once EXPERIMENTAL_ICING_ADVANCED_QUERY is fully- // supported.- boolean turnOnIcingAdvancedQuery =- mSearchSpec.isNumericSearchEnabled() || mSearchSpec.isVerbatimSearchEnabled()- || mSearchSpec.isListFilterQueryLanguageEnabled();- if (turnOnIcingAdvancedQuery) {- protoBuilder.setSearchType(- SearchSpecProto.SearchType.Code.EXPERIMENTAL_ICING_ADVANCED_QUERY);+ if (mSearchSpec.isListFilterHasPropertyFunctionEnabled()+ && !mIcingOptionsConfig.getBuildPropertyExistenceMetadataHits()) {+ // This condition should never be reached as long as Features.isFeatureSupported() is+ // consistent with IcingOptionsConfig.+ throw new UnsupportedOperationException(+ FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION+ + " is currently not operational because the building process for the "+ + "associated metadata has not yet been turned on."); } // Set enabled features- protoBuilder.addAllEnabledFeatures(mSearchSpec.getEnabledFeatures());+ protoBuilder.addAllEnabledFeatures(toIcingSearchFeatures(mSearchSpec.getEnabledFeatures())); return protoBuilder.build(); }@@ -468,7 +488,7 @@
// Rewrite filters to include a database prefix. for (int i = 0; i < typePropertyMaskBuilders.size(); i++) { String unprefixedType = typePropertyMaskBuilders.get(i).getSchemaType();- if (unprefixedType.equals(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD)) {+ if (unprefixedType.equals(SearchSpec.SCHEMA_TYPE_WILDCARD)) { resultSpecBuilder.addTypePropertyMasks(typePropertyMaskBuilders.get(i).build()); } else { // Qualify the given schema types@@ -502,6 +522,8 @@
addTypePropertyWeights(mSearchSpec.getPropertyWeights(), protoBuilder); protoBuilder.setAdvancedScoringExpression(mSearchSpec.getAdvancedRankingExpression());+ protoBuilder.addAllAdditionalAdvancedScoringExpressions(+ mSearchSpec.getInformationalRankingExpressions()); return protoBuilder.build(); }@@ -536,6 +558,26 @@
} /**+ * Maps a list of AppSearch search feature strings to the list of the corresponding Icing+ * feature strings.+ *+ * @param appSearchFeatures The list of AppSearch search feature strings.+ */+ @NonNull+ private static List toIcingSearchFeatures(@NonNull List appSearchFeatures) {+ List result = new ArrayList<>();+ for (int i = 0; i < appSearchFeatures.size(); i++) {+ String appSearchFeature = appSearchFeatures.get(i);+ if (appSearchFeature.equals(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION)) {+ result.add("HAS_PROPERTY_FUNCTION");+ } else {+ result.add(appSearchFeature);+ }+ }+ return result;+ }++ /** * Returns a Map of namespace to prefixedNamespaces. This is NOT necessarily the * same as the list of namespaces. If a namespace exists under different packages and/or * different databases, they should still be grouped together.@@ -593,7 +635,7 @@
String packageName = getPackageName(prefix); // Create a new prefix without the database name. This will allow us to group namespaces // that have the same name and package but a different database name together.- String emptyDatabasePrefix = createPrefix(packageName, /*databaseName*/"");+ String emptyDatabasePrefix = createPrefix(packageName, /* databaseName= */""); for (String prefixedNamespace : prefixedNamespaces) { String namespace; try {
@@ -87,7 +87,7 @@
} /**- * @return whether this search's target filters are empty. If any target filter is empty, we+ * Returns whether this search's target filters are empty. If any target filter is empty, we * should skip send request to Icing. */ public boolean hasNothingToSearch() {
@@ -45,6 +45,7 @@
*/ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class CallStats {+ /** Call types. */ @IntDef(value = { CALL_TYPE_UNKNOWN, CALL_TYPE_INITIALIZE,@@ -77,6 +78,7 @@
CALL_TYPE_REGISTER_OBSERVER_CALLBACK, CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK, CALL_TYPE_GLOBAL_GET_NEXT_PAGE,+ CALL_TYPE_EXECUTE_APP_FUNCTION }) @Retention(RetentionPolicy.SOURCE) public @interface CallType {@@ -113,6 +115,7 @@
public static final int CALL_TYPE_REGISTER_OBSERVER_CALLBACK = 28; public static final int CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK = 29; public static final int CALL_TYPE_GLOBAL_GET_NEXT_PAGE = 30;+ public static final int CALL_TYPE_EXECUTE_APP_FUNCTION = 31; // These strings are for the subset of call types that correspond to an AppSearchManager API private static final String CALL_TYPE_STRING_INITIALIZE = "initialize";@@ -144,6 +147,7 @@
private static final String CALL_TYPE_STRING_UNREGISTER_OBSERVER_CALLBACK = "globalUnregisterObserverCallback"; private static final String CALL_TYPE_STRING_GLOBAL_GET_NEXT_PAGE = "globalGetNextPage";+ private static final String CALL_TYPE_STRING_EXECUTE_APP_FUNCTION = "executeAppFunction"; @Nullable private final String mPackageName;@@ -410,6 +414,8 @@
return CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK; case CALL_TYPE_STRING_GLOBAL_GET_NEXT_PAGE: return CALL_TYPE_GLOBAL_GET_NEXT_PAGE;+ case CALL_TYPE_STRING_EXECUTE_APP_FUNCTION:+ return CALL_TYPE_EXECUTE_APP_FUNCTION; default: return CALL_TYPE_UNKNOWN; }@@ -445,6 +451,7 @@
CALL_TYPE_GET_STORAGE_INFO, CALL_TYPE_REGISTER_OBSERVER_CALLBACK, CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK,- CALL_TYPE_GLOBAL_GET_NEXT_PAGE));+ CALL_TYPE_GLOBAL_GET_NEXT_PAGE,+ CALL_TYPE_EXECUTE_APP_FUNCTION)); } }
@@ -0,0 +1,117 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.localstorage.stats;++import androidx.annotation.NonNull;+import androidx.annotation.RestrictTo;+import androidx.appsearch.annotation.CanIgnoreReturnValue;+import androidx.core.util.Preconditions;++// TODO(b/319285816): link converter here.+/**+ * Class holds detailed stats of a click action, converted from+ * {@link androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public class ClickStats {+ private final long mTimestampMillis;++ private final long mTimeStayOnResultMillis;++ private final int mResultRankInBlock;++ private final int mResultRankGlobal;++ ClickStats(@NonNull Builder builder) {+ Preconditions.checkNotNull(builder);+ mTimestampMillis = builder.mTimestampMillis;+ mTimeStayOnResultMillis = builder.mTimeStayOnResultMillis;+ mResultRankInBlock = builder.mResultRankInBlock;+ mResultRankGlobal = builder.mResultRankGlobal;+ }++ /** Returns the click action timestamp in milliseconds since Unix epoch. */+ public long getTimestampMillis() {+ return mTimestampMillis;+ }++ /** Returns the time (duration) of the user staying on the clicked result. */+ public long getTimeStayOnResultMillis() {+ return mTimeStayOnResultMillis;+ }++ /** Returns the in-block rank of the clicked result. */+ public int getResultRankInBlock() {+ return mResultRankInBlock;+ }++ /** Returns the global rank of the clicked result. */+ public int getResultRankGlobal() {+ return mResultRankGlobal;+ }++ /** Builder for {@link ClickStats} */+ public static final class Builder {+ private long mTimestampMillis;++ private long mTimeStayOnResultMillis;++ private int mResultRankInBlock;++ private int mResultRankGlobal;++ /** Sets the click action timestamp in milliseconds since Unix epoch. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setTimestampMillis(long timestampMillis) {+ mTimestampMillis = timestampMillis;+ return this;+ }++ /** Sets the time (duration) of the user staying on the clicked result. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setTimeStayOnResultMillis(long timeStayOnResultMillis) {+ mTimeStayOnResultMillis = timeStayOnResultMillis;+ return this;+ }++ /** Sets the in-block rank of the clicked result. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setResultRankInBlock(int resultRankInBlock) {+ mResultRankInBlock = resultRankInBlock;+ return this;+ }++ /** Sets the global rank of the clicked result. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setResultRankGlobal(int resultRankGlobal) {+ mResultRankGlobal = resultRankGlobal;+ return this;+ }++ /** Builds a new {@link ClickStats} from the {@link ClickStats.Builder}. */+ @NonNull+ public ClickStats build() {+ return new ClickStats(/* builder= */ this);+ }+ }+}
@@ -37,6 +37,7 @@
*/ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public final class RemoveStats {+ /** Types of stats available for remove API. */ @IntDef(value = { // It needs to be sync with DeleteType.Code in // external/icing/proto/icing/proto/logging.proto#DeleteStatsProto
@@ -0,0 +1,279 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.localstorage.stats;++import androidx.annotation.IntDef;+import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RestrictTo;+import androidx.appsearch.annotation.CanIgnoreReturnValue;+import androidx.core.util.Preconditions;++import java.lang.annotation.Retention;+import java.lang.annotation.RetentionPolicy;+import java.util.ArrayList;+import java.util.Arrays;+import java.util.Collection;+import java.util.List;++// TODO(b/319285816): link converter here.+/**+ * Class holds detailed stats of a search intent, converted from+ * {@link androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}.+ *+ * A search intent includes a valid AppSearch search request, potentially followed by several user+ * click actions (see {@link ClickStats}) on fetched result documents. Related information of a+ * search intent will be extracted from+ * {@link androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public final class SearchIntentStats {+ /** AppSearch query correction type compared with the previous query. */+ @IntDef(value = {+ QUERY_CORRECTION_TYPE_UNKNOWN,+ QUERY_CORRECTION_TYPE_FIRST_QUERY,+ QUERY_CORRECTION_TYPE_REFINEMENT,+ QUERY_CORRECTION_TYPE_ABANDONMENT,+ })+ @Retention(RetentionPolicy.SOURCE)+ public @interface QueryCorrectionType {+ }++ public static final int QUERY_CORRECTION_TYPE_UNKNOWN = 0;++ public static final int QUERY_CORRECTION_TYPE_FIRST_QUERY = 1;++ public static final int QUERY_CORRECTION_TYPE_REFINEMENT = 2;++ public static final int QUERY_CORRECTION_TYPE_ABANDONMENT = 3;++ @NonNull+ private final String mPackageName;++ @Nullable+ private final String mDatabase;++ @Nullable+ private final String mPrevQuery;++ @Nullable+ private final String mCurrQuery;++ private final long mTimestampMillis;++ private final int mNumResultsFetched;++ @QueryCorrectionType+ private final int mQueryCorrectionType;++ @NonNull+ private final List mClicksStats;++ SearchIntentStats(@NonNull Builder builder) {+ Preconditions.checkNotNull(builder);+ mPackageName = builder.mPackageName;+ mDatabase = builder.mDatabase;+ mPrevQuery = builder.mPrevQuery;+ mCurrQuery = builder.mCurrQuery;+ mTimestampMillis = builder.mTimestampMillis;+ mNumResultsFetched = builder.mNumResultsFetched;+ mQueryCorrectionType = builder.mQueryCorrectionType;+ mClicksStats = builder.mClicksStats;+ }++ /** Returns calling package name. */+ @NonNull+ public String getPackageName() {+ return mPackageName;+ }++ /** Returns calling database name. */+ @Nullable+ public String getDatabase() {+ return mDatabase;+ }++ /** Returns the raw query string of the previous search intent. */+ @Nullable+ public String getPrevQuery() {+ return mPrevQuery;+ }++ /** Returns the raw query string of this (current) search intent. */+ @Nullable+ public String getCurrQuery() {+ return mCurrQuery;+ }++ /** Returns the search intent timestamp in milliseconds since Unix epoch. */+ public long getTimestampMillis() {+ return mTimestampMillis;+ }++ /**+ * Returns total number of results fetched from AppSearch by the client in this search intent.+ */+ public int getNumResultsFetched() {+ return mNumResultsFetched;+ }++ /**+ * Returns the correction type of the query in this search intent compared with the previous+ * search intent. Default value: {@link SearchIntentStats#QUERY_CORRECTION_TYPE_UNKNOWN}.+ */+ @QueryCorrectionType+ public int getQueryCorrectionType() {+ return mQueryCorrectionType;+ }++ /** Returns the list of {@link ClickStats} in this search intent. */+ @NonNull+ public List getClicksStats() {+ return mClicksStats;+ }++ /** Builder for {@link SearchIntentStats} */+ public static final class Builder {+ @NonNull+ private final String mPackageName;++ @Nullable+ private String mDatabase;++ @Nullable+ private String mPrevQuery;++ @Nullable+ private String mCurrQuery;++ private long mTimestampMillis;++ private int mNumResultsFetched;++ @QueryCorrectionType+ private int mQueryCorrectionType = QUERY_CORRECTION_TYPE_UNKNOWN;++ @NonNull+ private List mClicksStats = new ArrayList<>();++ private boolean mBuilt = false;++ /** Constructor for the {@link Builder}. */+ public Builder(@NonNull String packageName) {+ mPackageName = Preconditions.checkNotNull(packageName);+ }++ /** Sets calling database name. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setDatabase(@Nullable String database) {+ resetIfBuilt();+ mDatabase = database;+ return this;+ }++ /** Sets the raw query string of the previous search intent. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setPrevQuery(@Nullable String prevQuery) {+ resetIfBuilt();+ mPrevQuery = prevQuery;+ return this;+ }++ /** Sets the raw query string of this (current) search intent. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setCurrQuery(@Nullable String currQuery) {+ resetIfBuilt();+ mCurrQuery = currQuery;+ return this;+ }++ /** Sets the search intent timestamp in milliseconds since Unix epoch. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setTimestampMillis(long timestampMillis) {+ resetIfBuilt();+ mTimestampMillis = timestampMillis;+ return this;+ }++ /**+ * Sets total number of results fetched from AppSearch by the client in this search intent.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setNumResultsFetched(int numResultsFetched) {+ resetIfBuilt();+ mNumResultsFetched = numResultsFetched;+ return this;+ }++ /**+ * Sets the correction type of the query in this search intent compared with the previous+ * search intent.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setQueryCorrectionType(@QueryCorrectionType int queryCorrectionType) {+ resetIfBuilt();+ mQueryCorrectionType = queryCorrectionType;+ return this;+ }++ /** Adds one or more {@link ClickStats} objects to this search intent. */+ @CanIgnoreReturnValue+ @SuppressWarnings("MissingGetterMatchingBuilder")+ @NonNull+ public Builder addClicksStats(@NonNull ClickStats... clicksStats) {+ Preconditions.checkNotNull(clicksStats);+ resetIfBuilt();+ return addClicksStats(Arrays.asList(clicksStats));+ }++ /** Adds a collection of {@link ClickStats} objects to this search intent. */+ @CanIgnoreReturnValue+ @SuppressWarnings("MissingGetterMatchingBuilder")+ @NonNull+ public Builder addClicksStats(@NonNull Collection extends ClickStats> clicksStats) {+ Preconditions.checkNotNull(clicksStats);+ resetIfBuilt();+ mClicksStats.addAll(clicksStats);+ return this;+ }++ /**+ * If built, make a copy of previous data for every field so that the builder can be reused.+ */+ private void resetIfBuilt() {+ if (mBuilt) {+ mClicksStats = new ArrayList<>(mClicksStats);+ mBuilt = false;+ }+ }++ /** Builds a new {@link SearchIntentStats} from the {@link Builder}. */+ @NonNull+ public SearchIntentStats build() {+ mBuilt = true;+ return new SearchIntentStats(/* builder= */ this);+ }+ }+}
@@ -36,6 +36,7 @@
*/ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public final class SearchStats {+ /** Types of Visibility scopes available for search. */ @IntDef(value = { // Searches apps' own documents. VISIBILITY_SCOPE_LOCAL,@@ -137,6 +138,7 @@
private final int mNativeNumJoinedResultsCurrentPage; /** Time taken to join documents together. */ private final int mNativeJoinLatencyMillis;+ @Nullable private final String mSearchSourceLogTag; SearchStats(@NonNull Builder builder) { Preconditions.checkNotNull(builder);@@ -170,6 +172,7 @@
mJoinType = builder.mJoinType; mNativeNumJoinedResultsCurrentPage = builder.mNativeNumJoinedResultsCurrentPage; mNativeJoinLatencyMillis = builder.mNativeJoinLatencyMillis;+ mSearchSourceLogTag = builder.mSearchSourceLogTag; } /** Returns the package name of the session. */@@ -342,6 +345,12 @@
return mNativeJoinLatencyMillis; }+ /** Returns a tag to indicate the source of this search, or {code null} if never set. */+ @Nullable+ public String getSearchSourceLogTag() {+ return mSearchSourceLogTag;+ }+ /** Builder for {@link SearchStats} */ public static class Builder { @NonNull@@ -377,6 +386,7 @@
@JoinableValueType int mJoinType; int mNativeNumJoinedResultsCurrentPage; int mNativeJoinLatencyMillis;+ @Nullable String mSearchSourceLogTag; /** * Constructor@@ -604,6 +614,7 @@
} /** Sets whether or not this is a join query */+ @CanIgnoreReturnValue @NonNull public Builder setJoinType(@JoinableValueType int joinType) { mJoinType = joinType;@@ -611,6 +622,7 @@
} /** Set the total number of joined documents in a page. */+ @CanIgnoreReturnValue @NonNull public Builder setNativeNumJoinedResultsCurrentPage(int nativeNumJoinedResultsCurrentPage) { mNativeNumJoinedResultsCurrentPage = nativeNumJoinedResultsCurrentPage;@@ -618,12 +630,21 @@
} /** Sets time it takes to join documents together in icing. */+ @CanIgnoreReturnValue @NonNull public Builder setNativeJoinLatencyMillis(int nativeJoinLatencyMillis) { mNativeJoinLatencyMillis = nativeJoinLatencyMillis; return this; }+ /** Sets a tag to indicate the source of this search. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setSearchSourceLogTag(@Nullable String searchSourceLogTag) {+ mSearchSourceLogTag = searchSourceLogTag;+ return this;+ }+ /** * Constructs a new {@link SearchStats} from the contents of this * {@link SearchStats.Builder}.
@@ -0,0 +1,174 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.localstorage.usagereporting;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RestrictTo;+import androidx.appsearch.annotation.CanIgnoreReturnValue;+import androidx.appsearch.app.AppSearchResult;+import androidx.appsearch.app.AppSearchSchema;+import androidx.appsearch.app.AppSearchSession;+import androidx.appsearch.app.GenericDocument;+import androidx.appsearch.usagereporting.ActionConstants;+import androidx.core.util.Preconditions;++/**+ * Wrapper class for+ * + * {@link androidx.appsearch.usagereporting.ClickAction}+ * + * {@link GenericDocument}, which contains getters for click action properties.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public class ClickActionGenericDocument extends TakenActionGenericDocument {+ private static final String PROPERTY_PATH_QUERY = "query";+ private static final String PROPERTY_PATH_RESULT_RANK_IN_BLOCK = "resultRankInBlock";+ private static final String PROPERTY_PATH_RESULT_RANK_GLOBAL = "resultRankGlobal";+ private static final String PROPERTY_PATH_TIME_STAY_ON_RESULT_MILLIS = "timeStayOnResultMillis";++ ClickActionGenericDocument(@NonNull GenericDocument document) {+ super(Preconditions.checkNotNull(document));+ }++ /** Returns the string value of property {@code query}. */+ @Nullable+ public String getQuery() {+ return getPropertyString(PROPERTY_PATH_QUERY);+ }++ /** Returns the integer value of property {@code resultRankInBlock}. */+ public int getResultRankInBlock() {+ return (int) getPropertyLong(PROPERTY_PATH_RESULT_RANK_IN_BLOCK);+ }++ /** Returns the integer value of property {@code resultRankGlobal}. */+ public int getResultRankGlobal() {+ return (int) getPropertyLong(PROPERTY_PATH_RESULT_RANK_GLOBAL);+ }++ /** Returns the long value of property {@code timeStayOnResultMillis}. */+ public long getTimeStayOnResultMillis() {+ return getPropertyLong(PROPERTY_PATH_TIME_STAY_ON_RESULT_MILLIS);+ }++ /** Builder for {@link ClickActionGenericDocument}. */+ public static final class Builder extends TakenActionGenericDocument.Builder {+ /**+ * Creates a new {@link ClickActionGenericDocument.Builder}.+ *+ *
Document IDs are unique within a namespace.
+ *+ *
The number of namespaces per app should be kept small for efficiency reasons.
+ *+ * @param namespace the namespace to set for the {@link GenericDocument}.+ * @param id the unique identifier for the {@link GenericDocument} in its namespace.+ * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The+ * provided {@code schemaType} must be defined using+ * {@link AppSearchSession#setSchemaAsync} prior+ * to inserting a document of this {@code schemaType} into the+ * AppSearch index using+ * {@link AppSearchSession#putAsync}.+ * Otherwise, the document will be rejected by+ * {@link AppSearchSession#putAsync} with result code+ * {@link AppSearchResult#RESULT_NOT_FOUND}.+ */+ public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {+ super(Preconditions.checkNotNull(namespace), Preconditions.checkNotNull(id),+ Preconditions.checkNotNull(schemaType), ActionConstants.ACTION_TYPE_CLICK);+ }++ /**+ * Creates a new {@link ClickActionGenericDocument.Builder} from an existing+ * {@link GenericDocument}.+ *+ * @param document a generic document object.+ *+ * @throws IllegalArgumentException if the integer value of property {@code actionType} is+ * not {@link ActionConstants#ACTION_TYPE_CLICK}.+ */+ public Builder(@NonNull GenericDocument document) {+ super(Preconditions.checkNotNull(document));++ if (document.getPropertyLong(PROPERTY_PATH_ACTION_TYPE)+ != ActionConstants.ACTION_TYPE_CLICK) {+ throw new IllegalArgumentException(+ "Invalid action type for ClickActionGenericDocument");+ }+ }++ /**+ * Sets the string value of property {@code query} by the user-entered search input+ * (without any operators or rewriting) that yielded the+ * {@link androidx.appsearch.app.SearchResult} on which the user clicked.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setQuery(@NonNull String query) {+ Preconditions.checkNotNull(query);+ setPropertyString(PROPERTY_PATH_QUERY, query);+ return this;+ }++ /**+ * Sets the integer value of property {@code resultRankInBlock} by the rank of the clicked+ * {@link androidx.appsearch.app.SearchResult} document among the user-defined block.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setResultRankInBlock(int resultRankInBlock) {+ Preconditions.checkArgumentNonnegative(resultRankInBlock);+ setPropertyLong(PROPERTY_PATH_RESULT_RANK_IN_BLOCK, resultRankInBlock);+ return this;+ }++ /**+ * Sets the integer value of property {@code resultRankGlobal} by the global rank of the+ * clicked {@link androidx.appsearch.app.SearchResult} document.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setResultRankGlobal(int resultRankGlobal) {+ Preconditions.checkArgumentNonnegative(resultRankGlobal);+ setPropertyLong(PROPERTY_PATH_RESULT_RANK_GLOBAL, resultRankGlobal);+ return this;+ }++ /**+ * Sets the integer value of property {@code timeStayOnResultMillis} by the time in+ * milliseconds that user stays on the {@link androidx.appsearch.app.SearchResult} document+ * after clicking it.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setTimeStayOnResultMillis(long timeStayOnResultMillis) {+ setPropertyLong(PROPERTY_PATH_TIME_STAY_ON_RESULT_MILLIS, timeStayOnResultMillis);+ return this;+ }++ /** Builds a {@link ClickActionGenericDocument}. */+ @Override+ @NonNull+ public ClickActionGenericDocument build() {+ return new ClickActionGenericDocument(super.build());+ }+ }+}
@@ -0,0 +1,137 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.localstorage.usagereporting;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RestrictTo;+import androidx.appsearch.annotation.CanIgnoreReturnValue;+import androidx.appsearch.app.AppSearchResult;+import androidx.appsearch.app.AppSearchSchema;+import androidx.appsearch.app.AppSearchSession;+import androidx.appsearch.app.GenericDocument;+import androidx.appsearch.usagereporting.ActionConstants;+import androidx.core.util.Preconditions;++/**+ * Wrapper class for+ * + * {@link androidx.appsearch.usagereporting.SearchAction}+ * + * {@link GenericDocument}, which contains getters for search action properties.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public class SearchActionGenericDocument extends TakenActionGenericDocument {+ private static final String PROPERTY_PATH_QUERY = "query";+ private static final String PROPERTY_PATH_FETCHED_RESULT_COUNT = "fetchedResultCount";++ SearchActionGenericDocument(@NonNull GenericDocument document) {+ super(Preconditions.checkNotNull(document));+ }++ /** Returns the string value of property {@code query}. */+ @Nullable+ public String getQuery() {+ return getPropertyString(PROPERTY_PATH_QUERY);+ }++ /** Returns the integer value of property {@code fetchedResultCount}. */+ public int getFetchedResultCount() {+ return (int) getPropertyLong(PROPERTY_PATH_FETCHED_RESULT_COUNT);+ }++ /** Builder for {@link SearchActionGenericDocument}. */+ public static final class Builder extends TakenActionGenericDocument.Builder {+ /**+ * Creates a new {@link SearchActionGenericDocument.Builder}.+ *+ *
Document IDs are unique within a namespace.
+ *+ *
The number of namespaces per app should be kept small for efficiency reasons.
+ *+ * @param namespace the namespace to set for the {@link GenericDocument}.+ * @param id the unique identifier for the {@link GenericDocument} in its namespace.+ * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The+ * provided {@code schemaType} must be defined using+ * {@link AppSearchSession#setSchemaAsync} prior+ * to inserting a document of this {@code schemaType} into the+ * AppSearch index using+ * {@link AppSearchSession#putAsync}.+ * Otherwise, the document will be rejected by+ * {@link AppSearchSession#putAsync} with result code+ * {@link AppSearchResult#RESULT_NOT_FOUND}.+ */+ public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {+ super(Preconditions.checkNotNull(namespace), Preconditions.checkNotNull(id),+ Preconditions.checkNotNull(schemaType), ActionConstants.ACTION_TYPE_SEARCH);+ }++ /**+ * Creates a new {@link SearchActionGenericDocument.Builder} from an existing+ * {@link GenericDocument}.+ *+ * @param document a generic document object.+ *+ * @throws IllegalArgumentException if the integer value of property {@code actionType} is+ * not {@link ActionConstants#ACTION_TYPE_SEARCH}.+ */+ public Builder(@NonNull GenericDocument document) {+ super(Preconditions.checkNotNull(document));++ if (document.getPropertyLong(PROPERTY_PATH_ACTION_TYPE)+ != ActionConstants.ACTION_TYPE_SEARCH) {+ throw new IllegalArgumentException(+ "Invalid action type for SearchActionGenericDocument");+ }+ }++ /**+ * Sets the string value of property {@code query} by the user-entered search input+ * (without any operators or rewriting).+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setQuery(@NonNull String query) {+ Preconditions.checkNotNull(query);+ setPropertyString(PROPERTY_PATH_QUERY, query);+ return this;+ }++ /**+ * Sets the integer value of property {@code fetchedResultCount} by total number of results+ * fetched from AppSearch by the client in this search action.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setFetchedResultCount(int fetchedResultCount) {+ Preconditions.checkArgumentNonnegative(fetchedResultCount);+ setPropertyLong(PROPERTY_PATH_FETCHED_RESULT_COUNT, fetchedResultCount);+ return this;+ }++ /** Builds a {@link SearchActionGenericDocument}. */+ @Override+ @NonNull+ public SearchActionGenericDocument build() {+ return new SearchActionGenericDocument(super.build());+ }+ }+}
@@ -0,0 +1,337 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.localstorage.usagereporting;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RestrictTo;+import androidx.appsearch.app.GenericDocument;+import androidx.appsearch.localstorage.stats.ClickStats;+import androidx.appsearch.localstorage.stats.SearchIntentStats;+import androidx.appsearch.usagereporting.ActionConstants;++import java.util.ArrayList;+import java.util.Collections;+import java.util.List;+import java.util.Objects;++/**+ * Extractor class for analyzing a list of taken action {@link GenericDocument} and creating a list+ * of {@link SearchIntentStats}.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public final class SearchIntentStatsExtractor {+ // TODO(b/319285816): make thresholds configurable.+ /**+ * Threshold for noise search intent detection, in millisecond. A search action will be+ * considered as a noise (and skipped) if all of the following conditions are satisfied:+ *
+ *
The action timestamp (action document creation timestamp) difference between it and
+ * its previous search action is below this threshold.+ *
There is no click action associated with it.
+ *
Its raw query string is a prefix of the previous search action's raw query string (or
+ * the other way around).+ * + */+ private static final long NOISE_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS = 2000L;++ /**+ * Threshold for independent search intent detection, in millisecond. If the action timestamp+ * (action document creation timestamp) difference between the previous and the current search+ * action exceeds this threshold, then the current search action will be considered as a+ * completely independent search intent, and there will be no correlation analysis between the+ * previous and the current search action.+ */+ private static final long INDEPENDENT_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS =+ 10L * 60 * 1000;++ /**+ * Threshold for backspace count to become query abandonment. If the user hits backspace for at+ * least QUERY_ABANDONMENT_BACKSPACE_COUNT times, then the query correction type will be+ * determined as abandonment.+ */+ private static final int QUERY_ABANDONMENT_BACKSPACE_COUNT = 2;++ @NonNull+ private final String mPackageName;+ @Nullable+ private final String mDatabase;++ /**+ * Constructs {@link SearchIntentStatsExtractor} with the caller's package and database name.+ *+ * @param packageName The package name of the caller.+ * @param database The database name of the caller.+ */+ public SearchIntentStatsExtractor(@NonNull String packageName, @Nullable String database) {+ Objects.requireNonNull(packageName);+ mPackageName = packageName;+ mDatabase = database;+ }++ /**+ * Returns the query correction type between the previous and current search actions.+ *+ * @param currSearchAction the current search action {@link SearchActionGenericDocument}.+ * @param prevSearchAction the previous search action {@link SearchActionGenericDocument}.+ */+ public static @SearchIntentStats.QueryCorrectionType int getQueryCorrectionType(+ @NonNull SearchActionGenericDocument currSearchAction,+ @Nullable SearchActionGenericDocument prevSearchAction) {+ Objects.requireNonNull(currSearchAction);++ if (currSearchAction.getQuery() == null) {+ // Query correction type cannot be determined if the client didn't provide the raw query+ // string.+ return SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN;+ }+ if (prevSearchAction == null) {+ // If the previous search action is missing, then it is the first query.+ return SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY;+ } else if (prevSearchAction.getQuery() == null) {+ // Query correction type cannot be determined if the client didn't provide the raw query+ // string.+ return SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN;+ }++ // Determine the query correction type by comparing the current and previous raw query+ // strings.+ String prevQuery = prevSearchAction.getQuery();+ String currQuery = currSearchAction.getQuery();+ int commonPrefixLength = getCommonPrefixLength(prevQuery, currQuery);+ // If the user hits backspace >= QUERY_ABANDONMENT_BACKSPACE_COUNT times, then it is query+ // abandonment. Otherwise, it is query refinement.+ if (commonPrefixLength <= prevQuery.length() - QUERY_ABANDONMENT_BACKSPACE_COUNT) {+ return SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;+ } else {+ return SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT;+ }+ }++ /**+ * Returns a list of {@link SearchIntentStats} extracted from the given list of taken action+ * {@link GenericDocument} .+ *+ *
Search intent is consist of a valid search action with 0 or more click actions. To extract
+ * search intent metrics, this function will try to group the given taken actions into several+ * search intents, and yield a {@link SearchIntentStats} for each search intent.+ *+ * @param genericDocuments a list of taken actions in generic document form.+ */+ @NonNull+ public List extract(+ @NonNull List genericDocuments) {+ Objects.requireNonNull(genericDocuments);++ // Convert GenericDocument list to TakenActionGenericDocument list and sort them by document+ // creation timestamp.+ List takenActionGenericDocuments =+ new ArrayList<>(genericDocuments.size());+ for (int i = 0; i < genericDocuments.size(); ++i) {+ try {+ takenActionGenericDocuments.add(+ TakenActionGenericDocument.create(genericDocuments.get(i)));+ } catch (IllegalArgumentException e) {+ // Skip generic documents with unknown action type.+ }+ }+ Collections.sort(takenActionGenericDocuments,+ (TakenActionGenericDocument doc1, TakenActionGenericDocument doc2) ->+ Long.compare(doc1.getCreationTimestampMillis(),+ doc2.getCreationTimestampMillis()));++ List result = new ArrayList<>();+ SearchActionGenericDocument prevSearchAction = null;+ // Clients are expected to report search action followed by its associated click actions.+ // For example, [searchAction1, clickAction1, searchAction2, searchAction3, clickAction2,+ // clickAction3]:+ // - There are 3 search actions and 3 click actions.+ // - clickAction1 is associated with searchAction1.+ // - There is no click action associated with searchAction2.+ // - clickAction2 and clickAction3 are associated with searchAction3.+ // Here we're going to break down the list into segments. Each segment starts with a search+ // action followed by 0 or more associated click actions, and they form a single search+ // intent. We will analyze and extract metrics from the taken actions for the search intent.+ for (int i = 0; i < takenActionGenericDocuments.size(); ++i) {+ if (takenActionGenericDocuments.get(i).getActionType()+ != ActionConstants.ACTION_TYPE_SEARCH) {+ continue;+ }++ SearchActionGenericDocument currSearchAction =+ (SearchActionGenericDocument) takenActionGenericDocuments.get(i);+ List clickActions = new ArrayList<>();+ // Get all click actions associated with the current search action by advancing until+ // the next search action.+ while (i + 1 < takenActionGenericDocuments.size()+ && takenActionGenericDocuments.get(i + 1).getActionType()+ != ActionConstants.ACTION_TYPE_SEARCH) {+ if (takenActionGenericDocuments.get(i + 1).getActionType()+ == ActionConstants.ACTION_TYPE_CLICK) {+ clickActions.add(+ (ClickActionGenericDocument) takenActionGenericDocuments.get(i + 1));+ }+ ++i;+ }++ // Get the reference of the next search action if it exists.+ SearchActionGenericDocument nextSearchAction = null;+ if (i + 1 < takenActionGenericDocuments.size()+ && takenActionGenericDocuments.get(i + 1).getActionType()+ == ActionConstants.ACTION_TYPE_SEARCH) {+ nextSearchAction =+ (SearchActionGenericDocument) takenActionGenericDocuments.get(i + 1);+ }++ if (isIndependentSearchAction(currSearchAction, prevSearchAction)) {+ // If the current search action is independent from the previous one, then ignore+ // the previous search action when extracting stats.+ prevSearchAction = null;+ } else if (clickActions.isEmpty()+ && isIntermediateSearchAction(+ currSearchAction, prevSearchAction, nextSearchAction)) {+ // If the current search action is an intermediate search action with no click+ // actions, then we consider it as a noise and skip it.+ continue;+ }++ // Now we get a valid search intent (the current search action + a list of click actions+ // associated with it). Extract metrics and add SearchIntentStats.+ result.add(createSearchIntentStats(currSearchAction, clickActions, prevSearchAction));+ prevSearchAction = currSearchAction;+ }+ return result;+ }++ /**+ * Creates a {@link SearchIntentStats} object from the current search action + its associated+ * click actions, and the previous search action (in generic document form).+ */+ private SearchIntentStats createSearchIntentStats(+ @NonNull SearchActionGenericDocument currSearchAction,+ @NonNull List clickActions,+ @Nullable SearchActionGenericDocument prevSearchAction) {+ SearchIntentStats.Builder builder = new SearchIntentStats.Builder(mPackageName)+ .setDatabase(mDatabase)+ .setTimestampMillis(currSearchAction.getCreationTimestampMillis())+ .setCurrQuery(currSearchAction.getQuery())+ .setNumResultsFetched(currSearchAction.getFetchedResultCount())+ .setQueryCorrectionType(getQueryCorrectionType(currSearchAction, prevSearchAction));+ if (prevSearchAction != null) {+ builder.setPrevQuery(prevSearchAction.getQuery());+ }+ for (int i = 0; i < clickActions.size(); ++i) {+ builder.addClicksStats(createClickStats(clickActions.get(i)));+ }+ return builder.build();+ }++ /**+ * Creates a {@link ClickStats} object from the given click action (in generic document form).+ */+ private ClickStats createClickStats(ClickActionGenericDocument clickAction) {+ return new ClickStats.Builder()+ .setTimestampMillis(clickAction.getCreationTimestampMillis())+ .setResultRankInBlock(clickAction.getResultRankInBlock())+ .setResultRankGlobal(clickAction.getResultRankGlobal())+ .setTimeStayOnResultMillis(clickAction.getTimeStayOnResultMillis())+ .build();+ }++ /**+ * Returns if the current search action is an intermediate search action.+ *+ *
An intermediate search action is used for detecting the situation when the user adds or
+ * deletes characters from the query (e.g. "a" -> "app" -> "apple" or "apple" -> "app" -> "a")+ * within a short period of time. More precisely, it has to satisfy all of the following+ * conditions:+ *
+ *
There are related (non-independent) search actions before and after it.
+ *
It occurs within the threshold after its previous search action.
+ *
Its raw query string is a prefix of its previous search action's raw query string, or
+ * the opposite direction.+ * + */+ private static boolean isIntermediateSearchAction(+ @NonNull SearchActionGenericDocument currSearchAction,+ @Nullable SearchActionGenericDocument prevSearchAction,+ @Nullable SearchActionGenericDocument nextSearchAction) {+ Objects.requireNonNull(currSearchAction);++ if (prevSearchAction == null || nextSearchAction == null) {+ return false;+ }++ // Whether the next search action is independent from the current search action. If true,+ // then the current search action will not be considered as an intermediate search action+ // since it is the last search action of the related search sequence.+ boolean isNextSearchActionIndependent =+ isIndependentSearchAction(nextSearchAction, currSearchAction);++ // Whether the current search action occurs within the threshold after the previous search+ // action.+ boolean occursWithinTimeThreshold =+ currSearchAction.getCreationTimestampMillis()+ - prevSearchAction.getCreationTimestampMillis()+ <= NOISE_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS;++ // Whether the previous search action's raw query string is a prefix of the current search+ // action's, or the opposite direction (e.g. "app" -> "apple" and "apple" -> "app").+ String prevQuery = prevSearchAction.getQuery();+ String currQuery = currSearchAction.getQuery();+ boolean isPrefix = prevQuery != null && currQuery != null+ && (currQuery.startsWith(prevQuery) || prevQuery.startsWith(currQuery));++ return !isNextSearchActionIndependent && occursWithinTimeThreshold && isPrefix;+ }++ /**+ * Returns if the current search action is independent from the previous search action.+ *+ *
If the current search action occurs later than the threshold after the previous search
+ * action, then they are considered independent.+ */+ private static boolean isIndependentSearchAction(+ @NonNull SearchActionGenericDocument currSearchAction,+ @Nullable SearchActionGenericDocument prevSearchAction) {+ Objects.requireNonNull(currSearchAction);++ if (prevSearchAction == null) {+ return true;+ }++ long searchTimeDiffMillis = currSearchAction.getCreationTimestampMillis()+ - prevSearchAction.getCreationTimestampMillis();+ return searchTimeDiffMillis > INDEPENDENT_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS;+ }++ /** Returns the common prefix length of the given 2 strings. */+ private static int getCommonPrefixLength(@NonNull String s1, @NonNull String s2) {+ Objects.requireNonNull(s1);+ Objects.requireNonNull(s2);++ int minLength = Math.min(s1.length(), s2.length());+ for (int i = 0; i < minLength; ++i) {+ if (s1.charAt(i) != s2.charAt(i)) {+ return i;+ }+ }+ return minLength;+ }+}
@@ -0,0 +1,111 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.localstorage.usagereporting;++import androidx.annotation.NonNull;+import androidx.annotation.RestrictTo;+import androidx.appsearch.app.AppSearchResult;+import androidx.appsearch.app.AppSearchSchema;+import androidx.appsearch.app.AppSearchSession;+import androidx.appsearch.app.GenericDocument;+import androidx.appsearch.usagereporting.ActionConstants;+import androidx.core.util.Preconditions;++/**+ * Abstract wrapper class for {@link GenericDocument} of all types of taken actions, which contains+ * common getters and constants.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public abstract class TakenActionGenericDocument extends GenericDocument {+ protected static final String PROPERTY_PATH_ACTION_TYPE = "actionType";++ /**+ * Static factory method to create a concrete object of {@link TakenActionGenericDocument} child+ * type, according to the given {@link GenericDocument}'s action type.+ *+ * @param document a generic document object.+ *+ * @throws IllegalArgumentException if the integer value of property {@code actionType} is+ * invalid.+ */+ @NonNull+ public static TakenActionGenericDocument create(@NonNull GenericDocument document)+ throws IllegalArgumentException {+ Preconditions.checkNotNull(document);+ int actionType = (int) document.getPropertyLong(PROPERTY_PATH_ACTION_TYPE);+ switch (actionType) {+ case ActionConstants.ACTION_TYPE_SEARCH:+ return new SearchActionGenericDocument.Builder(document).build();+ case ActionConstants.ACTION_TYPE_CLICK:+ return new ClickActionGenericDocument.Builder(document).build();+ default:+ throw new IllegalArgumentException(+ "Cannot create taken action generic document with unknown action type");+ }+ }++ protected TakenActionGenericDocument(@NonNull GenericDocument document) {+ super(Preconditions.checkNotNull(document));+ }++ /** Returns the (enum) integer value of property {@code actionType}. */+ public int getActionType() {+ return (int) getPropertyLong(PROPERTY_PATH_ACTION_TYPE);+ }++ /** Abstract builder for {@link TakenActionGenericDocument}. */+ abstract static class Builder> extends GenericDocument.Builder {+ /**+ * Creates a new {@link TakenActionGenericDocument.Builder}.+ *+ *
Document IDs are unique within a namespace.
+ *+ *
The number of namespaces per app should be kept small for efficiency reasons.
+ *+ * @param namespace the namespace to set for the {@link GenericDocument}.+ * @param id the unique identifier for the {@link GenericDocument} in its namespace.+ * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The+ * provided {@code schemaType} must be defined using+ * {@link AppSearchSession#setSchemaAsync} prior+ * to inserting a document of this {@code schemaType} into the+ * AppSearch index using+ * {@link AppSearchSession#putAsync}.+ * Otherwise, the document will be rejected by+ * {@link AppSearchSession#putAsync} with result code+ * {@link AppSearchResult#RESULT_NOT_FOUND}.+ * @param actionType the action type of the taken action. See definitions in+ * {@link ActionConstants}.+ */+ Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType,+ int actionType) {+ super(Preconditions.checkNotNull(namespace), Preconditions.checkNotNull(id),+ Preconditions.checkNotNull(schemaType));++ setPropertyLong(PROPERTY_PATH_ACTION_TYPE, actionType);+ }++ /**+ * Creates a new {@link TakenActionGenericDocument.Builder} from an existing+ * {@link GenericDocument}.+ */+ Builder(@NonNull GenericDocument document) {+ super(Preconditions.checkNotNull(document));+ }+ }+}
@@ -44,10 +44,19 @@
return mCallingPackageName; }+ /** Returns whether the caller should have default access to data in its own package. */+ public boolean doesCallerHaveSelfAccess() {+ return true;+ }+ @Override public boolean equals(@Nullable Object o) {- if (this == o) return true;- if (!(o instanceof CallerAccess)) return false;+ if (this == o) {+ return true;+ }+ if (!(o instanceof CallerAccess)) {+ return false;+ } CallerAccess that = (CallerAccess) o; return mCallingPackageName.equals(that.mCallingPackageName); }
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo;+import androidx.appsearch.annotation.CanIgnoreReturnValue; import androidx.appsearch.app.AppSearchSchema; import androidx.appsearch.app.GenericDocument; import androidx.appsearch.app.PackageIdentifier;@@ -143,6 +144,7 @@
} /** Sets whether this schema has opted out of platform surfacing. */+ @CanIgnoreReturnValue @NonNull Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) { return setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY,@@ -150,6 +152,7 @@
} /** Add {@link PackageIdentifier} of packages which has access to this schema. */+ @CanIgnoreReturnValue @NonNull Builder addVisibleToPackages(@NonNull Set packageIdentifiers) { Preconditions.checkNotNull(packageIdentifiers);@@ -158,6 +161,7 @@
} /** Add {@link PackageIdentifier} of packages which has access to this schema. */+ @CanIgnoreReturnValue @NonNull Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) { Preconditions.checkNotNull(packageIdentifier);@@ -167,6 +171,7 @@
/** Add a set of Android role that has access to the schema this * {@link VisibilityDocumentV1} represents. */+ @CanIgnoreReturnValue @NonNull Builder setVisibleToRoles(@NonNull Set visibleToRoles) { Preconditions.checkNotNull(visibleToRoles);@@ -176,6 +181,7 @@
/** Add a set of Android role that has access to the schema this * {@link VisibilityDocumentV1} represents. */+ @CanIgnoreReturnValue @NonNull Builder setVisibleToPermissions(@NonNull Set visibleToPermissions) { Preconditions.checkNotNull(visibleToPermissions);
@@ -27,11 +27,15 @@
import androidx.appsearch.app.GenericDocument; import androidx.appsearch.app.GetSchemaResponse; import androidx.appsearch.app.InternalSetSchemaResponse;-import androidx.appsearch.app.VisibilityDocument;-import androidx.appsearch.app.VisibilityPermissionDocument;+import androidx.appsearch.app.InternalVisibilityConfig;+import androidx.appsearch.app.VisibilityPermissionConfig;+import androidx.appsearch.checker.initialization.qual.UnderInitialization;+import androidx.appsearch.checker.initialization.qual.UnknownInitialization;+import androidx.appsearch.checker.nullness.qual.RequiresNonNull; import androidx.appsearch.exceptions.AppSearchException; import androidx.appsearch.localstorage.AppSearchImpl; import androidx.appsearch.localstorage.util.PrefixUtil;+import androidx.appsearch.util.LogUtil; import androidx.collection.ArrayMap; import androidx.core.util.Preconditions;@@ -47,10 +51,11 @@
* Stores all visibility settings for all databases that AppSearchImpl knows about. * Persists the visibility settings and reloads them on initialization. *- *
The VisibilityStore creates a {@link VisibilityDocument} for each schema. This document holds
- * the visibility settings that apply to that schema. The VisibilityStore also creates a- * schema for these documents and has its own package and database so that its data doesn't- * interfere with any clients' data. It persists the document and schema through AppSearchImpl.+ *
The VisibilityStore creates a {@link InternalVisibilityConfig} for each schema. This config
+ * holds the visibility settings that apply to that schema. The VisibilityStore also creates a+ * schema and documents for these {@link InternalVisibilityConfig} and has its own+ * package and database so that its data doesn't interfere with any clients' data. It persists+ * the document and schema through AppSearchImpl. * *
These visibility settings won't be used in AppSearch Jetpack, we only store them for clients
* to look up.@@ -67,12 +72,13 @@
public static final String VISIBILITY_PACKAGE_NAME = "VS#Pkg"; public static final String VISIBILITY_DATABASE_NAME = "VS#Db";+ public static final String ANDROID_V_OVERLAY_DATABASE_NAME = "VS#AndroidVDb"; /**- * Map of PrefixedSchemaType and VisibilityDocument stores visibility information for each+ * Map of PrefixedSchemaType to InternalVisibilityConfig stores visibility information for each * schema type. */- private final Map mVisibilityDocumentMap = new ArrayMap<>();+ private final Map mVisibilityConfigMap = new ArrayMap<>(); private final AppSearchImpl mAppSearchImpl;@@ -86,7 +92,7 @@
new CallerAccess(/*callingPackageName=*/VISIBILITY_PACKAGE_NAME)); List visibilityDocumentsV1s = null; switch (getSchemaResponse.getVersion()) {- case VisibilityDocument.SCHEMA_VERSION_DOC_PER_PACKAGE:+ case VisibilityToDocumentConverter.SCHEMA_VERSION_DOC_PER_PACKAGE: // TODO (b/202194495) add VisibilityDocument in version 0 back instead of using // GenericDocument. List visibilityDocumentsV0s =@@ -95,7 +101,7 @@
visibilityDocumentsV1s = VisibilityStoreMigrationHelperFromV0 .toVisibilityDocumentV1(visibilityDocumentsV0s); // fall through- case VisibilityDocument.SCHEMA_VERSION_DOC_PER_SCHEMA:+ case VisibilityToDocumentConverter.SCHEMA_VERSION_DOC_PER_SCHEMA: if (visibilityDocumentsV1s == null) { // We need to read VisibilityDocument in Version 1 from AppSearch instead of // taking from the above step.@@ -106,37 +112,12 @@
setLatestSchemaAndDocuments(VisibilityStoreMigrationHelperFromV1 .toVisibilityDocumentsV2(visibilityDocumentsV1s)); break;- case VisibilityDocument.SCHEMA_VERSION_LATEST:- Set existingVisibilitySchema = getSchemaResponse.getSchemas();- if (existingVisibilitySchema.contains(VisibilityDocument.SCHEMA)- && existingVisibilitySchema.contains(VisibilityPermissionDocument.SCHEMA)) {- // The latest Visibility schema is in AppSearch, we must find our schema type.- // Extract all stored Visibility Document into mVisibilityDocumentMap.- loadVisibilityDocumentMap();- } else {- // We must have a broken schema. Reset it to the latest version.- // Do NOT set forceOverride to be true here, see comment below.- InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(- VISIBILITY_PACKAGE_NAME,- VISIBILITY_DATABASE_NAME,- Arrays.asList(VisibilityDocument.SCHEMA,- VisibilityPermissionDocument.SCHEMA),- /*visibilityDocuments=*/ Collections.emptyList(),- /*forceOverride=*/ false,- /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST,- /*setSchemaStatsBuilder=*/ null);- if (!internalSetSchemaResponse.isSuccess()) {- // If you hit problem here it means you made a incompatible change in- // Visibility Schema without update the version number. You should bump- // the version number and create a VisibilityStoreMigrationHelper which- // can analyse the different between the old version and the new version- // to migration user's visibility settings.- throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,- "Fail to set the latest visibility schema to AppSearch. "- + "You may need to update the visibility schema version "- + "number.");- }- }+ case VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST:+ verifyOrSetLatestVisibilitySchema(getSchemaResponse);+ // Check the version for visibility overlay database.+ migrateVisibilityOverlayDatabase();+ // Now we have the latest schema, load visibility config map.+ loadVisibilityConfigMap(); break; default: // We must did something wrong.@@ -146,30 +127,64 @@
} /**- * Sets visibility settings for the given {@link VisibilityDocument}s. Any previous- * {@link VisibilityDocument}s with same prefixed schema type will be overwritten.+ * Sets visibility settings for the given {@link InternalVisibilityConfig}s. Any previous+ * {@link InternalVisibilityConfig}s with same prefixed schema type will be overwritten. *- * @param prefixedVisibilityDocuments List of prefixed {@link VisibilityDocument} which- * contains schema type's visibility information.+ * @param prefixedVisibilityConfigs List of prefixed {@link InternalVisibilityConfig}s which+ * contains schema type's visibility information. * @throws AppSearchException on AppSearchImpl error. */- public void setVisibility(@NonNull List prefixedVisibilityDocuments)+ public void setVisibility(@NonNull List prefixedVisibilityConfigs) throws AppSearchException {- Preconditions.checkNotNull(prefixedVisibilityDocuments);+ Preconditions.checkNotNull(prefixedVisibilityConfigs); // Save new setting.- for (int i = 0; i < prefixedVisibilityDocuments.size(); i++) {- // put VisibilityDocument to AppSearchImpl and mVisibilityDocumentMap. If there is a- // VisibilityDocument with same prefixed schema exists, it will be replaced by new- // VisibilityDocument in both AppSearch and memory look up map.- VisibilityDocument prefixedVisibilityDocument = prefixedVisibilityDocuments.get(i);+ for (int i = 0; i < prefixedVisibilityConfigs.size(); i++) {+ // put VisibilityConfig to AppSearchImpl and mVisibilityConfigMap. If there is a+ // VisibilityConfig with same prefixed schema exists, it will be replaced by new+ // VisibilityConfig in both AppSearch and memory look up map.+ InternalVisibilityConfig prefixedVisibilityConfig = prefixedVisibilityConfigs.get(i);+ InternalVisibilityConfig oldVisibilityConfig =+ mVisibilityConfigMap.get(prefixedVisibilityConfig.getSchemaType()); mAppSearchImpl.putDocument( VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,- prefixedVisibilityDocument.toGenericDocument(),+ VisibilityToDocumentConverter.createVisibilityDocument(+ prefixedVisibilityConfig), /*sendChangeNotifications=*/ false, /*logger=*/ null);- mVisibilityDocumentMap.put(prefixedVisibilityDocument.getId(),- prefixedVisibilityDocument);++ // Put the android V visibility overlay document to AppSearchImpl.+ GenericDocument androidVOverlay =+ VisibilityToDocumentConverter.createAndroidVOverlay(prefixedVisibilityConfig);+ if (androidVOverlay != null) {+ mAppSearchImpl.putDocument(+ VISIBILITY_PACKAGE_NAME,+ ANDROID_V_OVERLAY_DATABASE_NAME,+ androidVOverlay,+ /*sendChangeNotifications=*/ false,+ /*logger=*/ null);+ } else if (isConfigContainsAndroidVOverlay(oldVisibilityConfig)) {+ // We need to make sure to remove the VisibilityOverlay on disk as the current+ // VisibilityConfig does not have a VisibilityOverlay.+ // For performance improvement, we should only make the remove call if the old+ // VisibilityConfig contains the overlay settings.+ try {+ mAppSearchImpl.remove(VISIBILITY_PACKAGE_NAME,+ ANDROID_V_OVERLAY_DATABASE_NAME,+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,+ prefixedVisibilityConfig.getSchemaType(),+ /*removeStatsBuilder=*/null);+ } catch (AppSearchException e) {+ // If it already doesn't exist, that is fine+ if (e.getResultCode() != RESULT_NOT_FOUND) {+ throw e;+ }+ }+ }++ // Put the VisibilityConfig to memory look up map.+ mVisibilityConfigMap.put(prefixedVisibilityConfig.getSchemaType(),+ prefixedVisibilityConfig); } // Now that the visibility document has been written. Persist the newly written data. mAppSearchImpl.persistToDisk(PersistType.Code.LITE);@@ -182,12 +197,13 @@
public void removeVisibility(@NonNull Set prefixedSchemaTypes) throws AppSearchException { for (String prefixedSchemaType : prefixedSchemaTypes) {- if (mVisibilityDocumentMap.remove(prefixedSchemaType) != null) {+ if (mVisibilityConfigMap.remove(prefixedSchemaType) != null) { // The deleted schema is not all-default setting, we need to remove its // VisibilityDocument from Icing. try { mAppSearchImpl.remove(VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,- VisibilityDocument.NAMESPACE, prefixedSchemaType,+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,+ prefixedSchemaType, /*removeStatsBuilder=*/null); } catch (AppSearchException e) { if (e.getResultCode() == RESULT_NOT_FOUND) {@@ -195,25 +211,45 @@
// to be fine if we cannot find it. Log.e(TAG, "Cannot find visibility document for " + prefixedSchemaType + " to remove.");- return;+ } else {+ throw e; }- throw e;+ }++ try {+ mAppSearchImpl.remove(VISIBILITY_PACKAGE_NAME,+ ANDROID_V_OVERLAY_DATABASE_NAME,+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,+ prefixedSchemaType,+ /*removeStatsBuilder=*/null);+ } catch (AppSearchException e) {+ if (e.getResultCode() == RESULT_NOT_FOUND) {+ // It's possible no overlay was set, so this this is fine.+ if (LogUtil.DEBUG) {+ Log.d(TAG, "Cannot find Android V overlay document for "+ + prefixedSchemaType + " to remove.");+ }+ } else {+ throw e;+ } } } } }- /** Gets the {@link VisibilityDocument} for the given prefixed schema type. */+ /** Gets the {@link InternalVisibilityConfig} for the given prefixed schema type. */ @Nullable- public VisibilityDocument getVisibility(@NonNull String prefixedSchemaType) {- return mVisibilityDocumentMap.get(prefixedSchemaType);+ public InternalVisibilityConfig getVisibility(@NonNull String prefixedSchemaType) {+ return mVisibilityConfigMap.get(prefixedSchemaType); } /**- * Loads all stored latest {@link VisibilityDocument} from Icing, and put them into- * {@link #mVisibilityDocumentMap}.+ * Loads all stored latest {@link InternalVisibilityConfig} from Icing, and put them into+ * {@link #mVisibilityConfigMap}. */- private void loadVisibilityDocumentMap() throws AppSearchException {+ @RequiresNonNull("mAppSearchImpl")+ private void loadVisibilityConfigMap(@UnderInitialization VisibilityStore this)+ throws AppSearchException { // Populate visibility settings set List cachedSchemaTypes = mAppSearchImpl.getAllPrefixedSchemaTypes(); for (int i = 0; i < cachedSchemaTypes.size(); i++) {@@ -223,16 +259,16 @@
continue; // Our own package. Skip. }- VisibilityDocument visibilityDocument;+ GenericDocument visibilityDocument;+ GenericDocument visibilityAndroidVOverlay = null; try { // Note: We use the other clients' prefixed schema type as ids- visibilityDocument = new VisibilityDocument.Builder(- mAppSearchImpl.getDocument(- VISIBILITY_PACKAGE_NAME,- VISIBILITY_DATABASE_NAME,- VisibilityDocument.NAMESPACE,- /*id=*/ prefixedSchemaType,- /*typePropertyPaths=*/ Collections.emptyMap())).build();+ visibilityDocument = mAppSearchImpl.getDocument(+ VISIBILITY_PACKAGE_NAME,+ VISIBILITY_DATABASE_NAME,+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,+ /*id=*/ prefixedSchemaType,+ /*typePropertyPaths=*/ Collections.emptyMap()); } catch (AppSearchException e) { if (e.getResultCode() == RESULT_NOT_FOUND) { // The schema has all default setting and we won't have a VisibilityDocument for@@ -242,25 +278,48 @@
// Otherwise, this is some other error we should pass up. throw e; }- mVisibilityDocumentMap.put(prefixedSchemaType, visibilityDocument);++ try {+ visibilityAndroidVOverlay = mAppSearchImpl.getDocument(+ VISIBILITY_PACKAGE_NAME,+ ANDROID_V_OVERLAY_DATABASE_NAME,+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,+ /*id=*/ prefixedSchemaType,+ /*typePropertyPaths=*/ Collections.emptyMap());+ } catch (AppSearchException e) {+ if (e.getResultCode() != RESULT_NOT_FOUND) {+ // This is some other error we should pass up.+ throw e;+ }+ // Otherwise we continue inserting into visibility document map as the overlay+ // map can be null+ }++ mVisibilityConfigMap.put(+ prefixedSchemaType,+ VisibilityToDocumentConverter.createInternalVisibilityConfig(+ visibilityDocument, visibilityAndroidVOverlay)); } } /**- * Set the latest version of {@link VisibilityDocument} and its schema to AppSearch.+ * Set the latest version of {@link InternalVisibilityConfig} and its schema to AppSearch. */- private void setLatestSchemaAndDocuments(@NonNull List migratedDocuments)+ @RequiresNonNull("mAppSearchImpl")+ private void setLatestSchemaAndDocuments(+ @UnderInitialization VisibilityStore this,+ @NonNull List migratedDocuments) throws AppSearchException { // The latest schema type doesn't exist yet. Add it. Set forceOverride true to // delete old schema. InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema( VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,- Arrays.asList(VisibilityDocument.SCHEMA,- VisibilityPermissionDocument.SCHEMA),- /*visibilityDocuments=*/ Collections.emptyList(),+ Arrays.asList(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,+ VisibilityPermissionConfig.SCHEMA),+ /*visibilityConfigs=*/ Collections.emptyList(), /*forceOverride=*/ true,- /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST,+ /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST, /*setSchemaStatsBuilder=*/ null); if (!internalSetSchemaResponse.isSuccess()) { // Impossible case, we just set forceOverride to be true, we should never@@ -268,15 +327,185 @@
throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, internalSetSchemaResponse.getErrorMessage()); }+ InternalSetSchemaResponse internalSetAndroidVOverlaySchemaResponse =+ mAppSearchImpl.setSchema(+ VISIBILITY_PACKAGE_NAME,+ ANDROID_V_OVERLAY_DATABASE_NAME,+ Collections.singletonList(+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA),+ /*visibilityConfigs=*/ Collections.emptyList(),+ /*forceOverride=*/ true,+ /*version=*/ VisibilityToDocumentConverter+ .ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,+ /*setSchemaStatsBuilder=*/ null);+ if (!internalSetAndroidVOverlaySchemaResponse.isSuccess()) {+ // Impossible case, we just set forceOverride to be true, we should never+ // fail in incompatible changes.+ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,+ internalSetAndroidVOverlaySchemaResponse.getErrorMessage());+ } for (int i = 0; i < migratedDocuments.size(); i++) {- VisibilityDocument migratedDocument = migratedDocuments.get(i);- mVisibilityDocumentMap.put(migratedDocument.getId(), migratedDocument);+ InternalVisibilityConfig migratedConfig = migratedDocuments.get(i);+ mVisibilityConfigMap.put(migratedConfig.getSchemaType(), migratedConfig); mAppSearchImpl.putDocument( VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,- migratedDocument.toGenericDocument(),+ VisibilityToDocumentConverter.createVisibilityDocument(migratedConfig), /*sendChangeNotifications=*/ false, /*logger=*/ null); } }++ /**+ * Check and migrate visibility schemas in {@link #ANDROID_V_OVERLAY_DATABASE_NAME} to+ * {@link VisibilityToDocumentConverter#ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST}.+ */+ @RequiresNonNull("mAppSearchImpl")+ private void migrateVisibilityOverlayDatabase(@UnderInitialization VisibilityStore this)+ throws AppSearchException {+ GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(+ VISIBILITY_PACKAGE_NAME,+ ANDROID_V_OVERLAY_DATABASE_NAME,+ new CallerAccess(/*callingPackageName=*/VISIBILITY_PACKAGE_NAME));+ switch (getSchemaResponse.getVersion()) {+ case VisibilityToDocumentConverter.OVERLAY_SCHEMA_VERSION_PUBLIC_ACL_VISIBLE_TO_CONFIG:+ // Force override to next version. This version hasn't released to any public+ // version. There shouldn't have any public device in this state, so we don't+ // actually need to migrate any document.+ InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(+ VISIBILITY_PACKAGE_NAME,+ ANDROID_V_OVERLAY_DATABASE_NAME,+ Collections.singletonList(+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA),+ /*visibilityConfigs=*/ Collections.emptyList(),+ /*forceOverride=*/ true, // force update to nest version.+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,+ /*setSchemaStatsBuilder=*/ null);+ if (!internalSetSchemaResponse.isSuccess()) {+ // Impossible case, we just set forceOverride to be true, we should never+ // fail in incompatible changes.+ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,+ internalSetSchemaResponse.getErrorMessage());+ }+ break;+ case VisibilityToDocumentConverter.OVERLAY_SCHEMA_VERSION_ALL_IN_PROTO:+ verifyOrSetLatestVisibilityOverlaySchema(getSchemaResponse);+ break;+ default:+ // We must did something wrong.+ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,+ "Found unsupported visibility version: " + getSchemaResponse.getVersion());+ }+ }++ /**+ * Verify the existing visibility schema, set the latest visibilility schema if it's missing.+ */+ @RequiresNonNull("mAppSearchImpl")+ private void verifyOrSetLatestVisibilitySchema(+ @UnderInitialization VisibilityStore this, @NonNull GetSchemaResponse getSchemaResponse)+ throws AppSearchException {+ // We cannot change the schema version past 2 as detecting version "3" would hit the+ // default block and throw an AppSearchException. This is why we added+ // VisibilityOverlay.++ // Check Visibility schema first.+ Set existingVisibilitySchema = getSchemaResponse.getSchemas();+ // Force to override visibility schema if it contains DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA.+ // The DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA was added to VISIBILITY_DATABASE_NAME and+ // removed to ANDROID_V_OVERLAY_DATABASE_NAME. We need to force update the schema to+ // migrate devices that have already store public acl schema.+ // TODO(b/321326441) remove this method when we no longer to migrate devices in this state.+ if (existingVisibilitySchema.contains(+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA)+ && existingVisibilitySchema.contains(VisibilityPermissionConfig.SCHEMA)+ && existingVisibilitySchema.contains(+ VisibilityToDocumentConverter.DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA)) {+ InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(+ VISIBILITY_PACKAGE_NAME,+ VISIBILITY_DATABASE_NAME,+ Arrays.asList(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,+ VisibilityPermissionConfig.SCHEMA),+ /*visibilityConfigs=*/ Collections.emptyList(),+ /*forceOverride=*/ true,+ /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,+ /*setSchemaStatsBuilder=*/ null);+ if (!internalSetSchemaResponse.isSuccess()) {+ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,+ "Fail to force override deprecated visibility schema with public acl.");+ }+ } else if (!(existingVisibilitySchema.contains(+ VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA)+ && existingVisibilitySchema.contains(VisibilityPermissionConfig.SCHEMA))) {+ // We must have a broken schema. Reset it to the latest version.+ // Do NOT set forceOverride to be true here, see comment below.+ InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(+ VISIBILITY_PACKAGE_NAME,+ VISIBILITY_DATABASE_NAME,+ Arrays.asList(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,+ VisibilityPermissionConfig.SCHEMA),+ /*visibilityConfigs=*/ Collections.emptyList(),+ /*forceOverride=*/ false,+ /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,+ /*setSchemaStatsBuilder=*/ null);+ if (!internalSetSchemaResponse.isSuccess()) {+ // If you hit problem here it means you made a incompatible change in+ // Visibility Schema without update the version number. You should bump+ // the version number and create a VisibilityStoreMigrationHelper which+ // can analyse the different between the old version and the new version+ // to migration user's visibility settings.+ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,+ "Fail to set the latest visibility schema to AppSearch. "+ + "You may need to update the visibility schema version "+ + "number.");+ }+ }+ }++ /**+ * Verify the existing visibility overlay schema, set the latest overlay schema if it's missing.+ */+ @RequiresNonNull("mAppSearchImpl")+ private void verifyOrSetLatestVisibilityOverlaySchema(+ @UnknownInitialization VisibilityStore this,+ @NonNull GetSchemaResponse getAndroidVOverlaySchemaResponse)+ throws AppSearchException {+ // Check Android V overlay schema.+ Set existingAndroidVOverlaySchema =+ getAndroidVOverlaySchemaResponse.getSchemas();+ if (!existingAndroidVOverlaySchema.contains(+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA)) {+ // We must have a broken schema. Reset it to the latest version.+ // Do NOT set forceOverride to be true here, see comment below.+ InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(+ VISIBILITY_PACKAGE_NAME,+ ANDROID_V_OVERLAY_DATABASE_NAME,+ Collections.singletonList(+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA),+ /*visibilityConfigs=*/ Collections.emptyList(),+ /*forceOverride=*/ false,+ VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,+ /*setSchemaStatsBuilder=*/ null);+ if (!internalSetSchemaResponse.isSuccess()) {+ // If you hit problem here it means you made a incompatible change in+ // Visibility Schema. You should create new overlay schema+ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,+ "Fail to set the overlay visibility schema to AppSearch. "+ + "You may need to create new overlay schema.");+ }+ }+ }++ /**+ * Whether the given {@link InternalVisibilityConfig} contains Android V overlay settings.+ *+ *
Android V overlay {@link VisibilityToDocumentConverter#ANDROID_V_OVERLAY_SCHEMA}
+ * contains public acl and visible to config.+ */+ private static boolean isConfigContainsAndroidVOverlay(+ @Nullable InternalVisibilityConfig config) {+ return config != null+ && (config.getVisibilityConfig().getPubliclyVisibleTargetPackage() != null+ || !config.getVisibleToConfigs().isEmpty());+ } }
@@ -0,0 +1,450 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.localstorage.visibilitystore;++import android.util.Log;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RestrictTo;+import androidx.appsearch.app.AppSearchSchema;+import androidx.appsearch.app.GenericDocument;+import androidx.appsearch.app.InternalVisibilityConfig;+import androidx.appsearch.app.PackageIdentifier;+import androidx.appsearch.app.SchemaVisibilityConfig;+import androidx.appsearch.app.VisibilityPermissionConfig;+import androidx.collection.ArraySet;++import com.google.android.appsearch.proto.AndroidVOverlayProto;+import com.google.android.appsearch.proto.PackageIdentifierProto;+import com.google.android.appsearch.proto.VisibilityConfigProto;+import com.google.android.appsearch.proto.VisibleToPermissionProto;+import com.google.android.icing.protobuf.ByteString;+import com.google.android.icing.protobuf.InvalidProtocolBufferException;++import java.util.List;+import java.util.Objects;+import java.util.Set;++/**+ * Utilities for working with {@link VisibilityChecker} and {@link VisibilityStore}.+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public class VisibilityToDocumentConverter {+ private static final String TAG = "AppSearchVisibilityToDo";++ /**+ * The Schema type for documents that hold AppSearch's metadata, such as visibility settings.+ */+ public static final String VISIBILITY_DOCUMENT_SCHEMA_TYPE = "VisibilityType";+ /** Namespace of documents that contain visibility settings */+ public static final String VISIBILITY_DOCUMENT_NAMESPACE = "";++ /**+ * The Schema type for the Android V visibility setting overlay documents, that allow for+ * additional visibility settings.+ */+ public static final String ANDROID_V_OVERLAY_SCHEMA_TYPE = "AndroidVOverlayType";+ /** Namespace of documents that contain Android V visibility setting overlay documents */+ public static final String ANDROID_V_OVERLAY_NAMESPACE = "androidVOverlay";+ /** Property that holds the serialized {@link AndroidVOverlayProto}. */+ public static final String VISIBILITY_PROTO_SERIALIZE_PROPERTY =+ "visibilityProtoSerializeProperty";++ /**+ * Property that holds the list of platform-hidden schemas, as part of the visibility settings.+ */+ private static final String NOT_DISPLAYED_BY_SYSTEM_PROPERTY = "notPlatformSurfaceable";++ /** Property that holds the package name that can access a schema. */+ private static final String VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY = "packageName";++ /** Property that holds the SHA 256 certificate of the app that can access a schema. */+ private static final String VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY = "sha256Cert";++ /** Property that holds the required permissions to access the schema. */+ private static final String PERMISSION_PROPERTY = "permission";++ // The initial schema version, one VisibilityConfig contains all visibility information for+ // whole package.+ public static final int SCHEMA_VERSION_DOC_PER_PACKAGE = 0;++ // One VisibilityConfig contains visibility information for a single schema.+ public static final int SCHEMA_VERSION_DOC_PER_SCHEMA = 1;++ // One VisibilityConfig contains visibility information for a single schema. The permission+ // visibility information is stored in a document property VisibilityPermissionConfig of the+ // outer doc.+ public static final int SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA = 2;++ public static final int SCHEMA_VERSION_LATEST = SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA;++ // The initial schema version, the overlay schema contains public acl and visible to config+ // properties.+ public static final int OVERLAY_SCHEMA_VERSION_PUBLIC_ACL_VISIBLE_TO_CONFIG = 0;++ // The overlay schema only contains a proto property contains all visibility setting.+ public static final int OVERLAY_SCHEMA_VERSION_ALL_IN_PROTO = 1;++ // The version number of schema saved in Android V overlay database.+ public static final int ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST =+ OVERLAY_SCHEMA_VERSION_ALL_IN_PROTO;++ /**+ * Schema for the VisibilityStore's documents.+ *+ *
NOTE: If you update this, also update {@link #SCHEMA_VERSION_LATEST}.
+ */+ public static final AppSearchSchema VISIBILITY_DOCUMENT_SCHEMA =+ new AppSearchSchema.Builder(VISIBILITY_DOCUMENT_SCHEMA_TYPE)+ .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(+ NOT_DISPLAYED_BY_SYSTEM_PROPERTY)+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)+ .build())+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(+ VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY)+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)+ .build())+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(+ VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY)+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)+ .build())+ .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(+ PERMISSION_PROPERTY, VisibilityPermissionConfig.SCHEMA_TYPE)+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)+ .build())+ .build();++ /** Schema for the VisibilityStore's Android V visibility setting overlay. */+ public static final AppSearchSchema ANDROID_V_OVERLAY_SCHEMA =+ new AppSearchSchema.Builder(ANDROID_V_OVERLAY_SCHEMA_TYPE)+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(+ VISIBILITY_PROTO_SERIALIZE_PROPERTY)+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)+ .build())+ .build();+ /**+ * The Deprecated schemas and properties that we need to remove from visibility database.+ * TODO(b/321326441) remove this method when we no longer to migrate devices in this state.+ */+ static final AppSearchSchema DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA =+ new AppSearchSchema.Builder("PublicAclOverlayType")+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(+ "publiclyVisibleTargetPackage")+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)+ .build())+ .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(+ "publiclyVisibleTargetPackageSha256Cert")+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)+ .build())+ .build();++ /**+ * Constructs a {@link InternalVisibilityConfig} from two {@link GenericDocument}s.+ *+ *
This constructor is still needed until we don't treat Visibility related documents as
+ * {@link GenericDocument}s internally.+ *+ * @param visibilityDocument a {@link GenericDocument} holding visibility properties+ * in {@link #VISIBILITY_DOCUMENT_SCHEMA}+ * @param androidVOverlayDocument a {@link GenericDocument} holding visibility properties+ * in {@link #ANDROID_V_OVERLAY_SCHEMA}+ */+ @NonNull+ public static InternalVisibilityConfig createInternalVisibilityConfig(+ @NonNull GenericDocument visibilityDocument,+ @Nullable GenericDocument androidVOverlayDocument) {+ Objects.requireNonNull(visibilityDocument);++ // Parse visibility proto if required+ AndroidVOverlayProto androidVOverlayProto = null;+ if (androidVOverlayDocument != null) {+ try {+ byte[] androidVOverlayProtoBytes = androidVOverlayDocument.getPropertyBytes(+ VISIBILITY_PROTO_SERIALIZE_PROPERTY);+ if (androidVOverlayProtoBytes != null) {+ androidVOverlayProto = AndroidVOverlayProto.parseFrom(+ androidVOverlayProtoBytes);+ }+ } catch (InvalidProtocolBufferException e) {+ Log.e(TAG, "Get an invalid android V visibility overlay proto.", e);+ }+ }++ // Handle all visibility settings other than visibleToConfigs+ SchemaVisibilityConfig schemaVisibilityConfig = createVisibilityConfig(+ visibilityDocument, androidVOverlayProto);++ // Handle visibleToConfigs+ String schemaType = visibilityDocument.getId();+ InternalVisibilityConfig.Builder builder = new InternalVisibilityConfig.Builder(schemaType)+ .setNotDisplayedBySystem(visibilityDocument+ .getPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY))+ .setVisibilityConfig(schemaVisibilityConfig);+ if (androidVOverlayProto != null) {+ List visibleToConfigProtoList =+ androidVOverlayProto.getVisibleToConfigsList();+ for (int i = 0; i < visibleToConfigProtoList.size(); i++) {+ SchemaVisibilityConfig visibleToConfig =+ convertVisibilityConfigFromProto(visibleToConfigProtoList.get(i));+ builder.addVisibleToConfig(visibleToConfig);+ }+ }++ return builder.build();+ }++ /**+ * Constructs a {@link SchemaVisibilityConfig} from a {@link GenericDocument} containing legacy+ * visibility settings, and an {@link AndroidVOverlayProto} containing extended visibility+ * settings.+ *+ *
This constructor is still needed until we don't treat Visibility related documents as
+ * {@link GenericDocument}s internally.+ *+ * @param visibilityDocument a {@link GenericDocument} holding all visibility properties+ * other than publiclyVisibleTargetPackage.+ * @param androidVOverlayProto the proto containing post-V visibility settings+ */+ @NonNull+ private static SchemaVisibilityConfig createVisibilityConfig(+ @NonNull GenericDocument visibilityDocument,+ @Nullable AndroidVOverlayProto androidVOverlayProto) {+ Objects.requireNonNull(visibilityDocument);++ // Pre-V visibility settings come from visibilityDocument+ SchemaVisibilityConfig.Builder builder = new SchemaVisibilityConfig.Builder();++ String[] visibleToPackageNames =+ visibilityDocument.getPropertyStringArray(VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY);+ byte[][] visibleToPackageShaCerts =+ visibilityDocument.getPropertyBytesArray(VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY);+ if (visibleToPackageNames != null && visibleToPackageShaCerts != null) {+ for (int i = 0; i < visibleToPackageNames.length; i++) {+ builder.addAllowedPackage(+ new PackageIdentifier(+ visibleToPackageNames[i], visibleToPackageShaCerts[i]));+ }+ }++ GenericDocument[] visibleToPermissionDocs =+ visibilityDocument.getPropertyDocumentArray(PERMISSION_PROPERTY);+ if (visibleToPermissionDocs != null) {+ for (int i = 0; i < visibleToPermissionDocs.length; ++i) {+ long[] visibleToPermissionLongs = visibleToPermissionDocs[i].getPropertyLongArray(+ VisibilityPermissionConfig.ALL_REQUIRED_PERMISSIONS_PROPERTY);+ if (visibleToPermissionLongs != null) {+ Set allRequiredPermissions =+ new ArraySet<>(visibleToPermissionLongs.length);+ for (int j = 0; j < visibleToPermissionLongs.length; j++) {+ allRequiredPermissions.add((int) visibleToPermissionLongs[j]);+ }+ builder.addRequiredPermissions(allRequiredPermissions);+ }+ }+ }++ // Post-V visibility settings come from androidVOverlayProto+ if (androidVOverlayProto != null) {+ SchemaVisibilityConfig androidVOverlayConfig =+ convertVisibilityConfigFromProto(+ androidVOverlayProto.getVisibilityConfig());+ builder.setPubliclyVisibleTargetPackage(+ androidVOverlayConfig.getPubliclyVisibleTargetPackage());+ }++ return builder.build();+ }++ @NonNull+ private static SchemaVisibilityConfig convertVisibilityConfigFromProto(+ @NonNull VisibilityConfigProto proto) {+ SchemaVisibilityConfig.Builder builder = new SchemaVisibilityConfig.Builder();++ List visibleToPackageProtoList = proto.getVisibleToPackagesList();+ for (int i = 0; i < visibleToPackageProtoList.size(); i++) {+ PackageIdentifierProto visibleToPackage = proto.getVisibleToPackages(i);+ builder.addAllowedPackage(convertPackageIdentifierFromProto(visibleToPackage));+ }++ List visibleToPermissionProtoList =+ proto.getVisibleToPermissionsList();+ for (int i = 0; i < visibleToPermissionProtoList.size(); i++) {+ VisibleToPermissionProto visibleToPermissionProto = visibleToPermissionProtoList.get(i);+ Set visibleToPermissions =+ new ArraySet<>(visibleToPermissionProto.getPermissionsList());+ builder.addRequiredPermissions(visibleToPermissions);+ }++ if (proto.hasPubliclyVisibleTargetPackage()) {+ PackageIdentifierProto publiclyVisibleTargetPackage =+ proto.getPubliclyVisibleTargetPackage();+ builder.setPubliclyVisibleTargetPackage(+ convertPackageIdentifierFromProto(publiclyVisibleTargetPackage));+ }++ return builder.build();+ }++ private static VisibilityConfigProto convertSchemaVisibilityConfigToProto(+ @NonNull SchemaVisibilityConfig schemaVisibilityConfig) {+ VisibilityConfigProto.Builder builder = VisibilityConfigProto.newBuilder();++ List visibleToPackages = schemaVisibilityConfig.getAllowedPackages();+ for (int i = 0; i < visibleToPackages.size(); i++) {+ PackageIdentifier visibleToPackage = visibleToPackages.get(i);+ builder.addVisibleToPackages(convertPackageIdentifierToProto(visibleToPackage));+ }++ Set> visibleToPermissions = schemaVisibilityConfig.getRequiredPermissions();+ if (!visibleToPermissions.isEmpty()) {+ for (Set allRequiredPermissions : visibleToPermissions) {+ builder.addVisibleToPermissions(+ VisibleToPermissionProto.newBuilder()+ .addAllPermissions(allRequiredPermissions));+ }+ }++ PackageIdentifier publicAclPackage =+ schemaVisibilityConfig.getPubliclyVisibleTargetPackage();+ if (publicAclPackage != null) {+ builder.setPubliclyVisibleTargetPackage(+ convertPackageIdentifierToProto(publicAclPackage));+ }++ return builder.build();+ }++ /**+ * Returns the {@link GenericDocument} for the visibility schema.+ *+ * @param config the configuration to populate into the document+ */+ @NonNull+ public static GenericDocument createVisibilityDocument(+ @NonNull InternalVisibilityConfig config) {+ GenericDocument.Builder> builder = new GenericDocument.Builder<>(+ VISIBILITY_DOCUMENT_NAMESPACE,+ config.getSchemaType(), // We are using the prefixedSchemaType to be the id+ VISIBILITY_DOCUMENT_SCHEMA_TYPE);+ builder.setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY,+ config.isNotDisplayedBySystem());+ SchemaVisibilityConfig schemaVisibilityConfig = config.getVisibilityConfig();+ List visibleToPackages = schemaVisibilityConfig.getAllowedPackages();+ String[] visibleToPackageNames = new String[visibleToPackages.size()];+ byte[][] visibleToPackageSha256Certs = new byte[visibleToPackages.size()][32];+ for (int i = 0; i < visibleToPackages.size(); i++) {+ visibleToPackageNames[i] = visibleToPackages.get(i).getPackageName();+ visibleToPackageSha256Certs[i] = visibleToPackages.get(i).getSha256Certificate();+ }+ builder.setPropertyString(VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY, visibleToPackageNames);+ builder.setPropertyBytes(VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY,+ visibleToPackageSha256Certs);++ // Generate an array of GenericDocument for VisibilityPermissionConfig.+ Set> visibleToPermissions = schemaVisibilityConfig.getRequiredPermissions();+ if (!visibleToPermissions.isEmpty()) {+ GenericDocument[] permissionGenericDocs =+ new GenericDocument[visibleToPermissions.size()];+ int i = 0;+ for (Set allRequiredPermissions : visibleToPermissions) {+ VisibilityPermissionConfig permissionDocument =+ new VisibilityPermissionConfig(allRequiredPermissions);+ permissionGenericDocs[i++] = permissionDocument.toGenericDocument();+ }+ builder.setPropertyDocument(PERMISSION_PROPERTY, permissionGenericDocs);+ }++ // The creationTimestamp doesn't matter for Visibility documents.+ // But to make tests pass, we set it 0 so two GenericDocuments generated from+ // the same VisibilityConfig can be same.+ builder.setCreationTimestampMillis(0L);++ return builder.build();+ }++ /**+ * Returns the {@link GenericDocument} for the Android V overlay schema if it is provided,+ * null otherwise.+ */+ @Nullable+ public static GenericDocument createAndroidVOverlay(+ @NonNull InternalVisibilityConfig internalVisibilityConfig) {+ PackageIdentifier publiclyVisibleTargetPackage =+ internalVisibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage();+ Set visibleToConfigs =+ internalVisibilityConfig.getVisibleToConfigs();+ if (publiclyVisibleTargetPackage == null && visibleToConfigs.isEmpty()) {+ // This config doesn't contains any Android V overlay settings+ return null;+ }++ VisibilityConfigProto.Builder visibilityConfigProtoBuilder =+ VisibilityConfigProto.newBuilder();+ // Set publicAcl+ if (publiclyVisibleTargetPackage != null) {+ visibilityConfigProtoBuilder.setPubliclyVisibleTargetPackage(+ convertPackageIdentifierToProto(publiclyVisibleTargetPackage));+ }++ // Set visibleToConfigs+ AndroidVOverlayProto.Builder androidVOverlayProtoBuilder =+ AndroidVOverlayProto.newBuilder().setVisibilityConfig(visibilityConfigProtoBuilder);+ if (!visibleToConfigs.isEmpty()) {+ for (SchemaVisibilityConfig visibleToConfig : visibleToConfigs) {+ VisibilityConfigProto visibleToConfigProto =+ convertSchemaVisibilityConfigToProto(visibleToConfig);+ androidVOverlayProtoBuilder.addVisibleToConfigs(visibleToConfigProto);+ }+ }++ GenericDocument.Builder> androidVOverlayBuilder = new GenericDocument.Builder<>(+ ANDROID_V_OVERLAY_NAMESPACE,+ internalVisibilityConfig.getSchemaType(),+ ANDROID_V_OVERLAY_SCHEMA_TYPE)+ .setPropertyBytes(+ VISIBILITY_PROTO_SERIALIZE_PROPERTY,+ androidVOverlayProtoBuilder.build().toByteArray());++ // The creationTimestamp doesn't matter for Visibility documents.+ // But to make tests pass, we set it 0 so two GenericDocuments generated from+ // the same VisibilityConfig can be same.+ androidVOverlayBuilder.setCreationTimestampMillis(0L);++ return androidVOverlayBuilder.build();+ }++ @NonNull+ private static PackageIdentifierProto convertPackageIdentifierToProto(+ @NonNull PackageIdentifier packageIdentifier) {+ return PackageIdentifierProto.newBuilder()+ .setPackageName(packageIdentifier.getPackageName())+ .setPackageSha256Cert(ByteString.copyFrom(packageIdentifier.getSha256Certificate()))+ .build();+ }++ @NonNull+ private static PackageIdentifier convertPackageIdentifierFromProto(+ @NonNull PackageIdentifierProto packageIdentifierProto) {+ return new PackageIdentifier(+ packageIdentifierProto.getPackageName(),+ packageIdentifierProto.getPackageSha256Cert().toByteArray());+ }++ private VisibilityToDocumentConverter() {}+}
@@ -54,8 +54,11 @@
Preconditions.checkNotNull(targetPackageName); Preconditions.checkNotNull(prefixedSchema);- if (callerAccess.getCallingPackageName().equals(targetPackageName)) {- return true; // Everyone is always allowed to retrieve their own data.+ // If the caller is allowed default access to its own data, check if the calling package+ // and the target package are the same.+ if (callerAccess.doesCallerHaveSelfAccess()+ && callerAccess.getCallingPackageName().equals(targetPackageName)) {+ return true; // Caller is allowed to retrieve its own data. } if (visibilityStore == null || visibilityChecker == null) { return false; // No visibility is configured at this time; no other access possible.
@@ -0,0 +1,118 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.platformstorage;++import androidx.annotation.NonNull;+import androidx.annotation.RequiresApi;+import androidx.annotation.RestrictTo;+import androidx.appsearch.app.AppSearchBatchResult;+import androidx.appsearch.app.EnterpriseGlobalSearchSession;+import androidx.appsearch.app.Features;+import androidx.appsearch.app.GenericDocument;+import androidx.appsearch.app.GetByDocumentIdRequest;+import androidx.appsearch.app.GetSchemaResponse;+import androidx.appsearch.app.SearchResults;+import androidx.appsearch.app.SearchSpec;+import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;+import androidx.appsearch.platformstorage.converter.GenericDocumentToPlatformConverter;+import androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter;+import androidx.appsearch.platformstorage.converter.RequestToPlatformConverter;+import androidx.appsearch.platformstorage.converter.SearchSpecToPlatformConverter;+import androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter;+import androidx.concurrent.futures.ResolvableFuture;+import androidx.core.os.BuildCompat;+import androidx.core.util.Preconditions;++import com.google.common.util.concurrent.ListenableFuture;++import java.util.concurrent.Executor;++/**+ * An implementation of {@link EnterpriseGlobalSearchSession} which proxies to a+ * platform {@link android.app.appsearch.EnterpriseGlobalSearchSession}.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+@RequiresApi(35)+// TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of+// BuildCompat.isAtLeastV() is removed.[email protected]+class EnterpriseGlobalSearchSessionImpl implements EnterpriseGlobalSearchSession {+ private final android.app.appsearch.EnterpriseGlobalSearchSession mPlatformSession;+ private final Executor mExecutor;+ private final Features mFeatures;++ EnterpriseGlobalSearchSessionImpl(+ @NonNull android.app.appsearch.EnterpriseGlobalSearchSession platformSession,+ @NonNull Executor executor,+ @NonNull Features features) {+ mPlatformSession = Preconditions.checkNotNull(platformSession);+ mExecutor = Preconditions.checkNotNull(executor);+ mFeatures = Preconditions.checkNotNull(features);+ }++ @NonNull+ @Override+ public ListenableFuture> getByDocumentIdAsync(+ @NonNull String packageName, @NonNull String databaseName,+ @NonNull GetByDocumentIdRequest request) {+ Preconditions.checkNotNull(packageName);+ Preconditions.checkNotNull(databaseName);+ Preconditions.checkNotNull(request);+ ResolvableFuture> future =+ ResolvableFuture.create();+ mPlatformSession.getByDocumentId(packageName, databaseName,+ RequestToPlatformConverter.toPlatformGetByDocumentIdRequest(request), mExecutor,+ new BatchResultCallbackAdapter<>(future,+ GenericDocumentToPlatformConverter::toJetpackGenericDocument));+ return future;+ }++ @Override+ @NonNull+ public SearchResults search(+ @NonNull String queryExpression,+ @NonNull SearchSpec searchSpec) {+ Preconditions.checkNotNull(queryExpression);+ Preconditions.checkNotNull(searchSpec);+ android.app.appsearch.SearchResults platformSearchResults =+ mPlatformSession.search(+ queryExpression,+ SearchSpecToPlatformConverter.toPlatformSearchSpec(searchSpec));+ return new SearchResultsImpl(platformSearchResults, searchSpec, mExecutor);+ }++ @NonNull+ @Override+ public ListenableFuture getSchemaAsync(@NonNull String packageName,+ @NonNull String databaseName) {+ Preconditions.checkNotNull(packageName);+ Preconditions.checkNotNull(databaseName);+ ResolvableFuture future = ResolvableFuture.create();+ mPlatformSession.getSchema(packageName, databaseName, mExecutor,+ result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(result,+ future, GetSchemaResponseToPlatformConverter::toJetpackGetSchemaResponse));+ return future;+ }++ @NonNull+ @Override+ public Features getFeatures() {+ return mFeatures;+ }+}
@@ -16,15 +16,12 @@
package androidx.appsearch.platformstorage; import android.content.Context;-import android.content.pm.ModuleInfo;-import android.content.pm.PackageInfo;-import android.content.pm.PackageManager; import android.os.Build;-import androidx.annotation.DoNotInline; import androidx.annotation.NonNull;-import androidx.annotation.RequiresApi; import androidx.appsearch.app.Features;+import androidx.appsearch.platformstorage.util.AppSearchVersionUtil;+import androidx.core.os.BuildCompat; import androidx.core.util.Preconditions; /**@@ -32,12 +29,6 @@
* level. */ final class FeaturesImpl implements Features {- private static final String APPSEARCH_MODULE_NAME = "com.android.appsearch";-- // This will be set to -1 to indicate the AppSearch version code hasn't bee checked, then to- // 0 if it is not found, or the version code if it is found.- private static volatile long sAppSearchVersionCode = -1;- // Context is used to check mainline module version, as support varies by module version. private final Context mContext;@@ -45,6 +36,9 @@
mContext = Preconditions.checkNotNull(context); }+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of+ // BuildCompat.isAtLeastV() is removed.+ @BuildCompat.PrereleaseSdkCheck @Override public boolean isFeatureSupported(@NonNull String feature) { switch (feature) {@@ -80,23 +74,39 @@
case Features.SET_SCHEMA_CIRCULAR_REFERENCES: return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE;- // Beyond Android U features+ // Android V Features case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:- // TODO(b/258715421) : Update to reflect support in Android U+ once this feature has- // an extservices sdk that includes it.- // fall through- case Features.SCHEMA_SET_DELETION_PROPAGATION:- // TODO(b/268521214) : Update when feature is ready in service-appsearch. // fall through case Features.SCHEMA_ADD_PARENT_TYPE:- // TODO(b/269295094) : Update when feature is ready in service-appsearch. // fall through case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:- // TODO(b/289150947) : Update when feature is ready in service-appsearch. // fall through case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:- // TODO(b/296088047) : Update when feature is ready in service-appsearch.- return false;+ // fall through+ case Features.LIST_FILTER_HAS_PROPERTY_FUNCTION:+ // fall through+ case Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG:+ // fall through+ case Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE:+ // fall through+ case Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG:+ // fall through+ case Features.ENTERPRISE_GLOBAL_SEARCH_SESSION:+ return BuildCompat.isAtLeastV();++ // Beyond Android V Features+ case Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG:+ // TODO(b/326656531) : Update when feature is ready in service-appsearch.+ // fall through+ case Features.SCHEMA_SET_DESCRIPTION:+ // TODO(b/326987971) : Update when feature is ready in service-appsearch.+ // fall through+ case Features.LIST_FILTER_TOKENIZE_FUNCTION:+ // TODO(b/332620561) : Update when feature is ready in service-appsearch.+ // fall through+ case Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS:+ // TODO(b/332642571) : Update when feature is ready in service-appsearch.+ // fall through default: return false; }@@ -107,53 +117,11 @@
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { return 64; } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {- // Sixty-four properties were enabled in mainline module 'aml_ase_331311020'- return getAppSearchVersionCode(mContext) >= 331311020 ? 64 : 16;+ // Sixty-four properties were enabled in mainline module of the U base version+ return AppSearchVersionUtil.getAppSearchVersionCode(mContext)+ >= AppSearchVersionUtil.APPSEARCH_U_BASE_VERSION_CODE ? 64 : 16; } else { return 16; } }-- @RequiresApi(Build.VERSION_CODES.Q)- private static long getAppSearchVersionCode(Context context) {- if (sAppSearchVersionCode != -1) {- return sAppSearchVersionCode;- }- synchronized (FeaturesImpl.class) {- // Check again in case it was assigned while waiting- if (sAppSearchVersionCode == -1) {- long appsearchVersionCode = 0;- try {- PackageManager packageManager = context.getPackageManager();- String appSearchPackageName =- ApiHelperForQ.getAppSearchPackageName(packageManager);- if (appSearchPackageName != null) {- PackageInfo pInfo = packageManager- .getPackageInfo(appSearchPackageName, PackageManager.MATCH_APEX);- appsearchVersionCode = ApiHelperForQ.getPackageInfoLongVersionCode(pInfo);- }- } catch (PackageManager.NameNotFoundException e) {- // Module not installed- }- sAppSearchVersionCode = appsearchVersionCode;- }- }- return sAppSearchVersionCode;- }-- @RequiresApi(Build.VERSION_CODES.Q)- private static class ApiHelperForQ {- @DoNotInline- static long getPackageInfoLongVersionCode(PackageInfo pInfo) {- return pInfo.getLongVersionCode();- }-- @DoNotInline- static String getAppSearchPackageName(PackageManager packageManager)- throws PackageManager.NameNotFoundException {- ModuleInfo appSearchModule =- packageManager.getModuleInfo(APPSEARCH_MODULE_NAME, 1);- return appSearchModule.getPackageName();- }- } }
@@ -20,20 +20,25 @@
import android.content.Context; import android.os.Build;+import androidx.annotation.DoNotInline; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi;+import androidx.appsearch.app.AppSearchEnvironmentFactory; import androidx.appsearch.app.AppSearchSession;+import androidx.appsearch.app.EnterpriseGlobalSearchSession;+import androidx.appsearch.app.Features; import androidx.appsearch.app.GlobalSearchSession; import androidx.appsearch.exceptions.AppSearchException; import androidx.appsearch.platformstorage.converter.SearchContextToPlatformConverter; import androidx.concurrent.futures.ResolvableFuture;+import androidx.core.os.BuildCompat; import androidx.core.util.Preconditions; import com.google.common.util.concurrent.ListenableFuture; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService;-import java.util.concurrent.Executors;+import java.util.function.Consumer; /** * An AppSearch storage system which stores data in the central AppSearch service, available on@@ -201,7 +206,8 @@
// execute() won't return anything, we will hang forever waiting for the execution. // AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all // mutate requests will need to gain write lock and query requests need to gain read lock.- static final Executor EXECUTOR = Executors.newCachedThreadPool();+ static final Executor EXECUTOR = AppSearchEnvironmentFactory.getEnvironmentInstance()+ .createCachedThreadPoolExecutor(); /** * Opens a new {@link AppSearchSession} on this storage.@@ -224,7 +230,7 @@
if (result.isSuccess()) { future.set( new SearchSessionImpl(result.getResultValue(), context.mExecutor,- new FeaturesImpl(context.mContext)));+ context.mContext)); } else { // Without the SuppressLint annotation on the method, this line causes a // lint error because getResultCode isn't defined as returning a value from@@ -266,4 +272,59 @@
}); return future; }++ /**+ * Opens a new {@link EnterpriseGlobalSearchSession} on this storage.+ */+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of+ // BuildCompat.isAtLeastV() is removed.+ @BuildCompat.PrereleaseSdkCheck+ @RequiresApi(35)+ @SuppressLint("WrongConstant")+ @NonNull+ public static ListenableFuture+ createEnterpriseGlobalSearchSessionAsync(@NonNull GlobalSearchContext context) {+ if (!BuildCompat.isAtLeastV()) {+ throw new UnsupportedOperationException(+ Features.ENTERPRISE_GLOBAL_SEARCH_SESSION+ + " is not supported on this AppSearch implementation");+ }+ Preconditions.checkNotNull(context);+ AppSearchManager appSearchManager =+ context.mContext.getSystemService(AppSearchManager.class);+ ResolvableFuture future = ResolvableFuture.create();+ ApiHelperForV.createEnterpriseGlobalSearchSession(+ appSearchManager,+ context.mExecutor,+ result -> {+ if (result.isSuccess()) {+ future.set(new EnterpriseGlobalSearchSessionImpl(+ result.getResultValue(), context.mExecutor,+ new FeaturesImpl(context.mContext)));+ } else {+ // Without the SuppressLint annotation on the method, this line causes a+ // lint error because getResultCode isn't defined as returning a value from+ // AppSearchResult.ResultCode+ future.setException(+ new AppSearchException(+ result.getResultCode(), result.getErrorMessage()));+ }+ });+ return future;+ }++ @RequiresApi(35)+ private static class ApiHelperForV {+ private ApiHelperForV() {+ // This class is not instantiable.+ }++ @DoNotInline+ static void createEnterpriseGlobalSearchSession(@NonNull AppSearchManager appSearchManager,+ @NonNull Executor executor,+ @NonNull Consumer+ android.app.appsearch.EnterpriseGlobalSearchSession>> callback) {+ appSearchManager.createEnterpriseGlobalSearchSession(executor, callback);+ }+ } }
@@ -21,6 +21,8 @@
import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo;+import androidx.appsearch.app.EmbeddingVector;+import androidx.appsearch.app.Features; import androidx.appsearch.app.GenericDocument; import androidx.core.util.Preconditions;@@ -87,6 +89,10 @@
platformSubDocuments[j] = toPlatformGenericDocument(documentValues[j]); } platformBuilder.setPropertyDocument(propertyName, platformSubDocuments);+ } else if (property instanceof EmbeddingVector[]) {+ // TODO(b/326656531): Remove this once embedding search APIs are available.+ throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG+ + " is not available on this AppSearch implementation."); } else { throw new IllegalStateException( String.format("Property \"%s\" has unsupported value type %s", propertyName,@@ -143,6 +149,8 @@
} jetpackBuilder.setPropertyDocument(propertyName, jetpackSubDocuments); } else {+ // TODO(b/326656531) : Add an entry for EmbeddingVector once it becomes+ // available in platform. throw new IllegalStateException( String.format("Property \"%s\" has unsupported value type %s", propertyName, property.getClass().toString()));
@@ -18,6 +18,7 @@
import android.os.Build;+import androidx.annotation.DoNotInline; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo;@@ -27,6 +28,7 @@
import androidx.appsearch.app.RemoveByDocumentIdRequest; import androidx.appsearch.app.ReportSystemUsageRequest; import androidx.appsearch.app.ReportUsageRequest;+import androidx.core.os.BuildCompat; import androidx.core.util.Preconditions; import java.util.List;@@ -45,16 +47,37 @@
* Translates a jetpack {@link PutDocumentsRequest} into a platform * {@link android.app.appsearch.PutDocumentsRequest}. */+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of+ // BuildCompat.isAtLeastV() is removed.+ @BuildCompat.PrereleaseSdkCheck @NonNull public static android.app.appsearch.PutDocumentsRequest toPlatformPutDocumentsRequest( @NonNull PutDocumentsRequest jetpackRequest) { Preconditions.checkNotNull(jetpackRequest); android.app.appsearch.PutDocumentsRequest.Builder platformBuilder = new android.app.appsearch.PutDocumentsRequest.Builder();+ // Convert normal generic documents. for (GenericDocument jetpackDocument : jetpackRequest.getGenericDocuments()) { platformBuilder.addGenericDocuments( GenericDocumentToPlatformConverter.toPlatformGenericDocument(jetpackDocument)); }+ // Convert taken action generic documents.+ for (GenericDocument jetpackTakenActionGenericDocument :+ jetpackRequest.getTakenActionGenericDocuments()) {+ if (BuildCompat.isAtLeastV()) {+ ApiHelperForV.addTakenActionGenericDocuments(+ platformBuilder,+ GenericDocumentToPlatformConverter.toPlatformGenericDocument(+ jetpackTakenActionGenericDocument));+ } else {+ // This version of platform-storage doesn't support the dedicated+ // addTakenActionGenericDocuments API, but we can still add them to the index via+ // the put API (just without logging).+ platformBuilder.addGenericDocuments(+ GenericDocumentToPlatformConverter.toPlatformGenericDocument(+ jetpackTakenActionGenericDocument));+ }+ } return platformBuilder.build(); }@@ -71,7 +94,7 @@
jetpackRequest.getNamespace()) .addIds(jetpackRequest.getIds()); for (Map.Entry> projection :- jetpackRequest.getProjectionsInternal().entrySet()) {+ jetpackRequest.getProjections().entrySet()) { platformBuilder.addProjection(projection.getKey(), projection.getValue()); } return platformBuilder.build();@@ -122,4 +145,24 @@
.setUsageTimestampMillis(jetpackRequest.getUsageTimestampMillis()) .build(); }++ @RequiresApi(35)+ private static class ApiHelperForV {+ private ApiHelperForV() {}++ @DoNotInline+ static void addTakenActionGenericDocuments(+ android.app.appsearch.PutDocumentsRequest.Builder platformBuilder,+ android.app.appsearch.GenericDocument platformTakenActionGenericDocument) {+ try {+ platformBuilder.addTakenActionGenericDocuments(platformTakenActionGenericDocument);+ } catch (android.app.appsearch.exceptions.AppSearchException e) {+ // This method incorrectly declares that it throws AppSearchException, whereas in+ // fact there's nothing in its implementation that would do so. Suppress it here+ // instead of piping all the way through the stack.+ throw new RuntimeException(+ "Unexpected AppSearchException which should not be possible", e);+ }+ }+ } }
@@ -28,8 +28,10 @@
import androidx.annotation.RestrictTo; import androidx.appsearch.app.AppSearchSchema; import androidx.appsearch.app.Features;+import androidx.core.os.BuildCompat; import androidx.core.util.Preconditions;+import java.util.Collection; import java.util.List; /**@@ -46,23 +48,36 @@
* Translates a jetpack {@link AppSearchSchema} into a platform * {@link android.app.appsearch.AppSearchSchema}. */+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of+ // BuildCompat.isAtLeastV() is removed.+ @BuildCompat.PrereleaseSdkCheck @NonNull public static android.app.appsearch.AppSearchSchema toPlatformSchema( @NonNull AppSearchSchema jetpackSchema) { Preconditions.checkNotNull(jetpackSchema); android.app.appsearch.AppSearchSchema.Builder platformBuilder = new android.app.appsearch.AppSearchSchema.Builder(jetpackSchema.getSchemaType());+ if (!jetpackSchema.getDescription().isEmpty()) {+ // TODO(b/326987971): Remove this once description becomes available.+ throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION+ + " is not available on this AppSearch implementation.");+ }+ if (!jetpackSchema.getParentTypes().isEmpty()) {+ if (!BuildCompat.isAtLeastV()) {+ throw new UnsupportedOperationException(Features.SCHEMA_ADD_PARENT_TYPE+ + " is not available on this AppSearch implementation.");+ }+ List parentTypes = jetpackSchema.getParentTypes();+ for (int i = 0; i < parentTypes.size(); i++) {+ ApiHelperForV.addParentType(platformBuilder, parentTypes.get(i));+ }+ } List properties = jetpackSchema.getProperties(); for (int i = 0; i < properties.size(); i++) { android.app.appsearch.AppSearchSchema.PropertyConfig platformProperty = toPlatformProperty(properties.get(i)); platformBuilder.addProperty(platformProperty); }- if (!jetpackSchema.getParentTypes().isEmpty()) {- // TODO(b/269295094): Remove this once polymorphism becomes available.- throw new UnsupportedOperationException(Features.SCHEMA_ADD_PARENT_TYPE- + " is not available on this AppSearch implementation.");- } return platformBuilder.build(); }@@ -70,6 +85,9 @@
* Translates a platform {@link android.app.appsearch.AppSearchSchema} to a jetpack * {@link AppSearchSchema}. */+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of+ // BuildCompat.isAtLeastV() is removed.+ @BuildCompat.PrereleaseSdkCheck @NonNull public static AppSearchSchema toJetpackSchema( @NonNull android.app.appsearch.AppSearchSchema platformSchema) {@@ -78,22 +96,36 @@
new AppSearchSchema.Builder(platformSchema.getSchemaType()); List properties = platformSchema.getProperties();+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform.+ if (BuildCompat.isAtLeastV()) {+ List parentTypes = ApiHelperForV.getParentTypes(platformSchema);+ for (int i = 0; i < parentTypes.size(); i++) {+ jetpackBuilder.addParentType(parentTypes.get(i));+ }+ } for (int i = 0; i < properties.size(); i++) { AppSearchSchema.PropertyConfig jetpackProperty = toJetpackProperty(properties.get(i)); jetpackBuilder.addProperty(jetpackProperty); }- // TODO(b/269295094): Call jetpackBuilder.addParentType() to add parent types once- // polymorphism becomes available in platform. return jetpackBuilder.build(); } // Most stringProperty.get calls cause WrongConstant lint errors because the methods are not // defined as returning the same constants as the corresponding setter expects, but they do @SuppressLint("WrongConstant")+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of+ // BuildCompat.isAtLeastV() is removed.+ @BuildCompat.PrereleaseSdkCheck @NonNull private static android.app.appsearch.AppSearchSchema.PropertyConfig toPlatformProperty( @NonNull AppSearchSchema.PropertyConfig jetpackProperty) { Preconditions.checkNotNull(jetpackProperty);+ if (!jetpackProperty.getDescription().isEmpty()) {+ // TODO(b/326987971): Remove this once description becomes available.+ throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION+ + " is not available on this AppSearch implementation.");+ } if (jetpackProperty instanceof AppSearchSchema.StringPropertyConfig) { AppSearchSchema.StringPropertyConfig stringProperty = (AppSearchSchema.StringPropertyConfig) jetpackProperty;@@ -110,12 +142,6 @@
TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, "tokenizerType"); }- if (stringProperty.getDeletionPropagation()) {- // TODO(b/268521214): Update once deletion propagation is available.- throw new UnsupportedOperationException("Setting deletion propagation is not "- + "supported on this AppSearch implementation.");- }- if (stringProperty.getJoinableValueType() == AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {@@ -163,18 +189,26 @@
} else if (jetpackProperty instanceof AppSearchSchema.DocumentPropertyConfig) { AppSearchSchema.DocumentPropertyConfig documentProperty = (AppSearchSchema.DocumentPropertyConfig) jetpackProperty;+ android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder platformBuilder =+ new android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder(+ documentProperty.getName(), documentProperty.getSchemaType())+ .setCardinality(documentProperty.getCardinality())+ .setShouldIndexNestedProperties(+ documentProperty.shouldIndexNestedProperties()); if (!documentProperty.getIndexableNestedProperties().isEmpty()) {- // TODO(b/289150947): Update and set list once indexable-nested-properties-list is- // available.- throw new UnsupportedOperationException(- "DocumentPropertyConfig.addIndexableNestedProperties is not supported on "- + "this AppSearch implementation.");+ if (!BuildCompat.isAtLeastV()) {+ throw new UnsupportedOperationException(+ "DocumentPropertyConfig.addIndexableNestedProperties is not supported "+ + "on this AppSearch implementation.");+ }+ ApiHelperForV.addIndexableNestedProperties(+ platformBuilder, documentProperty.getIndexableNestedProperties()); }- return new android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder(- documentProperty.getName(), documentProperty.getSchemaType())- .setCardinality(documentProperty.getCardinality())- .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())- .build();+ return platformBuilder.build();+ } else if (jetpackProperty instanceof AppSearchSchema.EmbeddingPropertyConfig) {+ // TODO(b/326656531): Remove this once embedding search APIs are available.+ throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG+ + " is not available on this AppSearch implementation."); } else { throw new IllegalArgumentException( "Invalid dataType: " + jetpackProperty.getDataType());@@ -184,6 +218,9 @@
// Most stringProperty.get calls cause WrongConstant lint errors because the methods are not // defined as returning the same constants as the corresponding setter expects, but they do @SuppressLint("WrongConstant")+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of+ // BuildCompat.isAtLeastV() is removed.+ @BuildCompat.PrereleaseSdkCheck @NonNull private static AppSearchSchema.PropertyConfig toJetpackProperty( @NonNull android.app.appsearch.AppSearchSchema.PropertyConfig platformProperty) {@@ -201,6 +238,8 @@
jetpackBuilder.setJoinableValueType( ApiHelperForU.getJoinableValueType(stringProperty)); }+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform. return jetpackBuilder.build(); } else if (platformProperty instanceof android.app.appsearch.AppSearchSchema.LongPropertyConfig) {@@ -213,19 +252,27 @@
jetpackBuilder.setIndexingType( ApiHelperForU.getIndexingType(longProperty)); }+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform. return jetpackBuilder.build(); } else if (platformProperty instanceof android.app.appsearch.AppSearchSchema.DoublePropertyConfig) {+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform. return new AppSearchSchema.DoublePropertyConfig.Builder(platformProperty.getName()) .setCardinality(platformProperty.getCardinality()) .build(); } else if (platformProperty instanceof android.app.appsearch.AppSearchSchema.BooleanPropertyConfig) {+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform. return new AppSearchSchema.BooleanPropertyConfig.Builder(platformProperty.getName()) .setCardinality(platformProperty.getCardinality()) .build(); } else if (platformProperty instanceof android.app.appsearch.AppSearchSchema.BytesPropertyConfig) {+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform. return new AppSearchSchema.BytesPropertyConfig.Builder(platformProperty.getName()) .setCardinality(platformProperty.getCardinality()) .build();@@ -233,15 +280,24 @@
instanceof android.app.appsearch.AppSearchSchema.DocumentPropertyConfig) { android.app.appsearch.AppSearchSchema.DocumentPropertyConfig documentProperty = (android.app.appsearch.AppSearchSchema.DocumentPropertyConfig) platformProperty;- return new AppSearchSchema.DocumentPropertyConfig.Builder(- documentProperty.getName(),- documentProperty.getSchemaType())- .setCardinality(documentProperty.getCardinality())- .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())- .build();- // TODO(b/289150947): Add the indexable_nested_properties_list once it becomes- // available in platform.+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform.+ AppSearchSchema.DocumentPropertyConfig.Builder jetpackBuilder =+ new AppSearchSchema.DocumentPropertyConfig.Builder(+ documentProperty.getName(),+ documentProperty.getSchemaType())+ .setCardinality(documentProperty.getCardinality())+ .setShouldIndexNestedProperties(+ documentProperty.shouldIndexNestedProperties());+ if (BuildCompat.isAtLeastV()) {+ List indexableNestedProperties =+ ApiHelperForV.getIndexableNestedProperties(documentProperty);+ jetpackBuilder.addIndexableNestedProperties(indexableNestedProperties);+ }+ return jetpackBuilder.build(); } else {+ // TODO(b/326656531) : Add an entry for EmbeddingPropertyConfig once it becomes+ // available in platform. throw new IllegalArgumentException( "Invalid property type " + platformProperty.getClass() + ": " + platformProperty);@@ -288,4 +344,38 @@
return longPropertyConfig.getIndexingType(); } }+++ @RequiresApi(35)+ private static class ApiHelperForV {+ private ApiHelperForV() {}++ @DoNotInline+ @SuppressLint("NewApi")+ static void addParentType(+ android.app.appsearch.AppSearchSchema.Builder platformBuilder,+ @NonNull String parentSchemaType) {+ platformBuilder.addParentType(parentSchemaType);+ }++ @DoNotInline+ static void addIndexableNestedProperties(+ android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder+ platformBuilder,+ @NonNull Collection indexableNestedProperties) {+ platformBuilder.addIndexableNestedProperties(indexableNestedProperties);+ }++ @DoNotInline+ static List getParentTypes(android.app.appsearch.AppSearchSchema platformSchema) {+ return platformSchema.getParentTypes();+ }++ @DoNotInline+ static List getIndexableNestedProperties(+ android.app.appsearch.AppSearchSchema.DocumentPropertyConfig+ platformDocumentProperty) {+ return platformDocumentProperty.getIndexableNestedProperties();+ }+ } }
@@ -26,6 +26,7 @@
import androidx.appsearch.app.Features; import androidx.appsearch.app.JoinSpec; import androidx.appsearch.app.SearchSpec;+import androidx.core.os.BuildCompat; import androidx.core.util.Preconditions; import java.util.List;@@ -46,6 +47,9 @@
// Most jetpackSearchSpec.get calls cause WrongConstant lint errors because the methods are not // defined as returning the same constants as the corresponding setter expects, but they do @SuppressLint("WrongConstant")+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of+ // BuildCompat.isAtLeastV() is removed.+ @BuildCompat.PrereleaseSdkCheck @NonNull public static android.app.appsearch.SearchSpec toPlatformSearchSpec( @NonNull SearchSpec jetpackSearchSpec) {@@ -77,15 +81,12 @@
.setSnippetCountPerProperty(jetpackSearchSpec.getSnippetCountPerProperty()) .setMaxSnippetSize(jetpackSearchSpec.getMaxSnippetSize()); if (jetpackSearchSpec.getResultGroupingTypeFlags() != 0) {- // TODO(b/258715421): Add Build.VERSION.SDK_INT condition once there is an extservices- // sdk that includes SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA.- if (true) {- if ((jetpackSearchSpec.getResultGroupingTypeFlags()- & SearchSpec.GROUPING_TYPE_PER_SCHEMA) != 0) {- throw new UnsupportedOperationException(+ if ((jetpackSearchSpec.getResultGroupingTypeFlags()+ & SearchSpec.GROUPING_TYPE_PER_SCHEMA) != 0+ && !BuildCompat.isAtLeastV()) {+ throw new UnsupportedOperationException( Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA- + " is not available on this AppSearch implementation.");- }+ + " is not available on this AppSearch implementation."); } platformBuilder.setResultGrouping( jetpackSearchSpec.getResultGroupingTypeFlags(),@@ -107,6 +108,7 @@
} if (!jetpackSearchSpec.getEnabledFeatures().isEmpty()) {+ // Copy U features if (jetpackSearchSpec.isNumericSearchEnabled() || jetpackSearchSpec.isVerbatimSearchEnabled() || jetpackSearchSpec.isListFilterQueryLanguageEnabled()) {@@ -118,6 +120,27 @@
} ApiHelperForU.copyEnabledFeatures(platformBuilder, jetpackSearchSpec); }+ // Copy V features+ if (jetpackSearchSpec.isListFilterHasPropertyFunctionEnabled()) {+ if (!BuildCompat.isAtLeastV()) {+ throw new UnsupportedOperationException(+ Features.LIST_FILTER_HAS_PROPERTY_FUNCTION+ + " is not available on this AppSearch implementation.");+ }+ ApiHelperForV.copyEnabledFeatures(platformBuilder, jetpackSearchSpec);+ }+ // Copy beyond-V features+ if (jetpackSearchSpec.isEmbeddingSearchEnabled()+ || !jetpackSearchSpec.getSearchEmbeddings().isEmpty()) {+ // TODO(b/326656531): Remove this once embedding search APIs are available.+ throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG+ + " is not available on this AppSearch implementation.");+ }+ if (jetpackSearchSpec.isListFilterTokenizeFunctionEnabled()) {+ // TODO(b/332620561): Remove this once 'tokenize' is supported.+ throw new UnsupportedOperationException(Features.LIST_FILTER_TOKENIZE_FUNCTION+ + " is not available on this AppSearch implementation.");+ } } if (jetpackSearchSpec.getJoinSpec() != null) {@@ -129,9 +152,29 @@
} if (!jetpackSearchSpec.getFilterProperties().isEmpty()) {- // TODO(b/296088047): Remove this once property filters become available.- throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES- + " is not available on this AppSearch implementation.");+ if (!BuildCompat.isAtLeastV()) {+ throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES+ + " is not available on this AppSearch implementation.");+ }+ ApiHelperForV.addFilterProperties(+ platformBuilder, jetpackSearchSpec.getFilterProperties());+ }++ if (jetpackSearchSpec.getSearchSourceLogTag() != null) {+ if (!BuildCompat.isAtLeastV()) {+ throw new UnsupportedOperationException(+ Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG+ + " is not available on this AppSearch implementation.");+ }+ ApiHelperForV.setSearchSourceLogTag(+ platformBuilder, jetpackSearchSpec.getSearchSourceLogTag());+ }++ if (!jetpackSearchSpec.getInformationalRankingExpressions().isEmpty()) {+ // TODO(b/332642571): Remove this once informational ranking expressions are available.+ throw new UnsupportedOperationException(+ Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS+ + " are not available on this AppSearch implementation."); } return platformBuilder.build(); }@@ -143,6 +186,9 @@
} @DoNotInline+ // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of+ // BuildCompat.isAtLeastV() is removed.+ @BuildCompat.PrereleaseSdkCheck static void setJoinSpec(@NonNull android.app.appsearch.SearchSpec.Builder builder, JoinSpec jetpackJoinSpec) { builder.setJoinSpec(JoinSpecToPlatformConverter.toPlatformJoinSpec(jetpackJoinSpec));@@ -176,4 +222,34 @@
} } }++ @RequiresApi(35)+ private static class ApiHelperForV {+ private ApiHelperForV() {}++ @DoNotInline+ static void addFilterProperties(+ @NonNull android.app.appsearch.SearchSpec.Builder platformBuilder,+ Map> properties) {+ for (Map.Entry> entry : properties.entrySet()) {+ platformBuilder.addFilterProperties(entry.getKey(), entry.getValue());+ }+ }++ @DoNotInline+ static void copyEnabledFeatures(+ @NonNull android.app.appsearch.SearchSpec.Builder platformBuilder,+ @NonNull SearchSpec jetpackSpec) {+ if (jetpackSpec.isListFilterHasPropertyFunctionEnabled()) {+ platformBuilder.setListFilterHasPropertyFunctionEnabled(true);+ }+ }++ @DoNotInline+ static void setSearchSourceLogTag(+ android.app.appsearch.SearchSpec.Builder platformBuilder,+ String searchSourceLogTag) {+ platformBuilder.setSearchSourceLogTag(searchSourceLogTag);+ }+ } }
@@ -0,0 +1,96 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.platformstorage.util;++import android.content.Context;+import android.content.pm.ModuleInfo;+import android.content.pm.PackageInfo;+import android.content.pm.PackageManager;+import android.os.Build;++import androidx.annotation.DoNotInline;+import androidx.annotation.NonNull;+import androidx.annotation.RequiresApi;+import androidx.annotation.RestrictTo;+import androidx.core.util.Preconditions;++/**+ * Utilities for retrieving platform AppSearch's module version code.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public class AppSearchVersionUtil {+ public static final long APPSEARCH_U_BASE_VERSION_CODE = 331311020;+ public static final long APPSEARCH_M2023_11_VERSION_CODE = 341113000;++ private static final String APPSEARCH_MODULE_NAME = "com.android.appsearch";++ // This will be set to -1 to indicate the AppSearch version code hasn't bee checked, then to+ // 0 if it is not found, or the version code if it is found.+ private static volatile long sAppSearchVersionCode = -1;++ private AppSearchVersionUtil() {+ }++ /**+ * Returns AppSearch's version code from the context.+ */+ @RequiresApi(Build.VERSION_CODES.Q)+ public static long getAppSearchVersionCode(@NonNull Context context) {+ Preconditions.checkNotNull(context);+ if (sAppSearchVersionCode != -1) {+ return sAppSearchVersionCode;+ }+ synchronized (AppSearchVersionUtil.class) {+ // Check again in case it was assigned while waiting+ if (sAppSearchVersionCode == -1) {+ long appsearchVersionCode = 0;+ try {+ PackageManager packageManager = context.getPackageManager();+ String appSearchPackageName =+ ApiHelperForQ.getAppSearchPackageName(packageManager);+ if (appSearchPackageName != null) {+ PackageInfo pInfo = packageManager+ .getPackageInfo(appSearchPackageName, PackageManager.MATCH_APEX);+ appsearchVersionCode = ApiHelperForQ.getPackageInfoLongVersionCode(pInfo);+ }+ } catch (PackageManager.NameNotFoundException e) {+ // Module not installed+ }+ sAppSearchVersionCode = appsearchVersionCode;+ }+ }+ return sAppSearchVersionCode;+ }++ @RequiresApi(Build.VERSION_CODES.Q)+ private static class ApiHelperForQ {+ @DoNotInline+ static long getPackageInfoLongVersionCode(PackageInfo pInfo) {+ return pInfo.getLongVersionCode();+ }++ @DoNotInline+ static String getAppSearchPackageName(PackageManager packageManager)+ throws PackageManager.NameNotFoundException {+ ModuleInfo appSearchModule =+ packageManager.getModuleInfo(APPSEARCH_MODULE_NAME, 1);+ return appSearchModule.getPackageName();+ }+ }+}
@@ -0,0 +1,157 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.platformstorage.util;++import androidx.annotation.NonNull;+import androidx.annotation.RestrictTo;+import androidx.appsearch.app.AppSearchSchema;+import androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig;+import androidx.appsearch.app.AppSearchSchema.LongPropertyConfig;+import androidx.appsearch.app.AppSearchSchema.PropertyConfig;+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;+import androidx.appsearch.exceptions.IllegalSchemaException;+import androidx.collection.ArrayMap;+import androidx.collection.ArraySet;++import java.util.Map;+import java.util.Set;++/**+ * Utilities for schema validation.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public class SchemaValidationUtil {+ private SchemaValidationUtil() {+ }++ /**+ * Checks that the set of AppSearch schemas form valid schema-type definitions, and do not+ * exceed the maximum number of indexed properties allowed.+ *+ * @param appSearchSchemas Set of AppSearch Schemas for a+ * {@link androidx.appsearch.app.SetSchemaRequest}.+ * @param maxSectionsAllowed The maximum number of sections allowed per AppSearch schema.+ * @throws IllegalSchemaException if any schema in the set is invalid. A schema is invalid if+ * it contains an invalid cycle, has an undefined document+ * property config, or exceeds the+ * maximum number of sections allowed.+ */+ public static void checkSchemasAreValidOrThrow(@NonNull Set appSearchSchemas,+ int maxSectionsAllowed) throws IllegalSchemaException {+ Map knownSchemas = new ArrayMap<>();+ for (AppSearchSchema schema : appSearchSchemas) {+ knownSchemas.put(schema.getSchemaType(), schema);+ }+ Map cachedNumSectionsMap = new ArrayMap<>();+ for (AppSearchSchema schema : appSearchSchemas) {+ // Check that the number of sections is below the max allowed+ int numSections = getNumSectionsInSchemaOrThrow(schema, knownSchemas,+ cachedNumSectionsMap, new ArraySet<>());+ if (numSections > maxSectionsAllowed) {+ throw new IllegalSchemaException(+ "Too many properties to be indexed, max " + "number of properties allowed: "+ + maxSectionsAllowed);+ }+ }+ }++ /**+ * Returns the number of indexes sections in a given AppSearch schema.+ *+ * @param schema The AppSearch schema to get the number of sections for.+ * @param knownSchemas Map of known schema-type strings to their corresponding+ * schemas.+ * @param cachedNumSectionsInSchema Map of the cached number of sections in schemas which have+ * already been expanded.+ * @param visitedSchemaTypes Set of schemas that have already been expanded as parents+ * of the current schema.+ * @throws IllegalSchemaException if the schema contains an invalid cycle, or contains a+ * DocumentPropertyConfig where the config's schema type is+ * unknown.+ */+ private static int getNumSectionsInSchemaOrThrow(@NonNull AppSearchSchema schema,+ @NonNull Map knownSchemas,+ @NonNull Map cachedNumSectionsInSchema,+ @NonNull Set visitedSchemaTypes)+ throws IllegalSchemaException {+ String schemaType = schema.getSchemaType();+ if (visitedSchemaTypes.contains(schemaType)) {+ // We've hit an illegal cycle where all DocumentPropertyConfigs set+ // shouldIndexNestedProperties = true.+ throw new IllegalSchemaException(+ "Invalid cycle detected in schema type configs. '" + schemaType+ + "' references itself.");+ }+ if (cachedNumSectionsInSchema.containsKey(schemaType)) {+ // We've already calculated and cached the number of sections in this AppSearch schema,+ // just return this value.+ return cachedNumSectionsInSchema.get(schemaType);+ }++ visitedSchemaTypes.add(schemaType);+ int numSections = 0;+ for (PropertyConfig property : schema.getProperties()) {+ if (property.getDataType() == PropertyConfig.DATA_TYPE_DOCUMENT) {+ DocumentPropertyConfig documentProperty = (DocumentPropertyConfig) property;+ String docPropertySchemaType = documentProperty.getSchemaType();+ if (!knownSchemas.containsKey(docPropertySchemaType)) {+ // The schema type that this document property config is referring to+ // does not exist in the provided schemas+ throw new IllegalSchemaException(+ "Undefined schema type: " + docPropertySchemaType);+ }+ if (!documentProperty.shouldIndexNestedProperties()) {+ numSections += documentProperty.getIndexableNestedProperties().size();+ } else {+ numSections += getNumSectionsInSchemaOrThrow(+ knownSchemas.get(docPropertySchemaType), knownSchemas,+ cachedNumSectionsInSchema, visitedSchemaTypes);+ }+ } else {+ numSections += isPropertyIndexable(property) ? 1 : 0;+ }+ }+ visitedSchemaTypes.remove(schemaType);+ cachedNumSectionsInSchema.put(schemaType, numSections);+ return numSections;+ }++ private static boolean isPropertyIndexable(PropertyConfig propertyConfig) {+ switch (propertyConfig.getDataType()) {+ case PropertyConfig.DATA_TYPE_STRING:+ return ((StringPropertyConfig) propertyConfig).getIndexingType()+ != StringPropertyConfig.INDEXING_TYPE_NONE;+ case PropertyConfig.DATA_TYPE_LONG:+ return ((LongPropertyConfig) propertyConfig).getIndexingType()+ != LongPropertyConfig.INDEXING_TYPE_NONE;+ case PropertyConfig.DATA_TYPE_DOCUMENT:+ DocumentPropertyConfig documentProperty = (DocumentPropertyConfig) propertyConfig;+ return documentProperty.shouldIndexNestedProperties()+ || !documentProperty.getIndexableNestedProperties().isEmpty();+ case PropertyConfig.DATA_TYPE_DOUBLE:+ // fallthrough+ case PropertyConfig.DATA_TYPE_BOOLEAN:+ // fallthrough+ case PropertyConfig.DATA_TYPE_BYTES:+ // fallthrough+ default:+ return false;+ }+ }+}
@@ -45,60 +45,88 @@
case Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK: // Android U Features case Features.SEARCH_SPEC_PROPERTY_WEIGHTS:- // TODO(b/203700301) : Update to reflect support in Android U+ once this feature is- // synced over into service-appsearch.+ // TODO(b/203700301) : Update to reflect support once this feature is synced over+ // into gms-appsearch. // fall through case Features.TOKENIZER_TYPE_RFC822:- // TODO(b/259294369) : Update to reflect support in Android U+ once this feature is- // synced over into service-appsearch.+ // TODO(b/259294369) : Update to reflect support once this feature is synced over+ // into gms-appsearch. // fall through case Features.NUMERIC_SEARCH:- // TODO(b/259744228) : Update to reflect support in Android U+ once this feature is- // synced over into service-appsearch.+ // TODO(b/259744228) : Update to reflect support once this feature is synced over+ // into gms-appsearch. // fall through case SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION:- // TODO(b/261474063) : Update to reflect support in Android U+ once advanced- // ranking becomes available.+ // TODO(b/261474063) : Update to reflect support once this feature is synced over+ // into gms-appsearch. // fall through case Features.JOIN_SPEC_AND_QUALIFIED_ID:- // TODO(b/256022027) : Update to reflect support in Android U+ once this feature is- // synced over into service-appsearch.+ // TODO(b/256022027) : Update to reflect support once this feature is synced over+ // into gms-appsearch. // fall through case Features.VERBATIM_SEARCH:- // TODO(b/204333391) : Update to reflect support in Android U+ once this feature is- // synced over into service-appsearch.+ // TODO(b/204333391) : Update to reflect support once this feature is synced over+ // into gms-appsearch. // fall through case Features.LIST_FILTER_QUERY_LANGUAGE:- // TODO(b/208654892) : Update to reflect support in Android U+ once this feature is- // synced over into service-appsearch.+ // TODO(b/208654892) : Update to reflect support once this feature is synced over+ // into gms-appsearch.+ // fall through+ case Features.LIST_FILTER_HAS_PROPERTY_FUNCTION:+ // TODO(b/309826655) : Update to reflect support once this feature is synced over+ // into gms-appsearch.+ // fall through+ case Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG:+ // TODO(b/326656531) : Update to reflect support once this feature is synced over+ // into gms-appsearch. // fall through case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:- // TODO(b/258715421) : Update to reflect support in Android U+ once this feature is- // synced over into service-appsearch.+ // TODO(b/258715421) : Update to reflect support once this feature is synced over+ // into gms-appsearch. // fall through case Features.SEARCH_SUGGESTION:- // TODO(b/227356108) : Update to reflect support in Android U+ once this feature is- // synced over into service-appsearch.- // fall through- case Features.SCHEMA_SET_DELETION_PROPAGATION:- // TODO(b/268521214) : Update to reflect support in Android U+ once this feature is- // synced over into service-appsearch.+ // TODO(b/227356108) : Update to reflect support once this feature is synced over+ // into gms-appsearch. // fall through case Features.SET_SCHEMA_CIRCULAR_REFERENCES:- // TODO(b/280698121) : Update to reflect support in Android U+ once this feature is- // synced over into service-appsearch.+ // TODO(b/280698121) : Update to reflect support once this feature is synced over+ // into gms-appsearch. // fall through case Features.SCHEMA_ADD_PARENT_TYPE:- // TODO(b/269295094) : Update to reflect support in Android U+ once this feature is- // synced over into service-appsearch.+ // TODO(b/269295094) : Update to reflect support once this feature is synced over+ // into gms-appsearch. // fall through case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:- // TODO(b/289150947) : Update to reflect support in Android U+ once this feature is- // synced over into service-appsearch.+ // TODO(b/289150947) : Update to reflect support once this feature is synced over+ // into gms-appsearch.+ // fall through+ case Features.SCHEMA_SET_DESCRIPTION:+ // TODO(b/326987971) : Update to reflect support once this feature is synced over+ // into gms-appsearch. // fall through case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:- // TODO(b/296088047) : Update to reflect support in Android U+ once this feature is- // synced over into service-appsearch.+ // TODO(b/296088047) : Update to reflect support once this feature is synced over+ // into gms-appsearch.+ // fall through+ case Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG:+ // TODO(b/296088047) : Update to reflect support once this feature is synced over+ // into gms-appsearch.+ // fall through+ case Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE:+ // TODO(b/275592563) : Update to reflect support once this feature is synced over+ // into gms-appsearch.+ // fall through+ case Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG:+ // TODO(b/275592563) : Update to reflect support once this feature is synced over+ // into gms-appsearch.+ // fall through+ case Features.LIST_FILTER_TOKENIZE_FUNCTION:+ // TODO(b/332620561) : Update to reflect support once this feature is synced over+ // into gms-appsearch.+ // fall through+ case Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS:+ // TODO(b/332642571) : Update to reflect support once this feature is synced over+ // into gms-appsearch. return false; default: return false; // AppSearch features in U+, absent in GMSCore AppSearch.
@@ -19,6 +19,7 @@
import android.content.Context; import androidx.annotation.NonNull;+import androidx.appsearch.app.AppSearchEnvironmentFactory; import androidx.appsearch.app.AppSearchSession; import androidx.appsearch.app.GlobalSearchSession; import androidx.appsearch.playservicesstorage.util.AppSearchTaskFutures;@@ -33,7 +34,6 @@
import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService;-import java.util.concurrent.Executors; /** * An AppSearch storage system which stores data in the central AppSearch service in Google@@ -200,7 +200,8 @@
// execute() won't return anything, we will hang forever waiting for the execution. // AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all // mutate requests will need to gain write lock and query requests need to gain read lock.- static final Executor EXECUTOR = Executors.newCachedThreadPool();+ static final Executor EXECUTOR = AppSearchEnvironmentFactory.getEnvironmentInstance()+ .createCachedThreadPoolExecutor(); /** * Opens a new {@link AppSearchSession} on this storage.
@@ -18,6 +18,8 @@
import androidx.annotation.NonNull; import androidx.annotation.RestrictTo;+import androidx.appsearch.app.EmbeddingVector;+import androidx.appsearch.app.Features; import androidx.appsearch.app.GenericDocument; import androidx.core.util.Preconditions;@@ -71,6 +73,10 @@
} gmsBuilder.setPropertyDocument(propertyName, gmsSubDocuments);+ } else if (property instanceof EmbeddingVector[]) {+ // TODO(b/326656531): Remove this once embedding search APIs are available.+ throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG+ + " is not available on this AppSearch implementation."); } else { throw new IllegalStateException( String.format("Property \"%s\" has unsupported value type %s",@@ -121,6 +127,8 @@
} jetpackBuilder.setPropertyDocument(propertyName, jetpackSubDocuments); } else {+ // TODO(b/326656531) : Add an entry for EmbeddingVector once it becomes+ // available in gms-appsearch. throw new IllegalStateException( String.format("Property \"%s\" has unsupported value type %s", propertyName, property.getClass().toString()));
@@ -19,6 +19,7 @@
import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.appsearch.app.AppSearchSchema;+import androidx.appsearch.app.Features; import androidx.core.util.Preconditions; import java.util.List;@@ -44,6 +45,11 @@
com.google.android.gms.appsearch.AppSearchSchema.Builder gmsBuilder = new com.google.android.gms.appsearch.AppSearchSchema .Builder(jetpackSchema.getSchemaType());+ if (!jetpackSchema.getDescription().isEmpty()) {+ // TODO(b/326987971): Remove this once description becomes available.+ throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION+ + " is not available on this AppSearch implementation.");+ } List properties = jetpackSchema.getProperties(); for (int i = 0; i < properties.size(); i++) { com.google.android.gms.appsearch.AppSearchSchema.PropertyConfig gmsProperty =@@ -64,6 +70,8 @@
Preconditions.checkNotNull(gmsSchema); AppSearchSchema.Builder jetpackBuilder = new AppSearchSchema.Builder(gmsSchema.getSchemaType());+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform. List properties = gmsSchema.getProperties(); for (int i = 0; i < properties.size(); i++) {@@ -77,6 +85,11 @@
private static com.google.android.gms.appsearch.AppSearchSchema.PropertyConfig toGmsProperty( @NonNull AppSearchSchema.PropertyConfig jetpackProperty) { Preconditions.checkNotNull(jetpackProperty);+ if (!jetpackProperty.getDescription().isEmpty()) {+ // TODO(b/326987971): Remove this once description becomes available.+ throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION+ + " is not available on this AppSearch implementation.");+ } if (jetpackProperty instanceof AppSearchSchema.StringPropertyConfig) { AppSearchSchema.StringPropertyConfig stringProperty = (AppSearchSchema.StringPropertyConfig) jetpackProperty;@@ -87,12 +100,6 @@
.setCardinality(stringProperty.getCardinality()) .setIndexingType(stringProperty.getIndexingType()) .setTokenizerType(stringProperty.getTokenizerType());- if (stringProperty.getDeletionPropagation()) {- // TODO(b/268521214): Update once deletion propagation is available.- throw new UnsupportedOperationException("Setting deletion propagation is not "- + "supported on this AppSearch implementation.");- }- if (stringProperty.getJoinableValueType() == AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) { //TODO(b/274986359) Add GMSCore feature check for Joins once available.@@ -144,6 +151,10 @@
.setCardinality(documentProperty.getCardinality()) .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties()) .build();+ } else if (jetpackProperty instanceof AppSearchSchema.EmbeddingPropertyConfig) {+ // TODO(b/326656531): Remove this once embedding search APIs are available.+ throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG+ + " is not available on this AppSearch implementation."); } else { throw new IllegalArgumentException( "Invalid dataType: " + jetpackProperty.getDataType());@@ -160,6 +171,8 @@
com.google.android.gms.appsearch.AppSearchSchema.StringPropertyConfig stringProperty = (com.google.android.gms.appsearch.AppSearchSchema.StringPropertyConfig) gmsProperty;+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform. return new AppSearchSchema.StringPropertyConfig.Builder(stringProperty.getName()) .setCardinality(stringProperty.getCardinality()) .setIndexingType(stringProperty.getIndexingType())@@ -167,24 +180,32 @@
.build(); } else if (gmsProperty instanceof com.google.android.gms.appsearch.AppSearchSchema.LongPropertyConfig) {+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform. return new AppSearchSchema.LongPropertyConfig.Builder( gmsProperty.getName()) .setCardinality(gmsProperty.getCardinality()) .build(); } else if (gmsProperty instanceof com.google.android.gms.appsearch.AppSearchSchema.DoublePropertyConfig) {+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform. return new AppSearchSchema.DoublePropertyConfig.Builder( gmsProperty.getName()) .setCardinality(gmsProperty.getCardinality()) .build(); } else if (gmsProperty instanceof com.google.android.gms.appsearch.AppSearchSchema.BooleanPropertyConfig) {+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform. return new AppSearchSchema.BooleanPropertyConfig.Builder( gmsProperty.getName()) .setCardinality(gmsProperty.getCardinality()) .build(); } else if (gmsProperty instanceof com.google.android.gms.appsearch.AppSearchSchema.BytesPropertyConfig) {+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform. return new AppSearchSchema.BytesPropertyConfig.Builder( gmsProperty.getName()) .setCardinality(gmsProperty.getCardinality())@@ -196,6 +217,8 @@
documentProperty = (com.google.android.gms.appsearch.AppSearchSchema.DocumentPropertyConfig) gmsProperty;+ // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become+ // available in platform. return new AppSearchSchema.DocumentPropertyConfig.Builder( documentProperty.getName(), documentProperty.getSchemaType())@@ -203,6 +226,8 @@
.setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties()) .build(); } else {+ // TODO(b/326656531) : Add an entry for EmbeddingPropertyConfig once it becomes+ // available in gms-appsearch. throw new IllegalArgumentException( "Invalid property type " + gmsProperty.getClass() + ": " + gmsProperty);
@@ -83,6 +83,22 @@
+ "LIST_FILTER_QUERY_LANGUAGE) are not supported with this " + "backend/Android API level combination."); }+ if (jetpackSearchSpec.isListFilterHasPropertyFunctionEnabled()) {+ // TODO(b/309826655): Remove this once the hasProperty function becomes available.+ throw new UnsupportedOperationException(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION+ + " is not available on this AppSearch implementation.");+ }+ if (jetpackSearchSpec.isEmbeddingSearchEnabled()+ || !jetpackSearchSpec.getSearchEmbeddings().isEmpty()) {+ // TODO(b/326656531): Remove this once embedding search APIs are available.+ throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG+ + " is not available on this AppSearch implementation.");+ }+ if (jetpackSearchSpec.isListFilterTokenizeFunctionEnabled()) {+ // TODO(b/332620561): Remove this once 'tokenize' is supported.+ throw new UnsupportedOperationException(Features.LIST_FILTER_TOKENIZE_FUNCTION+ + " is not available on this AppSearch implementation.");+ } } if (!jetpackSearchSpec.getPropertyWeights().isEmpty()) {@@ -104,6 +120,13 @@
+ " is not available on this AppSearch implementation."); }+ if (!jetpackSearchSpec.getInformationalRankingExpressions().isEmpty()) {+ // TODO(b/332642571): Remove this once informational ranking expressions are available.+ throw new UnsupportedOperationException(+ Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS+ + " are not available on this AppSearch implementation.");+ }+ return gmsBuilder.build(); } }
@@ -35,6 +35,12 @@
method public abstract boolean required() default false; }+ @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.EmbeddingProperty {+ method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;+ method public abstract String name() default "";+ method public abstract boolean required() default false;+ }+ @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Id { }@@ -115,11 +121,14 @@
field public static final int RESULT_NOT_FOUND = 6; // 0x6 field public static final int RESULT_OK = 0; // 0x0 field public static final int RESULT_OUT_OF_SPACE = 5; // 0x5+ field public static final int RESULT_RATE_LIMITED = 10; // 0xa field public static final int RESULT_SECURITY_ERROR = 8; // 0x8+ field public static final int RESULT_TIMED_OUT = 11; // 0xb field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1 } public final class AppSearchSchema {+ method public String getDescription(); method public java.util.List getParentTypes(); method public java.util.List getProperties(); method public String getSchemaType();@@ -132,6 +141,7 @@
ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String); method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build(); method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setDescription(String); } public static final class AppSearchSchema.Builder {@@ -139,6 +149,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_PARENT_TYPE) public androidx.appsearch.app.AppSearchSchema.Builder addParentType(String); method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig); method public androidx.appsearch.app.AppSearchSchema build();+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.Builder setDescription(String); } public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {@@ -148,6 +159,7 @@
ctor public AppSearchSchema.BytesPropertyConfig.Builder(String); method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build(); method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setDescription(String); } public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {@@ -164,6 +176,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(java.util.Collection); method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build(); method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setDescription(String); method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean); }@@ -174,6 +187,21 @@
ctor public AppSearchSchema.DoublePropertyConfig.Builder(String); method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build(); method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setDescription(String);+ }++ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public static final class AppSearchSchema.EmbeddingPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {+ method public int getIndexingType();+ field public static final int INDEXING_TYPE_NONE = 0; // 0x0+ field public static final int INDEXING_TYPE_SIMILARITY = 1; // 0x1+ }++ public static final class AppSearchSchema.EmbeddingPropertyConfig.Builder {+ ctor public AppSearchSchema.EmbeddingPropertyConfig.Builder(String);+ method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig build();+ method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setCardinality(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setDescription(String);+ method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setIndexingType(int); } public static final class AppSearchSchema.LongPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {@@ -186,11 +214,13 @@
ctor public AppSearchSchema.LongPropertyConfig.Builder(String); method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig build(); method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setCardinality(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setDescription(String); method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setIndexingType(int); } public abstract static class AppSearchSchema.PropertyConfig { method public int getCardinality();+ method public String getDescription(); method public String getName(); field public static final int CARDINALITY_OPTIONAL = 2; // 0x2 field public static final int CARDINALITY_REPEATED = 1; // 0x1@@ -198,7 +228,6 @@
} public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {- method public boolean getDeletionPropagation(); method public int getIndexingType(); method public int getJoinableValueType(); method public int getTokenizerType();@@ -217,7 +246,7 @@
ctor public AppSearchSchema.StringPropertyConfig.Builder(String); method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build(); method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);- method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DELETION_PROPAGATION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDeletionPropagation(boolean);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDescription(String); method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int); method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setJoinableValueType(int); method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);@@ -248,25 +277,47 @@
method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException; }+ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public final class EmbeddingVector {+ ctor public EmbeddingVector(float[], String);+ method public String getModelSignature();+ method public float[] getValues();+ }++ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ENTERPRISE_GLOBAL_SEARCH_SESSION) public interface EnterpriseGlobalSearchSession {+ method public com.google.common.util.concurrent.ListenableFuture!> getByDocumentIdAsync(String, String, androidx.appsearch.app.GetByDocumentIdRequest);+ method public androidx.appsearch.app.Features getFeatures();+ method public com.google.common.util.concurrent.ListenableFuture getSchemaAsync(String, String);+ method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);+ }+ public interface Features { method public int getMaxIndexedProperties(); method public boolean isFeatureSupported(String); field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";+ field public static final String ENTERPRISE_GLOBAL_SEARCH_SESSION = "ENTERPRISE_GLOBAL_SEARCH_SESSION"; field public static final String GLOBAL_SEARCH_SESSION_GET_BY_ID = "GLOBAL_SEARCH_SESSION_GET_BY_ID"; field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA"; field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK"; field public static final String JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";+ field public static final String LIST_FILTER_HAS_PROPERTY_FUNCTION = "LIST_FILTER_HAS_PROPERTY_FUNCTION"; field public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";+ field public static final String LIST_FILTER_TOKENIZE_FUNCTION = "LIST_FILTER_TOKENIZE_FUNCTION"; field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH"; field public static final String SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES = "SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES"; field public static final String SCHEMA_ADD_PARENT_TYPE = "SCHEMA_ADD_PARENT_TYPE";- field public static final String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";+ field public static final String SCHEMA_EMBEDDING_PROPERTY_CONFIG = "SCHEMA_EMBEDDING_PROPERTY_CONFIG";+ field public static final String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION"; field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";+ field public static final String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";+ field public static final String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS = "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS"; field public static final String SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION = "SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION"; field public static final String SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA = "SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA"; field public static final String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";+ field public static final String SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG = "SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG"; field public static final String SEARCH_SUGGESTION = "SEARCH_SUGGESTION"; field public static final String SET_SCHEMA_CIRCULAR_REFERENCES = "SET_SCHEMA_CIRCULAR_REFERENCES";+ field public static final String SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG = "SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG";+ field public static final String SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE = "SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE"; field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822"; field public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH"; }@@ -287,6 +338,8 @@
method public androidx.appsearch.app.GenericDocument![]? getPropertyDocumentArray(String); method public double getPropertyDouble(String); method public double[]? getPropertyDoubleArray(String);+ method public androidx.appsearch.app.EmbeddingVector? getPropertyEmbedding(String);+ method public androidx.appsearch.app.EmbeddingVector![]? getPropertyEmbeddingArray(String); method public long getPropertyLong(String); method public long[]? getPropertyLongArray(String); method public java.util.Set getPropertyNames();@@ -301,6 +354,7 @@
} public static class GenericDocument.Builder {+ ctor public GenericDocument.Builder(androidx.appsearch.app.GenericDocument); ctor public GenericDocument.Builder(String, String, String); method public androidx.appsearch.app.GenericDocument build(); method public BuilderType clearProperty(String);@@ -311,6 +365,7 @@
method public BuilderType setPropertyBytes(String, byte[]!...); method public BuilderType setPropertyDocument(String, androidx.appsearch.app.GenericDocument!...); method public BuilderType setPropertyDouble(String, double...);+ method public BuilderType setPropertyEmbedding(String, androidx.appsearch.app.EmbeddingVector!...); method public BuilderType setPropertyLong(String, long...); method public BuilderType setPropertyString(String, java.lang.String!...); method public BuilderType setSchemaType(String);@@ -336,8 +391,10 @@
} public final class GetSchemaResponse {+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map getPubliclyVisibleSchemas(); method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map!>!> getRequiredPermissionsForSchemaTypeVisibility(); method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Set getSchemaTypesNotDisplayedBySystem();+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map!> getSchemaTypesVisibleToConfigs(); method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map!> getSchemaTypesVisibleToPackages(); method public java.util.Set getSchemas(); method @IntRange(from=0) public int getVersion();@@ -348,7 +405,9 @@
method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema); method public androidx.appsearch.app.GetSchemaResponse.Builder addSchemaTypeNotDisplayedBySystem(String); method public androidx.appsearch.app.GetSchemaResponse build();+ method public androidx.appsearch.app.GetSchemaResponse.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier); method public androidx.appsearch.app.GetSchemaResponse.Builder setRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set!>);+ method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToConfigs(String, java.util.Set); method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToPackages(String, java.util.Set); method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int); method public androidx.appsearch.app.GetSchemaResponse.Builder setVisibilitySettingSupported(boolean);@@ -423,6 +482,7 @@
public final class PutDocumentsRequest { method public java.util.List getGenericDocuments();+ method public java.util.List getTakenActionGenericDocuments(); } public static final class PutDocumentsRequest.Builder {@@ -431,6 +491,8 @@
method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...); method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection extends androidx.appsearch.app.GenericDocument!>);+ method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(androidx.appsearch.usagereporting.TakenAction!...) throws androidx.appsearch.exceptions.AppSearchException;+ method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(java.util.Collection extends androidx.appsearch.usagereporting.TakenAction!>) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.PutDocumentsRequest build(); }@@ -472,11 +534,28 @@
method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long); }+ public final class SchemaVisibilityConfig {+ method public java.util.List getAllowedPackages();+ method public androidx.appsearch.app.PackageIdentifier? getPubliclyVisibleTargetPackage();+ method public java.util.Set!> getRequiredPermissions();+ }++ public static final class SchemaVisibilityConfig.Builder {+ ctor public SchemaVisibilityConfig.Builder();+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addAllowedPackage(androidx.appsearch.app.PackageIdentifier);+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addRequiredPermissions(java.util.Set);+ method public androidx.appsearch.app.SchemaVisibilityConfig build();+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearAllowedPackages();+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearRequiredPermissions();+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder setPubliclyVisibleTargetPackage(androidx.appsearch.app.PackageIdentifier?);+ }+ public final class SearchResult { method public String getDatabaseName(); method public T getDocument(Class) throws androidx.appsearch.exceptions.AppSearchException; method public T getDocument(Class, java.util.Map!>?) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.GenericDocument getGenericDocument();+ method public java.util.List getInformationalRankingSignals(); method public java.util.List getJoinedResults(); method public java.util.List getMatchInfos(); method public String getPackageName();@@ -485,6 +564,7 @@
public static final class SearchResult.Builder { ctor public SearchResult.Builder(String, String);+ method public androidx.appsearch.app.SearchResult.Builder addInformationalRankingSignal(double); method public androidx.appsearch.app.SearchResult.Builder addJoinedResult(androidx.appsearch.app.SearchResult); method public androidx.appsearch.app.SearchResult.Builder addMatchInfo(androidx.appsearch.app.SearchResult.MatchInfo); method public androidx.appsearch.app.SearchResult build();@@ -526,9 +606,12 @@
public final class SearchSpec { method public String getAdvancedRankingExpression();+ method public int getDefaultEmbeddingSearchMetricType(); method public java.util.List getFilterNamespaces(); method public java.util.List getFilterPackageNames();+ method public java.util.Map!> getFilterProperties(); method public java.util.List getFilterSchemas();+ method public java.util.List getInformationalRankingExpressions(); method public androidx.appsearch.app.JoinSpec? getJoinSpec(); method public int getMaxSnippetSize(); method public int getOrder();@@ -540,18 +623,26 @@
method public int getResultCountPerPage(); method public int getResultGroupingLimit(); method public int getResultGroupingTypeFlags();+ method public java.util.List getSearchEmbeddings();+ method public String? getSearchSourceLogTag(); method public int getSnippetCount(); method public int getSnippetCountPerProperty(); method public int getTermMatch();+ method public boolean isEmbeddingSearchEnabled();+ method public boolean isListFilterHasPropertyFunctionEnabled(); method public boolean isListFilterQueryLanguageEnabled();+ method public boolean isListFilterTokenizeFunctionEnabled(); method public boolean isNumericSearchEnabled(); method public boolean isVerbatimSearchEnabled();+ field public static final int EMBEDDING_SEARCH_METRIC_TYPE_COSINE = 1; // 0x1+ field public static final int EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT = 2; // 0x2+ field public static final int EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN = 3; // 0x3 field public static final int GROUPING_TYPE_PER_NAMESPACE = 2; // 0x2 field public static final int GROUPING_TYPE_PER_PACKAGE = 1; // 0x1 field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA) public static final int GROUPING_TYPE_PER_SCHEMA = 4; // 0x4 field public static final int ORDER_ASCENDING = 1; // 0x1 field public static final int ORDER_DESCENDING = 0; // 0x0- field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";+ field @Deprecated public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*"; field public static final int RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION = 9; // 0x9 field public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; // 0x2 field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1@@ -562,6 +653,7 @@
field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7 field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4 field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5+ field public static final String SCHEMA_TYPE_WILDCARD = "*"; field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1 field public static final int TERM_MATCH_PREFIX = 2; // 0x2 }@@ -574,15 +666,27 @@
method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection); method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...); method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(Class extends java.lang.Object!>, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(String, java.util.Collection);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(Class extends java.lang.Object!>, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(String, java.util.Collection); method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...); method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.lang.String!...);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.util.Collection); method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection); method public androidx.appsearch.app.SearchSpec.Builder addProjectionPaths(String, java.util.Collection); method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class extends java.lang.Object!>, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class extends java.lang.Object!>, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(androidx.appsearch.app.EmbeddingVector!...);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(java.util.Collection); method public androidx.appsearch.app.SearchSpec build();+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setDefaultEmbeddingSearchMetricType(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setEmbeddingSearchEnabled(boolean); method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_HAS_PROPERTY_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterHasPropertyFunctionEnabled(boolean); method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_QUERY_LANGUAGE) public androidx.appsearch.app.SearchSpec.Builder setListFilterQueryLanguageEnabled(boolean);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_TOKENIZE_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterTokenizeFunctionEnabled(boolean); method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int); method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.NUMERIC_SEARCH) public androidx.appsearch.app.SearchSpec.Builder setNumericSearchEnabled(boolean); method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);@@ -594,6 +698,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION) public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(String); method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int); method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG) public androidx.appsearch.app.SearchSpec.Builder setSearchSourceLogTag(String); method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=0x2710) int); method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=0x2710) int); method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);@@ -613,6 +718,7 @@
public final class SearchSuggestionSpec { method public java.util.Map!> getFilterDocumentIds(); method public java.util.List getFilterNamespaces();+ method public java.util.Map!> getFilterProperties(); method public java.util.List getFilterSchemas(); method public int getMaximumResultCount(); method public int getRankingStrategy();@@ -629,6 +735,10 @@
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterDocumentIds(String, java.util.Collection); method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.lang.String!...); method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.util.Collection);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(Class extends java.lang.Object!>, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(String, java.util.Collection);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(Class extends java.lang.Object!>, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(String, java.util.Collection); method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.lang.String!...); method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.util.Collection); method public androidx.appsearch.app.SearchSuggestionSpec build();@@ -637,9 +747,11 @@
public final class SetSchemaRequest { method public java.util.Map getMigrators();+ method public java.util.Map getPubliclyVisibleSchemas(); method public java.util.Map!>!> getRequiredPermissionsForSchemaTypeVisibility(); method public java.util.Set getSchemas(); method public java.util.Set getSchemasNotDisplayedBySystem();+ method public java.util.Map!> getSchemasVisibleToConfigs(); method public java.util.Map!> getSchemasVisibleToPackages(); method @IntRange(from=1) public int getVersion(); method public boolean isForceOverride();@@ -653,26 +765,32 @@
public static final class SetSchemaRequest.Builder { ctor public SetSchemaRequest.Builder();+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClassVisibleToConfig(Class extends java.lang.Object!>, androidx.appsearch.app.SchemaVisibilityConfig) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class extends java.lang.Object!>!...) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection extends java.lang.Class extends java.lang.Object!>!>) throws androidx.appsearch.exceptions.AppSearchException; method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForDocumentClassVisibility(Class extends java.lang.Object!>, java.util.Set) throws androidx.appsearch.exceptions.AppSearchException; method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addSchemaTypeVisibleToConfig(String, androidx.appsearch.app.SchemaVisibilityConfig); method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(androidx.appsearch.app.AppSearchSchema!...); method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection); method public androidx.appsearch.app.SetSchemaRequest build();+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearDocumentClassVisibleToConfigs(Class extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException; method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForDocumentClassVisibility(Class extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException; method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForSchemaTypeVisibility(String);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearSchemaTypeVisibleToConfigs(String); method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class extends java.lang.Object!>, boolean) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class extends java.lang.Object!>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean); method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator); method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleDocumentClass(Class extends java.lang.Object!>, androidx.appsearch.app.PackageIdentifier?) throws androidx.appsearch.exceptions.AppSearchException;+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier?); method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean); method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier); method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int); }- public class SetSchemaResponse {+ public final class SetSchemaResponse { method public java.util.Set getDeletedTypes(); method public java.util.Set getIncompatibleTypes(); method public java.util.Set getMigratedTypes();@@ -700,7 +818,7 @@
method public String getSchemaType(); }- public class StorageInfo {+ public final class StorageInfo { method public int getAliveDocumentsCount(); method public int getAliveNamespacesCount(); method public long getSizeBytes();@@ -771,6 +889,51 @@
}+package androidx.appsearch.usagereporting {++ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) @androidx.appsearch.annotation.Document(name="builtin:ClickAction") public class ClickAction extends androidx.appsearch.usagereporting.TakenAction {+ method public String? getQuery();+ method public String? getReferencedQualifiedId();+ method public int getResultRankGlobal();+ method public int getResultRankInBlock();+ method public long getTimeStayOnResultMillis();+ }++ @androidx.appsearch.annotation.Document.BuilderProducer public static final class ClickAction.Builder {+ ctor public ClickAction.Builder(androidx.appsearch.usagereporting.ClickAction);+ ctor public ClickAction.Builder(String, String, long);+ method public androidx.appsearch.usagereporting.ClickAction build();+ method public androidx.appsearch.usagereporting.ClickAction.Builder setDocumentTtlMillis(long);+ method public androidx.appsearch.usagereporting.ClickAction.Builder setQuery(String?);+ method public androidx.appsearch.usagereporting.ClickAction.Builder setReferencedQualifiedId(String?);+ method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankGlobal(int);+ method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankInBlock(int);+ method public androidx.appsearch.usagereporting.ClickAction.Builder setTimeStayOnResultMillis(long);+ }++ @androidx.appsearch.annotation.Document(name="builtin:SearchAction") public class SearchAction extends androidx.appsearch.usagereporting.TakenAction {+ method public int getFetchedResultCount();+ method public String? getQuery();+ }++ @androidx.appsearch.annotation.Document.BuilderProducer public static final class SearchAction.Builder {+ ctor public SearchAction.Builder(androidx.appsearch.usagereporting.SearchAction);+ ctor public SearchAction.Builder(String, String, long);+ method public androidx.appsearch.usagereporting.SearchAction build();+ method public androidx.appsearch.usagereporting.SearchAction.Builder setDocumentTtlMillis(long);+ method public androidx.appsearch.usagereporting.SearchAction.Builder setFetchedResultCount(int);+ method public androidx.appsearch.usagereporting.SearchAction.Builder setQuery(String?);+ }++ @androidx.appsearch.annotation.Document(name="builtin:TakenAction") public abstract class TakenAction {+ method public long getActionTimestampMillis();+ method public long getDocumentTtlMillis();+ method public String getId();+ method public String getNamespace();+ }++}+ package androidx.appsearch.util { public class DocumentIdUtil {
@@ -35,6 +35,12 @@
method public abstract boolean required() default false; }+ @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.EmbeddingProperty {+ method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;+ method public abstract String name() default "";+ method public abstract boolean required() default false;+ }+ @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Id { }@@ -115,11 +121,14 @@
field public static final int RESULT_NOT_FOUND = 6; // 0x6 field public static final int RESULT_OK = 0; // 0x0 field public static final int RESULT_OUT_OF_SPACE = 5; // 0x5+ field public static final int RESULT_RATE_LIMITED = 10; // 0xa field public static final int RESULT_SECURITY_ERROR = 8; // 0x8+ field public static final int RESULT_TIMED_OUT = 11; // 0xb field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1 } public final class AppSearchSchema {+ method public String getDescription(); method public java.util.List getParentTypes(); method public java.util.List getProperties(); method public String getSchemaType();@@ -132,6 +141,7 @@
ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String); method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build(); method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setDescription(String); } public static final class AppSearchSchema.Builder {@@ -139,6 +149,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_PARENT_TYPE) public androidx.appsearch.app.AppSearchSchema.Builder addParentType(String); method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig); method public androidx.appsearch.app.AppSearchSchema build();+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.Builder setDescription(String); } public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {@@ -148,6 +159,7 @@
ctor public AppSearchSchema.BytesPropertyConfig.Builder(String); method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build(); method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setDescription(String); } public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {@@ -164,6 +176,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(java.util.Collection); method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build(); method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setDescription(String); method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean); }@@ -174,6 +187,21 @@
ctor public AppSearchSchema.DoublePropertyConfig.Builder(String); method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build(); method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setDescription(String);+ }++ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public static final class AppSearchSchema.EmbeddingPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {+ method public int getIndexingType();+ field public static final int INDEXING_TYPE_NONE = 0; // 0x0+ field public static final int INDEXING_TYPE_SIMILARITY = 1; // 0x1+ }++ public static final class AppSearchSchema.EmbeddingPropertyConfig.Builder {+ ctor public AppSearchSchema.EmbeddingPropertyConfig.Builder(String);+ method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig build();+ method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setCardinality(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setDescription(String);+ method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setIndexingType(int); } public static final class AppSearchSchema.LongPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {@@ -186,11 +214,13 @@
ctor public AppSearchSchema.LongPropertyConfig.Builder(String); method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig build(); method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setCardinality(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setDescription(String); method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setIndexingType(int); } public abstract static class AppSearchSchema.PropertyConfig { method public int getCardinality();+ method public String getDescription(); method public String getName(); field public static final int CARDINALITY_OPTIONAL = 2; // 0x2 field public static final int CARDINALITY_REPEATED = 1; // 0x1@@ -198,7 +228,6 @@
} public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {- method public boolean getDeletionPropagation(); method public int getIndexingType(); method public int getJoinableValueType(); method public int getTokenizerType();@@ -217,7 +246,7 @@
ctor public AppSearchSchema.StringPropertyConfig.Builder(String); method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build(); method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);- method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DELETION_PROPAGATION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDeletionPropagation(boolean);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDescription(String); method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int); method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setJoinableValueType(int); method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);@@ -248,25 +277,47 @@
method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException; }+ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public final class EmbeddingVector {+ ctor public EmbeddingVector(float[], String);+ method public String getModelSignature();+ method public float[] getValues();+ }++ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ENTERPRISE_GLOBAL_SEARCH_SESSION) public interface EnterpriseGlobalSearchSession {+ method public com.google.common.util.concurrent.ListenableFuture!> getByDocumentIdAsync(String, String, androidx.appsearch.app.GetByDocumentIdRequest);+ method public androidx.appsearch.app.Features getFeatures();+ method public com.google.common.util.concurrent.ListenableFuture getSchemaAsync(String, String);+ method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);+ }+ public interface Features { method public int getMaxIndexedProperties(); method public boolean isFeatureSupported(String); field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";+ field public static final String ENTERPRISE_GLOBAL_SEARCH_SESSION = "ENTERPRISE_GLOBAL_SEARCH_SESSION"; field public static final String GLOBAL_SEARCH_SESSION_GET_BY_ID = "GLOBAL_SEARCH_SESSION_GET_BY_ID"; field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA"; field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK"; field public static final String JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";+ field public static final String LIST_FILTER_HAS_PROPERTY_FUNCTION = "LIST_FILTER_HAS_PROPERTY_FUNCTION"; field public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";+ field public static final String LIST_FILTER_TOKENIZE_FUNCTION = "LIST_FILTER_TOKENIZE_FUNCTION"; field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH"; field public static final String SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES = "SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES"; field public static final String SCHEMA_ADD_PARENT_TYPE = "SCHEMA_ADD_PARENT_TYPE";- field public static final String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";+ field public static final String SCHEMA_EMBEDDING_PROPERTY_CONFIG = "SCHEMA_EMBEDDING_PROPERTY_CONFIG";+ field public static final String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION"; field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";+ field public static final String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";+ field public static final String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS = "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS"; field public static final String SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION = "SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION"; field public static final String SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA = "SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA"; field public static final String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";+ field public static final String SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG = "SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG"; field public static final String SEARCH_SUGGESTION = "SEARCH_SUGGESTION"; field public static final String SET_SCHEMA_CIRCULAR_REFERENCES = "SET_SCHEMA_CIRCULAR_REFERENCES";+ field public static final String SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG = "SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG";+ field public static final String SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE = "SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE"; field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822"; field public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH"; }@@ -287,6 +338,8 @@
method public androidx.appsearch.app.GenericDocument![]? getPropertyDocumentArray(String); method public double getPropertyDouble(String); method public double[]? getPropertyDoubleArray(String);+ method public androidx.appsearch.app.EmbeddingVector? getPropertyEmbedding(String);+ method public androidx.appsearch.app.EmbeddingVector![]? getPropertyEmbeddingArray(String); method public long getPropertyLong(String); method public long[]? getPropertyLongArray(String); method public java.util.Set getPropertyNames();@@ -301,6 +354,7 @@
} public static class GenericDocument.Builder {+ ctor public GenericDocument.Builder(androidx.appsearch.app.GenericDocument); ctor public GenericDocument.Builder(String, String, String); method public androidx.appsearch.app.GenericDocument build(); method public BuilderType clearProperty(String);@@ -311,6 +365,7 @@
method public BuilderType setPropertyBytes(String, byte[]!...); method public BuilderType setPropertyDocument(String, androidx.appsearch.app.GenericDocument!...); method public BuilderType setPropertyDouble(String, double...);+ method public BuilderType setPropertyEmbedding(String, androidx.appsearch.app.EmbeddingVector!...); method public BuilderType setPropertyLong(String, long...); method public BuilderType setPropertyString(String, java.lang.String!...); method public BuilderType setSchemaType(String);@@ -336,8 +391,10 @@
} public final class GetSchemaResponse {+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map getPubliclyVisibleSchemas(); method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map!>!> getRequiredPermissionsForSchemaTypeVisibility(); method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Set getSchemaTypesNotDisplayedBySystem();+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map!> getSchemaTypesVisibleToConfigs(); method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map!> getSchemaTypesVisibleToPackages(); method public java.util.Set getSchemas(); method @IntRange(from=0) public int getVersion();@@ -348,7 +405,9 @@
method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema); method public androidx.appsearch.app.GetSchemaResponse.Builder addSchemaTypeNotDisplayedBySystem(String); method public androidx.appsearch.app.GetSchemaResponse build();+ method public androidx.appsearch.app.GetSchemaResponse.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier); method public androidx.appsearch.app.GetSchemaResponse.Builder setRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set!>);+ method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToConfigs(String, java.util.Set); method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToPackages(String, java.util.Set); method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int); method public androidx.appsearch.app.GetSchemaResponse.Builder setVisibilitySettingSupported(boolean);@@ -423,6 +482,7 @@
public final class PutDocumentsRequest { method public java.util.List getGenericDocuments();+ method public java.util.List getTakenActionGenericDocuments(); } public static final class PutDocumentsRequest.Builder {@@ -431,6 +491,8 @@
method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...); method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection extends androidx.appsearch.app.GenericDocument!>);+ method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(androidx.appsearch.usagereporting.TakenAction!...) throws androidx.appsearch.exceptions.AppSearchException;+ method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(java.util.Collection extends androidx.appsearch.usagereporting.TakenAction!>) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.PutDocumentsRequest build(); }@@ -472,11 +534,28 @@
method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long); }+ public final class SchemaVisibilityConfig {+ method public java.util.List getAllowedPackages();+ method public androidx.appsearch.app.PackageIdentifier? getPubliclyVisibleTargetPackage();+ method public java.util.Set!> getRequiredPermissions();+ }++ public static final class SchemaVisibilityConfig.Builder {+ ctor public SchemaVisibilityConfig.Builder();+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addAllowedPackage(androidx.appsearch.app.PackageIdentifier);+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addRequiredPermissions(java.util.Set);+ method public androidx.appsearch.app.SchemaVisibilityConfig build();+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearAllowedPackages();+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearRequiredPermissions();+ method public androidx.appsearch.app.SchemaVisibilityConfig.Builder setPubliclyVisibleTargetPackage(androidx.appsearch.app.PackageIdentifier?);+ }+ public final class SearchResult { method public String getDatabaseName(); method public T getDocument(Class) throws androidx.appsearch.exceptions.AppSearchException; method public T getDocument(Class, java.util.Map!>?) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.GenericDocument getGenericDocument();+ method public java.util.List getInformationalRankingSignals(); method public java.util.List getJoinedResults(); method public java.util.List getMatchInfos(); method public String getPackageName();@@ -485,6 +564,7 @@
public static final class SearchResult.Builder { ctor public SearchResult.Builder(String, String);+ method public androidx.appsearch.app.SearchResult.Builder addInformationalRankingSignal(double); method public androidx.appsearch.app.SearchResult.Builder addJoinedResult(androidx.appsearch.app.SearchResult); method public androidx.appsearch.app.SearchResult.Builder addMatchInfo(androidx.appsearch.app.SearchResult.MatchInfo); method public androidx.appsearch.app.SearchResult build();@@ -526,9 +606,12 @@
public final class SearchSpec { method public String getAdvancedRankingExpression();+ method public int getDefaultEmbeddingSearchMetricType(); method public java.util.List getFilterNamespaces(); method public java.util.List getFilterPackageNames();+ method public java.util.Map!> getFilterProperties(); method public java.util.List getFilterSchemas();+ method public java.util.List getInformationalRankingExpressions(); method public androidx.appsearch.app.JoinSpec? getJoinSpec(); method public int getMaxSnippetSize(); method public int getOrder();@@ -540,18 +623,26 @@
method public int getResultCountPerPage(); method public int getResultGroupingLimit(); method public int getResultGroupingTypeFlags();+ method public java.util.List getSearchEmbeddings();+ method public String? getSearchSourceLogTag(); method public int getSnippetCount(); method public int getSnippetCountPerProperty(); method public int getTermMatch();+ method public boolean isEmbeddingSearchEnabled();+ method public boolean isListFilterHasPropertyFunctionEnabled(); method public boolean isListFilterQueryLanguageEnabled();+ method public boolean isListFilterTokenizeFunctionEnabled(); method public boolean isNumericSearchEnabled(); method public boolean isVerbatimSearchEnabled();+ field public static final int EMBEDDING_SEARCH_METRIC_TYPE_COSINE = 1; // 0x1+ field public static final int EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT = 2; // 0x2+ field public static final int EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN = 3; // 0x3 field public static final int GROUPING_TYPE_PER_NAMESPACE = 2; // 0x2 field public static final int GROUPING_TYPE_PER_PACKAGE = 1; // 0x1 field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA) public static final int GROUPING_TYPE_PER_SCHEMA = 4; // 0x4 field public static final int ORDER_ASCENDING = 1; // 0x1 field public static final int ORDER_DESCENDING = 0; // 0x0- field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";+ field @Deprecated public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*"; field public static final int RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION = 9; // 0x9 field public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; // 0x2 field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1@@ -562,6 +653,7 @@
field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7 field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4 field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5+ field public static final String SCHEMA_TYPE_WILDCARD = "*"; field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1 field public static final int TERM_MATCH_PREFIX = 2; // 0x2 }@@ -574,15 +666,27 @@
method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection); method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...); method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(Class extends java.lang.Object!>, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(String, java.util.Collection);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(Class extends java.lang.Object!>, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(String, java.util.Collection); method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...); method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.lang.String!...);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.util.Collection); method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection); method public androidx.appsearch.app.SearchSpec.Builder addProjectionPaths(String, java.util.Collection); method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class extends java.lang.Object!>, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class extends java.lang.Object!>, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(androidx.appsearch.app.EmbeddingVector!...);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(java.util.Collection); method public androidx.appsearch.app.SearchSpec build();+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setDefaultEmbeddingSearchMetricType(int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setEmbeddingSearchEnabled(boolean); method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_HAS_PROPERTY_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterHasPropertyFunctionEnabled(boolean); method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_QUERY_LANGUAGE) public androidx.appsearch.app.SearchSpec.Builder setListFilterQueryLanguageEnabled(boolean);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_TOKENIZE_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterTokenizeFunctionEnabled(boolean); method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int); method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.NUMERIC_SEARCH) public androidx.appsearch.app.SearchSpec.Builder setNumericSearchEnabled(boolean); method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);@@ -594,6 +698,7 @@
method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION) public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(String); method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int); method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG) public androidx.appsearch.app.SearchSpec.Builder setSearchSourceLogTag(String); method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=0x2710) int); method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=0x2710) int); method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);@@ -613,6 +718,7 @@
public final class SearchSuggestionSpec { method public java.util.Map!> getFilterDocumentIds(); method public java.util.List getFilterNamespaces();+ method public java.util.Map!> getFilterProperties(); method public java.util.List getFilterSchemas(); method public int getMaximumResultCount(); method public int getRankingStrategy();@@ -629,6 +735,10 @@
method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterDocumentIds(String, java.util.Collection); method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.lang.String!...); method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.util.Collection);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(Class extends java.lang.Object!>, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(String, java.util.Collection);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(Class extends java.lang.Object!>, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(String, java.util.Collection); method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.lang.String!...); method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.util.Collection); method public androidx.appsearch.app.SearchSuggestionSpec build();@@ -637,9 +747,11 @@
public final class SetSchemaRequest { method public java.util.Map getMigrators();+ method public java.util.Map getPubliclyVisibleSchemas(); method public java.util.Map!>!> getRequiredPermissionsForSchemaTypeVisibility(); method public java.util.Set getSchemas(); method public java.util.Set getSchemasNotDisplayedBySystem();+ method public java.util.Map!> getSchemasVisibleToConfigs(); method public java.util.Map!> getSchemasVisibleToPackages(); method @IntRange(from=1) public int getVersion(); method public boolean isForceOverride();@@ -653,26 +765,32 @@
public static final class SetSchemaRequest.Builder { ctor public SetSchemaRequest.Builder();+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClassVisibleToConfig(Class extends java.lang.Object!>, androidx.appsearch.app.SchemaVisibilityConfig) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class extends java.lang.Object!>!...) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection extends java.lang.Class extends java.lang.Object!>!>) throws androidx.appsearch.exceptions.AppSearchException; method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForDocumentClassVisibility(Class extends java.lang.Object!>, java.util.Set) throws androidx.appsearch.exceptions.AppSearchException; method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addSchemaTypeVisibleToConfig(String, androidx.appsearch.app.SchemaVisibilityConfig); method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(androidx.appsearch.app.AppSearchSchema!...); method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection); method public androidx.appsearch.app.SetSchemaRequest build();+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearDocumentClassVisibleToConfigs(Class extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException; method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForDocumentClassVisibility(Class extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException; method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForSchemaTypeVisibility(String);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearSchemaTypeVisibleToConfigs(String); method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class extends java.lang.Object!>, boolean) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class extends java.lang.Object!>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException; method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean); method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator); method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map);+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleDocumentClass(Class extends java.lang.Object!>, androidx.appsearch.app.PackageIdentifier?) throws androidx.appsearch.exceptions.AppSearchException;+ method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier?); method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean); method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier); method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int); }- public class SetSchemaResponse {+ public final class SetSchemaResponse { method public java.util.Set getDeletedTypes(); method public java.util.Set getIncompatibleTypes(); method public java.util.Set getMigratedTypes();@@ -700,7 +818,7 @@
method public String getSchemaType(); }- public class StorageInfo {+ public final class StorageInfo { method public int getAliveDocumentsCount(); method public int getAliveNamespacesCount(); method public long getSizeBytes();@@ -771,6 +889,51 @@
}+package androidx.appsearch.usagereporting {++ @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) @androidx.appsearch.annotation.Document(name="builtin:ClickAction") public class ClickAction extends androidx.appsearch.usagereporting.TakenAction {+ method public String? getQuery();+ method public String? getReferencedQualifiedId();+ method public int getResultRankGlobal();+ method public int getResultRankInBlock();+ method public long getTimeStayOnResultMillis();+ }++ @androidx.appsearch.annotation.Document.BuilderProducer public static final class ClickAction.Builder {+ ctor public ClickAction.Builder(androidx.appsearch.usagereporting.ClickAction);+ ctor public ClickAction.Builder(String, String, long);+ method public androidx.appsearch.usagereporting.ClickAction build();+ method public androidx.appsearch.usagereporting.ClickAction.Builder setDocumentTtlMillis(long);+ method public androidx.appsearch.usagereporting.ClickAction.Builder setQuery(String?);+ method public androidx.appsearch.usagereporting.ClickAction.Builder setReferencedQualifiedId(String?);+ method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankGlobal(int);+ method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankInBlock(int);+ method public androidx.appsearch.usagereporting.ClickAction.Builder setTimeStayOnResultMillis(long);+ }++ @androidx.appsearch.annotation.Document(name="builtin:SearchAction") public class SearchAction extends androidx.appsearch.usagereporting.TakenAction {+ method public int getFetchedResultCount();+ method public String? getQuery();+ }++ @androidx.appsearch.annotation.Document.BuilderProducer public static final class SearchAction.Builder {+ ctor public SearchAction.Builder(androidx.appsearch.usagereporting.SearchAction);+ ctor public SearchAction.Builder(String, String, long);+ method public androidx.appsearch.usagereporting.SearchAction build();+ method public androidx.appsearch.usagereporting.SearchAction.Builder setDocumentTtlMillis(long);+ method public androidx.appsearch.usagereporting.SearchAction.Builder setFetchedResultCount(int);+ method public androidx.appsearch.usagereporting.SearchAction.Builder setQuery(String?);+ }++ @androidx.appsearch.annotation.Document(name="builtin:TakenAction") public abstract class TakenAction {+ method public long getActionTimestampMillis();+ method public long getDocumentTtlMillis();+ method public String getId();+ method public String getNamespace();+ }++}+ package androidx.appsearch.util { public class DocumentIdUtil {
@@ -1,269 +0,0 @@
-/*- * Copyright 2023 The Android Open Source Project- *- * Licensed under the Apache License, Version 2.0 (the "License");- * you may not use this file except in compliance with the License.- * You may obtain a copy of the License at- *- * http://www.apache.org/licenses/LICENSE-2.0- *- * Unless required by applicable law or agreed to in writing, software- * distributed under the License is distributed on an "AS IS" BASIS,- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.- * See the License for the specific language governing permissions and- * limitations under the License.- */--package androidx.appsearch.app;--import static com.google.common.truth.Truth.assertThat;--import androidx.appsearch.testutil.AppSearchEmail;--import org.junit.Test;--import java.util.List;--/** Tests for private APIs of {@link AppSearchSchema}. */-public class AppSearchSchemaInternalTest {- // TODO(b/291122592): move to CTS once the APIs it uses are public- @Test- public void testParentTypes() {- AppSearchSchema schema =- new AppSearchSchema.Builder("EmailMessage")- .addParentType("Email")- .addParentType("Message")- .build();- assertThat(schema.getParentTypes()).containsExactly("Email", "Message");- }-- // TODO(b/291122592): move to CTS once the APIs it uses are public- @Test- public void testDuplicateParentTypes() {- AppSearchSchema schema =- new AppSearchSchema.Builder("EmailMessage")- .addParentType("Email")- .addParentType("Message")- .addParentType("Email")- .build();- assertThat(schema.getParentTypes()).containsExactly("Email", "Message");- }-- // TODO(b/291122592): move to CTS once the APIs it uses are public- @Test- public void testDocumentPropertyConfig_indexableNestedPropertyStrings() {- AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =- new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")- .addIndexableNestedProperties("prop1", "prop2", "prop1.prop2")- .build();- assertThat(documentPropertyConfig.getIndexableNestedProperties())- .containsExactly("prop1", "prop2", "prop1.prop2");- }-- // TODO(b/291122592): move to CTS once the APIs it uses are public- @Test- public void testDocumentPropertyConfig_indexableNestedPropertyPropertyPaths() {- AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =- new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")- .addIndexableNestedPropertyPaths(- new PropertyPath("prop1"), new PropertyPath("prop1.prop2"))- .build();- assertThat(documentPropertyConfig.getIndexableNestedProperties())- .containsExactly("prop1", "prop1.prop2");- }-- // TODO(b/291122592): move to CTS once the APIs it uses are public- @Test- public void testDocumentPropertyConfig_indexableNestedPropertyProperty_duplicatePaths() {- AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =- new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")- .addIndexableNestedPropertyPaths(- new PropertyPath("prop1"), new PropertyPath("prop1.prop2"))- .addIndexableNestedProperties("prop1")- .build();- assertThat(documentPropertyConfig.getIndexableNestedProperties())- .containsExactly("prop1", "prop1.prop2");- }-- // TODO(b/291122592): move to CTS once the APIs it uses are public- @Test- public void testDocumentPropertyConfig_reusingBuilderDoesNotAffectPreviouslyBuiltConfigs() {- AppSearchSchema.DocumentPropertyConfig.Builder builder =- new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")- .addIndexableNestedProperties("prop1");- AppSearchSchema.DocumentPropertyConfig config1 = builder.build();- assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");-- builder.addIndexableNestedProperties("prop2");- AppSearchSchema.DocumentPropertyConfig config2 = builder.build();- assertThat(config2.getIndexableNestedProperties()).containsExactly("prop1", "prop2");- assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");-- builder.addIndexableNestedPropertyPaths(new PropertyPath("prop3"));- AppSearchSchema.DocumentPropertyConfig config3 = builder.build();- assertThat(config3.getIndexableNestedProperties())- .containsExactly("prop1", "prop2", "prop3");- assertThat(config2.getIndexableNestedProperties()).containsExactly("prop1", "prop2");- assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");- }-- // TODO(b/291122592): move to CTS once the APIs it uses are public- @Test- public void testPropertyConfig() {- AppSearchSchema schema =- new AppSearchSchema.Builder("Test")- .addProperty(- new AppSearchSchema.StringPropertyConfig.Builder("string")- .setCardinality(- AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)- .setIndexingType(- AppSearchSchema.StringPropertyConfig- .INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(- AppSearchSchema.StringPropertyConfig- .TOKENIZER_TYPE_PLAIN)- .build())- .addProperty(- new AppSearchSchema.LongPropertyConfig.Builder("long")- .setCardinality(- AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)- .setIndexingType(- AppSearchSchema.LongPropertyConfig- .INDEXING_TYPE_NONE)- .build())- .addProperty(- new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")- .setCardinality(- AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)- .setIndexingType(- AppSearchSchema.LongPropertyConfig- .INDEXING_TYPE_RANGE)- .build())- .addProperty(- new AppSearchSchema.DoublePropertyConfig.Builder("double")- .setCardinality(- AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)- .build())- .addProperty(- new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")- .setCardinality(- AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)- .build())- .addProperty(- new AppSearchSchema.BytesPropertyConfig.Builder("bytes")- .setCardinality(- AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)- .build())- .addProperty(- new AppSearchSchema.DocumentPropertyConfig.Builder(- "document1", AppSearchEmail.SCHEMA_TYPE)- .setCardinality(- AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)- .setShouldIndexNestedProperties(true)- .build())- .addProperty(- new AppSearchSchema.DocumentPropertyConfig.Builder(- "document2", AppSearchEmail.SCHEMA_TYPE)- .setCardinality(- AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)- .setShouldIndexNestedProperties(false)- .addIndexableNestedProperties("path1", "path2", "path3")- .build())- .addProperty(- new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId1")- .setCardinality(- AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)- .setJoinableValueType(- AppSearchSchema.StringPropertyConfig- .JOINABLE_VALUE_TYPE_QUALIFIED_ID)- .build())- .addProperty(- new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId2")- .setCardinality(- AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)- .setJoinableValueType(- AppSearchSchema.StringPropertyConfig- .JOINABLE_VALUE_TYPE_QUALIFIED_ID)- .build())- .build();-- assertThat(schema.getSchemaType()).isEqualTo("Test");- List properties = schema.getProperties();- assertThat(properties).hasSize(10);-- assertThat(properties.get(0).getName()).isEqualTo("string");- assertThat(properties.get(0).getCardinality())- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);- assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getIndexingType())- .isEqualTo(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);- assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getTokenizerType())- .isEqualTo(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN);-- assertThat(properties.get(1).getName()).isEqualTo("long");- assertThat(properties.get(1).getCardinality())- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);- assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(1)).getIndexingType())- .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE);-- assertThat(properties.get(2).getName()).isEqualTo("indexableLong");- assertThat(properties.get(2).getCardinality())- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);- assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(2)).getIndexingType())- .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE);-- assertThat(properties.get(3).getName()).isEqualTo("double");- assertThat(properties.get(3).getCardinality())- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);- assertThat(properties.get(3)).isInstanceOf(AppSearchSchema.DoublePropertyConfig.class);-- assertThat(properties.get(4).getName()).isEqualTo("boolean");- assertThat(properties.get(4).getCardinality())- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);- assertThat(properties.get(4)).isInstanceOf(AppSearchSchema.BooleanPropertyConfig.class);-- assertThat(properties.get(5).getName()).isEqualTo("bytes");- assertThat(properties.get(5).getCardinality())- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);- assertThat(properties.get(5)).isInstanceOf(AppSearchSchema.BytesPropertyConfig.class);-- assertThat(properties.get(6).getName()).isEqualTo("document1");- assertThat(properties.get(6).getCardinality())- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);- assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(6)).getSchemaType())- .isEqualTo(AppSearchEmail.SCHEMA_TYPE);- assertThat(- ((AppSearchSchema.DocumentPropertyConfig) properties.get(6))- .shouldIndexNestedProperties())- .isEqualTo(true);-- assertThat(properties.get(7).getName()).isEqualTo("document2");- assertThat(properties.get(7).getCardinality())- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);- assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(7)).getSchemaType())- .isEqualTo(AppSearchEmail.SCHEMA_TYPE);- assertThat(- ((AppSearchSchema.DocumentPropertyConfig) properties.get(7))- .shouldIndexNestedProperties())- .isEqualTo(false);- assertThat(- ((AppSearchSchema.DocumentPropertyConfig) properties.get(7))- .getIndexableNestedProperties())- .containsExactly("path1", "path2", "path3");-- assertThat(properties.get(8).getName()).isEqualTo("qualifiedId1");- assertThat(properties.get(8).getCardinality())- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);- assertThat(- ((AppSearchSchema.StringPropertyConfig) properties.get(8))- .getJoinableValueType())- .isEqualTo(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);-- assertThat(properties.get(9).getName()).isEqualTo("qualifiedId2");- assertThat(properties.get(9).getCardinality())- .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);- assertThat(- ((AppSearchSchema.StringPropertyConfig) properties.get(9))- .getJoinableValueType())- .isEqualTo(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);- }-}
@@ -21,16 +21,12 @@
import static com.google.common.truth.Truth.assertThat;-import static org.junit.Assert.assertThrows;-import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; import androidx.annotation.NonNull; import androidx.appsearch.app.AppSearchSchema.PropertyConfig; import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig; import androidx.appsearch.testutil.AppSearchEmail;-import androidx.appsearch.util.DocumentIdUtil;-import androidx.test.core.app.ApplicationProvider; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture;@@ -46,6 +42,8 @@
import java.util.Set; import java.util.concurrent.ExecutorService;++/** This class holds all tests that won't be exported to the framework. */ public abstract class AppSearchSessionInternalTestBase { static final String DB_NAME_1 = "";@@ -77,730 +75,10 @@
mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get(); }- // TODO(b/228240987) delete this test when we support property restrict for multiple terms- @Test- public void testSearchSuggestion_propertyFilter() throws Exception {- // Schema registration- AppSearchSchema schemaType1 =- new AppSearchSchema.Builder("Type1")- .addProperty(- new StringPropertyConfig.Builder("propertyone")- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_PREFIXES)- .build())- .addProperty(- new StringPropertyConfig.Builder("propertytwo")- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_PREFIXES)- .build())- .build();- AppSearchSchema schemaType2 =- new AppSearchSchema.Builder("Type2")- .addProperty(- new StringPropertyConfig.Builder("propertythree")- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_PREFIXES)- .build())- .addProperty(- new StringPropertyConfig.Builder("propertyfour")- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_PREFIXES)- .build())- .build();- mDb1.setSchemaAsync(- new SetSchemaRequest.Builder().addSchemas(schemaType1, schemaType2).build())- .get();-- // Index documents- GenericDocument doc1 =- new GenericDocument.Builder<>("namespace", "id1", "Type1")- .setPropertyString("propertyone", "termone")- .setPropertyString("propertytwo", "termtwo")- .build();- GenericDocument doc2 =- new GenericDocument.Builder<>("namespace", "id2", "Type2")- .setPropertyString("propertythree", "termthree")- .setPropertyString("propertyfour", "termfour")- .build();-- checkIsBatchResultSuccess(- mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));-- SearchSuggestionResult resultOne =- new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();- SearchSuggestionResult resultTwo =- new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();- SearchSuggestionResult resultThree =- new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build();- SearchSuggestionResult resultFour =- new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build();-- // Only search for type1/propertyone- List suggestions =- mDb1.searchSuggestionAsync(- /* suggestionQueryExpression= */ "t",- new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)- .addFilterSchemas("Type1")- .addFilterProperties(- "Type1", ImmutableList.of("propertyone"))- .build())- .get();- assertThat(suggestions).containsExactly(resultOne);-- // Only search for type1/propertyone and type1/propertytwo- suggestions =- mDb1.searchSuggestionAsync(- /* suggestionQueryExpression= */ "t",- new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)- .addFilterSchemas("Type1")- .addFilterProperties(- "Type1",- ImmutableList.of("propertyone", "propertytwo"))- .build())- .get();- assertThat(suggestions).containsExactly(resultOne, resultTwo);-- // Only search for type1/propertyone and type2/propertythree- suggestions =- mDb1.searchSuggestionAsync(- /* suggestionQueryExpression= */ "t",- new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)- .addFilterSchemas("Type1", "Type2")- .addFilterProperties(- "Type1", ImmutableList.of("propertyone"))- .addFilterProperties(- "Type2", ImmutableList.of("propertythree"))- .build())- .get();- assertThat(suggestions).containsExactly(resultOne, resultThree);-- // Only search for type1/propertyone and type2/propertyfour, in addFilterPropertyPaths- suggestions =- mDb1.searchSuggestionAsync(- /* suggestionQueryExpression= */ "t",- new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)- .addFilterSchemas("Type1", "Type2")- .addFilterProperties(- "Type1", ImmutableList.of("propertyone"))- .addFilterPropertyPaths(- "Type2",- ImmutableList.of(new PropertyPath("propertyfour")))- .build())- .get();- assertThat(suggestions).containsExactly(resultOne, resultFour);-- // Only search for type1/propertyone and everything in type2- suggestions =- mDb1.searchSuggestionAsync(- /* suggestionQueryExpression= */ "t",- new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)- .addFilterProperties(- "Type1", ImmutableList.of("propertyone"))- .build())- .get();- assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);- }-- // TODO(b/296088047): move to CTS once the APIs it uses are public- @Test- public void testQuery_typePropertyFilters() throws Exception {- assumeTrue(mDb1.getFeatures().isFeatureSupported(- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));- // Schema registration- mDb1.setSchemaAsync(- new SetSchemaRequest.Builder()- .addSchemas(AppSearchEmail.SCHEMA)- .build()).get();-- // Index two documents- AppSearchEmail email1 =- new AppSearchEmail.Builder("namespace", "id1")- .setCreationTimestampMillis(1000)- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();- AppSearchEmail email2 =- new AppSearchEmail.Builder("namespace", "id2")- .setCreationTimestampMillis(1000)- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example subject with some body")- .setBody("This is the body of the testPut email")- .build();- checkIsBatchResultSuccess(mDb1.putAsync(- new PutDocumentsRequest.Builder()- .addGenericDocuments(email1, email2).build()));-- // Query with type property filters {"Email", ["subject", "to"]}- SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))- .build());- List documents = convertSearchResultsToDocuments(searchResults);- // Only email2 should be returned because email1 doesn't have the term "body" in subject- // or to fields- assertThat(documents).containsExactly(email2);- }-- // TODO(b/296088047): move to CTS once the APIs it uses are public- @Test- public void testQuery_typePropertyFiltersWithDifferentSchemaTypes() throws Exception {- assumeTrue(mDb1.getFeatures().isFeatureSupported(- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));- // Schema registration- mDb1.setSchemaAsync(- new SetSchemaRequest.Builder()- .addSchemas(AppSearchEmail.SCHEMA)- .addSchemas(new AppSearchSchema.Builder("Note")- .addProperty(new StringPropertyConfig.Builder("title")- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build())- .addProperty(new StringPropertyConfig.Builder("body")- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build())- .build())- .build()).get();-- // Index two documents- AppSearchEmail email =- new AppSearchEmail.Builder("namespace", "id1")- .setCreationTimestampMillis(1000)- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();- GenericDocument note =- new GenericDocument.Builder<>("namespace", "id2", "Note")- .setCreationTimestampMillis(1000)- .setPropertyString("title", "Note title")- .setPropertyString("body", "Note body").build();- checkIsBatchResultSuccess(mDb1.putAsync(- new PutDocumentsRequest.Builder()- .addGenericDocuments(email, note).build()));-- // Query with type property paths {"Email": ["subject", "to"], "Note": ["body"]}. Note- // schema has body in its property filter but Email schema doesn't.- SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))- .addFilterProperties("Note", ImmutableList.of("body"))- .build());- List documents = convertSearchResultsToDocuments(searchResults);- // Only the note document should be returned because the email property filter doesn't- // allow searching in the body.- assertThat(documents).containsExactly(note);- }-- // TODO(b/296088047): move to CTS once the APIs it uses are public- @Test- public void testQuery_typePropertyFiltersWithWildcard() throws Exception {- assumeTrue(mDb1.getFeatures().isFeatureSupported(- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));- // Schema registration- mDb1.setSchemaAsync(- new SetSchemaRequest.Builder()- .addSchemas(AppSearchEmail.SCHEMA)- .addSchemas(new AppSearchSchema.Builder("Note")- .addProperty(new StringPropertyConfig.Builder("title")- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build())- .addProperty(new StringPropertyConfig.Builder("body")- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build())- .build())- .build()).get();-- // Index two documents- AppSearchEmail email =- new AppSearchEmail.Builder("namespace", "id1")- .setCreationTimestampMillis(1000)- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example subject with some body")- .setBody("This is the body of the testPut email")- .build();- GenericDocument note =- new GenericDocument.Builder<>("namespace", "id2", "Note")- .setCreationTimestampMillis(1000)- .setPropertyString("title", "Note title")- .setPropertyString("body", "Note body").build();- checkIsBatchResultSuccess(mDb1.putAsync(- new PutDocumentsRequest.Builder()- .addGenericDocuments(email, note).build()));-- // Query with type property paths {"*": ["subject", "title"]}- SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,- ImmutableList.of("subject", "title"))- .build());- List documents = convertSearchResultsToDocuments(searchResults);- // The wildcard property filter will apply to both the Email and Note schema. The email- // document should be returned since it has the term "body" in its subject property. The- // note document should not be returned since it doesn't have the term "body" in the title- // property (subject property is not applicable for Note schema)- assertThat(documents).containsExactly(email);- }-- // TODO(b/296088047): move to CTS once the APIs it uses are public- @Test- public void testQuery_typePropertyFiltersWithWildcardAndExplicitSchema() throws Exception {- assumeTrue(mDb1.getFeatures().isFeatureSupported(- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));- // Schema registration- mDb1.setSchemaAsync(- new SetSchemaRequest.Builder()- .addSchemas(AppSearchEmail.SCHEMA)- .addSchemas(new AppSearchSchema.Builder("Note")- .addProperty(new StringPropertyConfig.Builder("title")- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build())- .addProperty(new StringPropertyConfig.Builder("body")- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build())- .build())- .build()).get();-- // Index two documents- AppSearchEmail email =- new AppSearchEmail.Builder("namespace", "id1")- .setCreationTimestampMillis(1000)- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example subject with some body")- .setBody("This is the body of the testPut email")- .build();- GenericDocument note =- new GenericDocument.Builder<>("namespace", "id2", "Note")- .setCreationTimestampMillis(1000)- .setPropertyString("title", "Note title")- .setPropertyString("body", "Note body").build();- checkIsBatchResultSuccess(mDb1.putAsync(- new PutDocumentsRequest.Builder()- .addGenericDocuments(email, note).build()));-- // Query with type property paths {"*": ["subject", "title"], "Note": ["body"]}- SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,- ImmutableList.of("subject", "title"))- .addFilterProperties("Note", ImmutableList.of("body"))- .build());- List documents = convertSearchResultsToDocuments(searchResults);- // The wildcard property filter will only apply to the Email schema since Note schema has- // its own explicit property filter specified. The email document should be returned since- // it has the term "body" in its subject property. The note document should also be returned- // since it has the term "body" in the body property.- assertThat(documents).containsExactly(email, note);- }-- // TODO(b/296088047): move to CTS once the APIs it uses are public- @Test- public void testQuery_typePropertyFiltersNonExistentType() throws Exception {- assumeTrue(mDb1.getFeatures().isFeatureSupported(- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));- // Schema registration- mDb1.setSchemaAsync(- new SetSchemaRequest.Builder()- .addSchemas(AppSearchEmail.SCHEMA)- .addSchemas(new AppSearchSchema.Builder("Note")- .addProperty(new StringPropertyConfig.Builder("title")- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build())- .addProperty(new StringPropertyConfig.Builder("body")- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build())- .build())- .build()).get();-- // Index two documents- AppSearchEmail email =- new AppSearchEmail.Builder("namespace", "id1")- .setCreationTimestampMillis(1000)- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example subject with some body")- .setBody("This is the body of the testPut email")- .build();- GenericDocument note =- new GenericDocument.Builder<>("namespace", "id2", "Note")- .setCreationTimestampMillis(1000)- .setPropertyString("title", "Note title")- .setPropertyString("body", "Note body").build();- checkIsBatchResultSuccess(mDb1.putAsync(- new PutDocumentsRequest.Builder()- .addGenericDocuments(email, note).build()));-- // Query with type property paths {"NonExistentType": ["to", "title"]}- SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .addFilterProperties("NonExistentType", ImmutableList.of("to", "title"))- .build());- List documents = convertSearchResultsToDocuments(searchResults);- // The supplied property filters don't apply to either schema types. Both the documents- // should be returned since the term "body" is present in at least one of their properties.- assertThat(documents).containsExactly(email, note);- }-- // TODO(b/296088047): move to CTS once the APIs it uses are public- @Test- public void testQuery_typePropertyFiltersEmpty() throws Exception {- assumeTrue(mDb1.getFeatures().isFeatureSupported(- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));- // Schema registration- mDb1.setSchemaAsync(- new SetSchemaRequest.Builder()- .addSchemas(AppSearchEmail.SCHEMA)- .addSchemas(new AppSearchSchema.Builder("Note")- .addProperty(new StringPropertyConfig.Builder("title")- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build())- .addProperty(new StringPropertyConfig.Builder("body")- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(- StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build())- .build())- .build()).get();-- // Index two documents- AppSearchEmail email =- new AppSearchEmail.Builder("namespace", "id1")- .setCreationTimestampMillis(1000)- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();- GenericDocument note =- new GenericDocument.Builder<>("namespace", "id2", "Note")- .setCreationTimestampMillis(1000)- .setPropertyString("title", "Note title")- .setPropertyString("body", "Note body").build();- checkIsBatchResultSuccess(mDb1.putAsync(- new PutDocumentsRequest.Builder()- .addGenericDocuments(email, note).build()));-- // Query with type property paths {"email": []}- SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())- .build());- List documents = convertSearchResultsToDocuments(searchResults);- // The email document should not be returned since the property filter doesn't allow- // searching any property.- assertThat(documents).containsExactly(note);- }-- // TODO(b/296088047): move to CTS once the APIs it uses are public- @Test- public void testQueryWithJoin_typePropertyFiltersOnNestedSpec() throws Exception {- assumeTrue(mDb1.getFeatures().isFeatureSupported(- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));- assumeTrue(mDb1.getFeatures()- .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));-- // A full example of how join might be used with property filters in join spec- AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")- .addProperty(new StringPropertyConfig.Builder("entityId")- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setJoinableValueType(StringPropertyConfig- .JOINABLE_VALUE_TYPE_QUALIFIED_ID)- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build()- ).addProperty(new StringPropertyConfig.Builder("note")- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build()- ).addProperty(new StringPropertyConfig.Builder("viewType")- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build()- ).build();-- // Schema registration- mDb1.setSchemaAsync(- new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)- .build()).get();-- // Index 2 email documents- AppSearchEmail inEmail =- new AppSearchEmail.Builder("namespace", "id1")- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();-- AppSearchEmail inEmail2 =- new AppSearchEmail.Builder("namespace", "id2")- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();-- // Index 2 viewAction documents, one for email1 and the other for email2- String qualifiedId1 =- DocumentIdUtil.createQualifiedId(- ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,- "namespace", "id1");- String qualifiedId2 =- DocumentIdUtil.createQualifiedId(- ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,- "namespace", "id2");- GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")- .setPropertyString("entityId", qualifiedId1)- .setPropertyString("note", "Viewed email on Monday")- .setPropertyString("viewType", "Stared").build();- GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")- .setPropertyString("entityId", qualifiedId2)- .setPropertyString("note", "Viewed email on Tuesday")- .setPropertyString("viewType", "Viewed").build();- checkIsBatchResultSuccess(mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,- viewAction1, viewAction2)- .build()));-- // The nested search spec only allows searching the viewType property for viewAction- // schema type. It also specifies a property filter for Email schema.- SearchSpec nestedSearchSpec =- new SearchSpec.Builder()- .addFilterProperties("ViewAction", ImmutableList.of("viewType"))- .addFilterProperties(AppSearchEmail.SCHEMA_TYPE,- ImmutableList.of("subject"))- .build();-- // Search for the term "Viewed" in join spec- JoinSpec js = new JoinSpec.Builder("entityId")- .setNestedSearch("Viewed", nestedSearchSpec)- .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)- .build();-- SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()- .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)- .setJoinSpec(js)- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .build());-- List sr = searchResults.getNextPageAsync().get();-- // Both email docs are returned, email2 comes first because it has higher number of- // joined documents. The property filters for Email schema specified in the nested search- // specs don't apply to the outer query (otherwise none of the email documents would have- // been returned).- assertThat(sr).hasSize(2);-- // Email2 has a viewAction document viewAction2 that satisfies the property filters in- // the join spec, so it should be present in the joined results.- assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");- assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);- assertThat(sr.get(0).getJoinedResults()).hasSize(1);- assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);-- // Email1 has a viewAction document viewAction1 but it doesn't satisfy the property filters- // in the join spec, so it should not be present in the joined results.- assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");- assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);- assertThat(sr.get(1).getJoinedResults()).isEmpty();- }-- // TODO(b/296088047): move to CTS once the APIs it uses are public- @Test- public void testQueryWithJoin_typePropertyFiltersOnOuterSpec() throws Exception {- assumeTrue(mDb1.getFeatures().isFeatureSupported(- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));- assumeTrue(mDb1.getFeatures()- .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));-- // A full example of how join might be used with property filters in join spec- AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")- .addProperty(new StringPropertyConfig.Builder("entityId")- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setJoinableValueType(StringPropertyConfig- .JOINABLE_VALUE_TYPE_QUALIFIED_ID)- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build()- ).addProperty(new StringPropertyConfig.Builder("note")- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build()- ).addProperty(new StringPropertyConfig.Builder("viewType")- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)- .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .build()- ).build();-- // Schema registration- mDb1.setSchemaAsync(- new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)- .build()).get();-- // Index 2 email documents- AppSearchEmail inEmail =- new AppSearchEmail.Builder("namespace", "id1")- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();-- AppSearchEmail inEmail2 =- new AppSearchEmail.Builder("namespace", "id2")- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();-- // Index 2 viewAction documents, one for email1 and the other for email2- String qualifiedId1 =- DocumentIdUtil.createQualifiedId(- ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,- "namespace", "id1");- String qualifiedId2 =- DocumentIdUtil.createQualifiedId(- ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,- "namespace", "id2");- GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")- .setPropertyString("entityId", qualifiedId1)- .setPropertyString("note", "Viewed email on Monday")- .setPropertyString("viewType", "Stared").build();- GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")- .setPropertyString("entityId", qualifiedId2)- .setPropertyString("note", "Viewed email on Tuesday")- .setPropertyString("viewType", "Viewed").build();- checkIsBatchResultSuccess(mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,- viewAction1, viewAction2)- .build()));-- // The nested search spec doesn't specify any property filters.- SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();-- // Search for the term "Viewed" in join spec- JoinSpec js = new JoinSpec.Builder("entityId")- .setNestedSearch("Viewed", nestedSearchSpec)- .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)- .build();-- // Outer search spec adds property filters for both Email and ViewAction schema- SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()- .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)- .setJoinSpec(js)- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body"))- .addFilterProperties("ViewAction", ImmutableList.of("viewType"))- .build());-- List sr = searchResults.getNextPageAsync().get();-- // Both email docs are returned as they both satisfy the property filters for Email, email2- // comes first because it has higher id lexicographically.- assertThat(sr).hasSize(2);-- // Email2 has a viewAction document viewAction2 that satisfies the property filters in- // the outer spec (although those property filters are irrelevant for joined documents),- // it should be present in the joined results.- assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");- assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);- assertThat(sr.get(0).getJoinedResults()).hasSize(1);- assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);-- // Email1 has a viewAction document viewAction1 that doesn't satisfy the property filters- // in the outer spec, but property filters in the outer spec should not apply on joined- // documents, so viewAction1 should be present in the joined results.- assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");- assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);- assertThat(sr.get(0).getJoinedResults()).hasSize(1);- assertThat(sr.get(1).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);- }-- // TODO(b/296088047): move to CTS once the APIs it uses are public- @Test- public void testQuery_typePropertyFiltersNotSupported() throws Exception {- assumeFalse(mDb1.getFeatures().isFeatureSupported(- Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));- // Schema registration- mDb1.setSchemaAsync(- new SetSchemaRequest.Builder()- .addSchemas(AppSearchEmail.SCHEMA)- .build()).get();-- // Query with type property filters {"Email", ["subject", "to"]} and verify that unsupported- // exception is thrown- SearchSpec searchSpec = new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))- .build();- UnsupportedOperationException exception =- assertThrows(UnsupportedOperationException.class,- () -> mDb1.search("body", searchSpec));- assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES- + " is not available on this AppSearch implementation.");- }- // TODO(b/268521214): Move test to cts once deletion propagation is available in framework. @Test public void testGetSchema_joinableValueType() throws Exception { assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));- assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DELETION_PROPAGATION)); AppSearchSchema inSchema = new AppSearchSchema.Builder("Test") .addProperty(@@ -820,11 +98,6 @@
.setJoinableValueType( StringPropertyConfig .JOINABLE_VALUE_TYPE_QUALIFIED_ID)- // TODO(b/274157614): Export this to framework when we- // can access hidden APIs.- // @exportToFramework:startStrip()- .setDeletionPropagation(true)- // @exportToFramework:endStrip() .build()) .build();@@ -837,465 +110,6 @@
assertThat(actual).containsExactlyElementsIn(request.getSchemas()); }- // TODO(b/268521214): Move test to cts once deletion propagation is available in framework.- @Test- public void testGetSchema_deletionPropagation_unsupported() {- assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));- assumeFalse(- mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DELETION_PROPAGATION));- AppSearchSchema schema =- new AppSearchSchema.Builder("Test")- .addProperty(- new StringPropertyConfig.Builder("qualifiedIdDeletionPropagation")- .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)- .setJoinableValueType(- StringPropertyConfig- .JOINABLE_VALUE_TYPE_QUALIFIED_ID)- .setDeletionPropagation(true)- .build())- .build();- SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(schema).build();- Exception e =- assertThrows(- UnsupportedOperationException.class,- () -> mDb1.setSchemaAsync(request).get());- assertThat(e.getMessage())- .isEqualTo(- "Setting deletion propagation is not supported "- + "on this AppSearch implementation.");- }-- // TODO(b/291122592): move to CTS once the APIs it uses are public- @Test- public void testQuery_ResultGroupingLimits_SchemaGroupingSupported() throws Exception {- assumeTrue(- mDb1.getFeatures()- .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));- // Schema registration- AppSearchSchema genericSchema =- new AppSearchSchema.Builder("Generic")- .addProperty(- new StringPropertyConfig.Builder("foo")- .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)- .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)- .setIndexingType(- StringPropertyConfig.INDEXING_TYPE_PREFIXES)- .build())- .build();- mDb1.setSchemaAsync(- new SetSchemaRequest.Builder()- .addSchemas(AppSearchEmail.SCHEMA)- .addSchemas(genericSchema)- .build())- .get();-- // Index four documents.- AppSearchEmail inEmail1 =- new AppSearchEmail.Builder("namespace1", "id1")- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();- checkIsBatchResultSuccess(- mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));- AppSearchEmail inEmail2 =- new AppSearchEmail.Builder("namespace1", "id2")- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();- checkIsBatchResultSuccess(- mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));- AppSearchEmail inEmail3 =- new AppSearchEmail.Builder("namespace2", "id3")- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();- checkIsBatchResultSuccess(- mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));- AppSearchEmail inEmail4 =- new AppSearchEmail.Builder("namespace2", "id4")- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();- checkIsBatchResultSuccess(- mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));- AppSearchEmail inEmail5 =- new AppSearchEmail.Builder("namespace2", "id5")- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();- checkIsBatchResultSuccess(- mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail5).build()));- GenericDocument inDoc1 =- new GenericDocument.Builder<>("namespace3", "id6", "Generic")- .setPropertyString("foo", "body")- .build();- checkIsBatchResultSuccess(- mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inDoc1).build()));- GenericDocument inDoc2 =- new GenericDocument.Builder<>("namespace3", "id7", "Generic")- .setPropertyString("foo", "body")- .build();- checkIsBatchResultSuccess(- mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inDoc2).build()));- GenericDocument inDoc3 =- new GenericDocument.Builder<>("namespace4", "id8", "Generic")- .setPropertyString("foo", "body")- .build();- checkIsBatchResultSuccess(- mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inDoc3).build()));-- // Query with per package result grouping. Only the last document 'doc3' should be- // returned.- SearchResults searchResults =- mDb1.search(- "body",- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_PACKAGE, /* resultLimit= */ 1)- .build());- List documents = convertSearchResultsToDocuments(searchResults);- assertThat(documents).containsExactly(inDoc3);-- // Query with per namespace result grouping. Only the last document in each namespace should- // be returned ('doc3', 'doc2', 'email5' and 'email2').- searchResults =- mDb1.search(- "body",- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_NAMESPACE,- /* resultLimit= */ 1)- .build());- documents = convertSearchResultsToDocuments(searchResults);- assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);-- // Query with per namespace result grouping. Two of the last documents in each namespace- // should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4', 'email2', 'email1')- searchResults =- mDb1.search(- "body",- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_NAMESPACE,- /* resultLimit= */ 2)- .build());- documents = convertSearchResultsToDocuments(searchResults);- assertThat(documents)- .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);-- // Query with per schema result grouping. Only the last document of each schema type should- // be returned ('doc3', 'email5')- searchResults =- mDb1.search(- "body",- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* resultLimit= */ 1)- .build());- documents = convertSearchResultsToDocuments(searchResults);- assertThat(documents).containsExactly(inDoc3, inEmail5);-- // Query with per schema result grouping. Only the last two documents of each schema type- // should be returned ('doc3', 'doc2', 'email5', 'email4')- searchResults =- mDb1.search(- "body",- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* resultLimit= */ 2)- .build());- documents = convertSearchResultsToDocuments(searchResults);- assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);-- // Query with per package and per namespace result grouping. Only the last document in each- // namespace should be returned ('doc3', 'doc2', 'email5' and 'email2').- searchResults =- mDb1.search(- "body",- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_NAMESPACE- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,- /* resultLimit= */ 1)- .build());- documents = convertSearchResultsToDocuments(searchResults);- assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);-- // Query with per package and per namespace result grouping. Only the last two documents- // in each namespace should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4',- // 'email2', 'email1')- searchResults =- mDb1.search(- "body",- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_NAMESPACE- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,- /* resultLimit= */ 2)- .build());- documents = convertSearchResultsToDocuments(searchResults);- assertThat(documents)- .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);-- // Query with per package and per schema type result grouping. Only the last document in- // each schema type should be returned. ('doc3', 'email5')- searchResults =- mDb1.search(- "body",- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_SCHEMA- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,- /* resultLimit= */ 1)- .build());- documents = convertSearchResultsToDocuments(searchResults);- assertThat(documents).containsExactly(inDoc3, inEmail5);-- // Query with per package and per schema type result grouping. Only the last two document in- // each schema type should be returned. ('doc3', 'doc2', 'email5', 'email4')- searchResults =- mDb1.search(- "body",- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_SCHEMA- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,- /* resultLimit= */ 2)- .build());- documents = convertSearchResultsToDocuments(searchResults);- assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);-- // Query with per namespace and per schema type result grouping. Only the last document in- // each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2').- searchResults =- mDb1.search(- "body",- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_NAMESPACE- | SearchSpec.GROUPING_TYPE_PER_SCHEMA,- /* resultLimit= */ 1)- .build());- documents = convertSearchResultsToDocuments(searchResults);- assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);-- // Query with per namespace and per schema type result grouping. Only the last two documents- // in each namespace should be returned. ('doc3', 'doc2', 'doc1', 'email5', 'email4',- // 'email2', 'email1')- searchResults =- mDb1.search(- "body",- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_NAMESPACE- | SearchSpec.GROUPING_TYPE_PER_SCHEMA,- /* resultLimit= */ 2)- .build());- documents = convertSearchResultsToDocuments(searchResults);- assertThat(documents)- .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);-- // Query with per namespace, per package and per schema type result grouping. Only the last- // document in each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2')- searchResults =- mDb1.search(- "body",- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_NAMESPACE- | SearchSpec.GROUPING_TYPE_PER_SCHEMA- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,- /* resultLimit= */ 1)- .build());- documents = convertSearchResultsToDocuments(searchResults);- assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);-- // Query with per namespace, per package and per schema type result grouping. Only the last- // two documents in each namespace should be returned.('doc3', 'doc2', 'doc1', 'email5',- // 'email4', 'email2', 'email1')- searchResults =- mDb1.search(- "body",- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_NAMESPACE- | SearchSpec.GROUPING_TYPE_PER_SCHEMA- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,- /* resultLimit= */ 2)- .build());- documents = convertSearchResultsToDocuments(searchResults);- assertThat(documents)- .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);- }-- // TODO(b/291122592): move to CTS once the APIs it uses are public- @Test- public void testQuery_ResultGroupingLimits_SchemaGroupingNotSupported() throws Exception {- assumeFalse(- mDb1.getFeatures()- .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));- // Schema registration- mDb1.setSchemaAsync(- new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())- .get();-- // Index four documents.- AppSearchEmail inEmail1 =- new AppSearchEmail.Builder("namespace1", "id1")- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();- checkIsBatchResultSuccess(- mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));- AppSearchEmail inEmail2 =- new AppSearchEmail.Builder("namespace1", "id2")- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();- checkIsBatchResultSuccess(- mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));- AppSearchEmail inEmail3 =- new AppSearchEmail.Builder("namespace2", "id3")- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();- checkIsBatchResultSuccess(- mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));- AppSearchEmail inEmail4 =- new AppSearchEmail.Builder("namespace2", "id4")- .setFrom("[email protected]")- .setTo("[email protected]", "[email protected]")- .setSubject("testPut example")- .setBody("This is the body of the testPut email")- .build();- checkIsBatchResultSuccess(- mDb1.putAsync(- new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));-- // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.- // UnsupportedOperationException will be thrown.- SearchSpec searchSpec1 =- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* resultLimit= */ 1)- .build();- UnsupportedOperationException exception =- assertThrows(- UnsupportedOperationException.class,- () -> mDb1.search("body", searchSpec1));- assertThat(exception)- .hasMessageThat()- .contains(- Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA- + " is not available on this"- + " AppSearch implementation.");-- // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.- // UnsupportedOperationException will be thrown.- SearchSpec searchSpec2 =- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_PACKAGE- | SearchSpec.GROUPING_TYPE_PER_SCHEMA,- /* resultLimit= */ 1)- .build();- exception =- assertThrows(- UnsupportedOperationException.class,- () -> mDb1.search("body", searchSpec2));- assertThat(exception)- .hasMessageThat()- .contains(- Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA- + " is not available on this"- + " AppSearch implementation.");-- // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.- // UnsupportedOperationException will be thrown.- SearchSpec searchSpec3 =- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_NAMESPACE- | SearchSpec.GROUPING_TYPE_PER_SCHEMA,- /* resultLimit= */ 1)- .build();- exception =- assertThrows(- UnsupportedOperationException.class,- () -> mDb1.search("body", searchSpec3));- assertThat(exception)- .hasMessageThat()- .contains(- Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA- + " is not available on this"- + " AppSearch implementation.");-- // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.- // UnsupportedOperationException will be thrown.- SearchSpec searchSpec4 =- new SearchSpec.Builder()- .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)- .setResultGrouping(- SearchSpec.GROUPING_TYPE_PER_NAMESPACE- | SearchSpec.GROUPING_TYPE_PER_SCHEMA- | SearchSpec.GROUPING_TYPE_PER_PACKAGE,- /* resultLimit= */ 1)- .build();- exception =- assertThrows(- UnsupportedOperationException.class,- () -> mDb1.search("body", searchSpec4));- assertThat(exception)- .hasMessageThat()- .contains(- Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA- + " is not available on this"- + " AppSearch implementation.");- }- @Test public void testQuery_typeFilterWithPolymorphism() throws Exception { assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));@@ -1664,4 +478,167 @@
assertThat(documents).hasSize(4); assertThat(documents).containsExactly(expectedDocA, expectedDocB, expectedDocC, docD); }++ // TODO(b/336277840): Move this if setParentTypes becomes public+ @Test+ public void testQuery_wildcardProjection_polymorphism() throws Exception {+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));+ AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message")+ .addProperty(new StringPropertyConfig.Builder("sender")+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)+ .build())+ .addProperty(new StringPropertyConfig.Builder("content")+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)+ .build())+ .build();+ AppSearchSchema textSchema = new AppSearchSchema.Builder("Text")+ .addParentType("Message")+ .addProperty(new StringPropertyConfig.Builder("sender")+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)+ .build())+ .addProperty(new StringPropertyConfig.Builder("content")+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)+ .build())+ .build();+ AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")+ .addParentType("Message")+ .addProperty(new StringPropertyConfig.Builder("sender")+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)+ .build())+ .addProperty(new StringPropertyConfig.Builder("content")+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)+ .build())+ .build();++ // Schema registration+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()+ .addSchemas(messageSchema, textSchema, emailSchema).build()).get();++ // Index two child documents+ GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text")+ .setCreationTimestampMillis(1000)+ .setPropertyString("sender", "Some sender")+ .setPropertyString("content", "Some note")+ .build();+ GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email")+ .setCreationTimestampMillis(1000)+ .setPropertyString("sender", "Some sender")+ .setPropertyString("content", "Some note")+ .build();+ checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()+ .addGenericDocuments(email, text).build()));++ SearchResults searchResults = mDb1.search("Some", new SearchSpec.Builder()+ .addFilterSchemas("Message")+ .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("sender"))+ .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("content"))+ .build());+ List documents = convertSearchResultsToDocuments(searchResults);++ // We specified the parent document in the filter schemas, but only indexed child documents.+ // As we also specified a wildcard schema type projection, it should apply to the child docs+ // The content property must not appear. Also emailNoContent should not appear as we are+ // filter on the content property+ GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text")+ .setParentTypes(Collections.singletonList("Message"))+ .setCreationTimestampMillis(1000)+ .setPropertyString("sender", "Some sender")+ .build();+ GenericDocument expectedEmail = new GenericDocument.Builder<>("namespace", "id2", "Email")+ .setParentTypes(Collections.singletonList("Message"))+ .setCreationTimestampMillis(1000)+ .setPropertyString("sender", "Some sender")+ .build();+ assertThat(documents).containsExactly(expectedText, expectedEmail);+ }++ // TODO(b/336277840): Move this if setParentTypes becomes public+ @Test+ public void testQuery_wildcardFilterSchema_polymorphism() throws Exception {+ assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));+ AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message")+ .addProperty(new StringPropertyConfig.Builder("content")+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)+ .build())+ .build();+ AppSearchSchema textSchema = new AppSearchSchema.Builder("Text")+ .addParentType("Message")+ .addProperty(new StringPropertyConfig.Builder("content")+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)+ .build())+ .addProperty(new StringPropertyConfig.Builder("carrier")+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)+ .build())+ .build();+ AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")+ .addParentType("Message")+ .addProperty(new StringPropertyConfig.Builder("content")+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)+ .build())+ .addProperty(new StringPropertyConfig.Builder("attachment")+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)+ .build())+ .build();++ // Schema registration+ mDb1.setSchemaAsync(new SetSchemaRequest.Builder()+ .addSchemas(messageSchema, textSchema, emailSchema).build()).get();++ // Index two child documents+ GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text")+ .setCreationTimestampMillis(1000)+ .setPropertyString("content", "Some note")+ .setPropertyString("carrier", "Network Inc")+ .build();+ GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email")+ .setCreationTimestampMillis(1000)+ .setPropertyString("content", "Some note")+ .setPropertyString("attachment", "Network report")+ .build();++ checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()+ .addGenericDocuments(email, text).build()));++ // Both email and text would match for "Network", but only text should match as it is in the+ // right property+ SearchResults searchResults = mDb1.search("Network", new SearchSpec.Builder()+ .addFilterSchemas("Message")+ .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("carrier"))+ .build());+ List documents = convertSearchResultsToDocuments(searchResults);++ // We specified the parent document in the filter schemas, but only indexed child documents.+ // As we also specified a wildcard schema type projection, it should apply to the child docs+ // The content property must not appear. Also emailNoContent should not appear as we are+ // filter on the content property+ GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text")+ .setParentTypes(Collections.singletonList("Message"))+ .setCreationTimestampMillis(1000)+ .setPropertyString("content", "Some note")+ .setPropertyString("carrier", "Network Inc")+ .build();+ assertThat(documents).containsExactly(expectedText);+ } }
@@ -26,7 +26,6 @@
import java.util.concurrent.ExecutorService;-// TODO(b/227356108): move this test to cts test once we un-hide search suggestion API. public class AppSearchSessionLocalInternalTest extends AppSearchSessionInternalTestBase { @Override protected ListenableFuture createSearchSessionAsync(@NonNull String dbName) {
@@ -26,11 +26,8 @@
import com.google.common.util.concurrent.ListenableFuture;-import org.junit.Test;- import java.util.concurrent.ExecutorService;-// TODO(b/227356108): move this test to cts test once we un-hide search suggestion API. @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S) public class AppSearchSessionPlatformInternalTest extends AppSearchSessionInternalTestBase { @Override@@ -48,10 +45,4 @@
new PlatformStorage.SearchContext.Builder(context, dbName) .setWorkerExecutor(executor).build()); }-- @Override- @Test- public void testSearchSuggestion_propertyFilter() throws Exception {- // TODO(b/227356108) enable the test when suggestion is ready in platform.- } }
@@ -0,0 +1,103 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.app;++import static com.google.common.truth.Truth.assertThat;++import com.google.common.collect.ImmutableSet;++import org.junit.Test;++import java.util.List;++public class InternalVisibilityConfigTest {++ @Test+ public void testVisibilityConfig_setNotDisplayBySystem() {+ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("schema")+ .setNotDisplayedBySystem(true).build();++ assertThat(visibilityConfig.isNotDisplayedBySystem()).isTrue();+ }++ @Test+ public void testVisibilityConfig_setVisibilityConfig() {+ String visibleToPackage1 = "com.example.package";+ byte[] visibleToPackageCert1 = new byte[32];+ String visibleToPackage2 = "com.example.package2";+ byte[] visibleToPackageCert2 = new byte[32];++ SchemaVisibilityConfig innerConfig1 = new SchemaVisibilityConfig.Builder()+ .addAllowedPackage(+ new PackageIdentifier(visibleToPackage1, visibleToPackageCert1))+ .addRequiredPermissions(ImmutableSet.of(1, 2))+ .build();+ SchemaVisibilityConfig innerConfig2 = new SchemaVisibilityConfig.Builder()+ .addAllowedPackage(+ new PackageIdentifier(visibleToPackage2, visibleToPackageCert2))+ .addRequiredPermissions(ImmutableSet.of(3, 4))+ .build();++ InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("schema")+ .addVisibleToConfig(innerConfig1)+ .addVisibleToConfig(innerConfig2)+ .build();++ assertThat(visibilityConfig.getVisibleToConfigs())+ .containsExactly(innerConfig1, innerConfig2);+ }++ @Test+ public void testToInternalVisibilityConfig() {+ byte[] packageSha256Cert = new byte[32];+ packageSha256Cert[0] = 24;+ packageSha256Cert[8] = 23;+ packageSha256Cert[16] = 22;+ packageSha256Cert[24] = 21;++ // Create a SetSchemaRequest for testing+ SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()+ .addSchemas(new AppSearchSchema.Builder("testSchema").build())+ .setSchemaTypeDisplayedBySystem("testSchema", false)+ .setSchemaTypeVisibilityForPackage("testSchema", /*visible=*/true,+ new PackageIdentifier("com.example.test", packageSha256Cert))+ .setPubliclyVisibleSchema("testSchema",+ new PackageIdentifier("com.example.test1", packageSha256Cert))+ .build();++ // Convert the SetSchemaRequest to GenericDocument map+ List visibilityConfigs =+ InternalVisibilityConfig.toInternalVisibilityConfigs(setSchemaRequest);++ // Check if the conversion is correct+ assertThat(visibilityConfigs).hasSize(1);+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(0);+ assertThat(visibilityConfig.isNotDisplayedBySystem()).isTrue();+ assertThat(visibilityConfig.getVisibilityConfig().getAllowedPackages())+ .containsExactly(new PackageIdentifier("com.example.test", packageSha256Cert));+ assertThat(visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage())+ .isNotNull();+ assertThat(+ visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage()+ .getPackageName())+ .isEqualTo("com.example.test1");+ assertThat(+ visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage()+ .getSha256Certificate())+ .isEqualTo(packageSha256Cert);+ }+}
@@ -0,0 +1,41 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.app;++import static com.google.common.truth.Truth.assertThat;++import org.junit.Test;++public class JoinSpecInternalTest {+ @Test+ public void testJoinSpecBuilderCopyConstructor() {+ JoinSpec joinSpec = new JoinSpec.Builder("childPropertyExpression")+ .setMaxJoinedResultCount(10)+ .setNestedSearch("nestedQuery", new SearchSpec.Builder().build())+ .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)+ .build();+ JoinSpec joinSpecCopy = new JoinSpec.Builder(joinSpec).build();+ assertThat(joinSpecCopy.getMaxJoinedResultCount()).isEqualTo(+ joinSpec.getMaxJoinedResultCount());+ assertThat(joinSpecCopy.getChildPropertyExpression()).isEqualTo(+ joinSpec.getChildPropertyExpression());+ assertThat(joinSpecCopy.getNestedQuery()).isEqualTo(joinSpec.getNestedQuery());+ assertThat(joinSpecCopy.getNestedSearchSpec()).isNotNull();+ assertThat(joinSpecCopy.getAggregationScoringStrategy()).isEqualTo(+ joinSpec.getAggregationScoringStrategy());+ }+}
@@ -0,0 +1,107 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.app;++import static com.google.common.truth.Truth.assertThat;++import org.junit.Test;++public class SearchResultInternalTest {+ @Test+ public void testSearchResultBuilderCopyConstructor() {+ GenericDocument document =+ new GenericDocument.Builder<>("namespace", "id", "schemaType").build();+ SearchResult searchResult = new SearchResult.Builder("package", "database")+ .setGenericDocument(document)+ .setRankingSignal(1.23)+ .addJoinedResult(new SearchResult.Builder("pkg1", "db1").setGenericDocument(+ document).build())+ .addJoinedResult(new SearchResult.Builder("pkg2", "db2").setGenericDocument(+ document).build())+ .addMatchInfo(new SearchResult.MatchInfo.Builder("propertyPath1").build())+ .addMatchInfo(new SearchResult.MatchInfo.Builder("propertyPath2").build())+ .addMatchInfo(new SearchResult.MatchInfo.Builder("propertyPath3").build())+ .build();+ SearchResult searchResultCopy = new SearchResult.Builder(searchResult).build();+ assertThat(searchResultCopy.getGenericDocument()).isEqualTo(+ searchResult.getGenericDocument());+ assertThat(searchResultCopy.getRankingSignal()).isEqualTo(searchResult.getRankingSignal());+ // Specifically test JoinedResults and MatchInfos with different sizes since briefly had+ // a bug where we looped through joinedResults using matchInfos.size()+ assertThat(searchResultCopy.getJoinedResults().size()).isEqualTo(+ searchResult.getJoinedResults().size());+ assertThat(searchResultCopy.getJoinedResults().get(0).getPackageName()).isEqualTo("pkg1");+ assertThat(searchResultCopy.getJoinedResults().get(0).getDatabaseName()).isEqualTo("db1");+ assertThat(searchResultCopy.getJoinedResults().get(1).getPackageName()).isEqualTo("pkg2");+ assertThat(searchResultCopy.getJoinedResults().get(1).getDatabaseName()).isEqualTo("db2");+ assertThat(searchResultCopy.getMatchInfos().size()).isEqualTo(+ searchResult.getMatchInfos().size());+ assertThat(searchResultCopy.getMatchInfos().get(0).getPropertyPath()).isEqualTo(+ "propertyPath1");+ assertThat(searchResultCopy.getMatchInfos().get(1).getPropertyPath()).isEqualTo(+ "propertyPath2");+ assertThat(searchResultCopy.getMatchInfos().get(2).getPropertyPath()).isEqualTo(+ "propertyPath3");+ }++ @Test+ public void testSearchResultBuilderCopyConstructor_informationalRankingSignal() {+ GenericDocument document =+ new GenericDocument.Builder<>("namespace", "id", "schemaType").build();+ SearchResult searchResult = new SearchResult.Builder("package", "database")+ .setGenericDocument(document)+ .setRankingSignal(1.23)+ .addInformationalRankingSignal(2)+ .addInformationalRankingSignal(3)+ .build();+ SearchResult searchResultCopy = new SearchResult.Builder(searchResult).build();+ assertThat(searchResultCopy.getRankingSignal()).isEqualTo(searchResult.getRankingSignal());+ assertThat(searchResultCopy.getInformationalRankingSignals()).isEqualTo(+ searchResult.getInformationalRankingSignals());+ }++ @Test+ public void testSearchResultBuilder_clearJoinedResults() {+ GenericDocument document =+ new GenericDocument.Builder<>("namespace", "id", "schemaType").build();+ SearchResult searchResult = new SearchResult.Builder("package", "database")+ .setGenericDocument(document)+ .addJoinedResult(new SearchResult.Builder("pkg", "db").setGenericDocument(+ document).build())+ .clearJoinedResults()+ .build();+ assertThat(searchResult.getJoinedResults()).isEmpty();+ }++ @Test+ public void testMatchInfoBuilderCopyConstructor() {+ SearchResult.MatchRange exactMatchRange = new SearchResult.MatchRange(3, 8);+ SearchResult.MatchRange submatchRange = new SearchResult.MatchRange(3, 5);+ SearchResult.MatchRange snippetMatchRange = new SearchResult.MatchRange(1, 10);+ SearchResult.MatchInfo matchInfo =+ new SearchResult.MatchInfo.Builder("propertyPath1")+ .setExactMatchRange(exactMatchRange)+ .setSubmatchRange(submatchRange)+ .setSnippetRange(snippetMatchRange).build();+ SearchResult.MatchInfo matchInfoCopy =+ new SearchResult.MatchInfo.Builder(matchInfo).build();+ assertThat(matchInfoCopy.getPropertyPath()).isEqualTo("propertyPath1");+ assertThat(matchInfoCopy.getExactMatchRange()).isEqualTo(exactMatchRange);+ assertThat(matchInfoCopy.getSubmatchRange()).isEqualTo(submatchRange);+ assertThat(matchInfoCopy.getSnippetRange()).isEqualTo(snippetMatchRange);+ }+}
@@ -0,0 +1,48 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.app;++import static com.google.common.truth.Truth.assertThat;++import org.junit.Test;++import java.util.Arrays;+import java.util.List;++public class SearchResultPageInternalTest {+ @Test+ public void testSearchResultPage() {+ GenericDocument document =+ new GenericDocument.Builder<>("namespace", "id", "schemaType").build();+ List results = Arrays.asList(+ new SearchResult.Builder("package1", "database1").setGenericDocument(+ document).build(),+ new SearchResult.Builder("package2", "database2").setGenericDocument(+ document).build()+ );+ SearchResultPage searchResultPage = new SearchResultPage(/*nextPageToken=*/ 123, results);+ assertThat(searchResultPage.getNextPageToken()).isEqualTo(123);+ List searchResults = searchResultPage.getResults();+ assertThat(searchResults).hasSize(2);+ assertThat(searchResults.get(0).getPackageName()).isEqualTo("package1");+ assertThat(searchResults.get(0).getDatabaseName()).isEqualTo("database1");+ assertThat(searchResults.get(0).getGenericDocument()).isEqualTo(document);+ assertThat(searchResults.get(1).getPackageName()).isEqualTo("package2");+ assertThat(searchResults.get(1).getDatabaseName()).isEqualTo("database2");+ assertThat(searchResults.get(1).getGenericDocument()).isEqualTo(document);+ }+}
@@ -1,88 +0,0 @@
-/*- * Copyright 2022 The Android Open Source Project- *- * Licensed under the Apache License, Version 2.0 (the "License");- * you may not use this file except in compliance with the License.- * You may obtain a copy of the License at- *- * http://www.apache.org/licenses/LICENSE-2.0- *- * Unless required by applicable law or agreed to in writing, software- * distributed under the License is distributed on an "AS IS" BASIS,- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.- * See the License for the specific language governing permissions and- * limitations under the License.- */--package androidx.appsearch.app;--import static com.google.common.truth.Truth.assertThat;--import static org.junit.Assert.assertThrows;--import com.google.common.collect.ImmutableList;--import org.junit.Test;--// TODO(b/228240987) delete this test when we support property restrict for multiple terms-public class SearchSuggestionSpecInternalTest {-- @Test- public void testBuildSearchSuggestionSpec_withPropertyFilter() throws Exception {- SearchSuggestionSpec searchSuggestionSpec =- new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)- .setRankingStrategy(SearchSuggestionSpec- .SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)- .addFilterSchemas("Person", "Email")- .addFilterSchemas(ImmutableList.of("Foo"))- .addFilterProperties("Email", ImmutableList.of("Subject", "body"))- .addFilterPropertyPaths("Foo",- ImmutableList.of(new PropertyPath("Bar")))- .build();-- assertThat(searchSuggestionSpec.getMaximumResultCount()).isEqualTo(123);- assertThat(searchSuggestionSpec.getFilterSchemas())- .containsExactly("Person", "Email", "Foo");- assertThat(searchSuggestionSpec.getFilterProperties())- .containsExactly("Email", ImmutableList.of("Subject", "body"),- "Foo", ImmutableList.of("Bar"));- }-- @Test- public void testPropertyFilterMustMatchSchemaFilter() throws Exception {- IllegalStateException e = assertThrows(IllegalStateException.class,- () -> new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)- .addFilterSchemas("Person")- .addFilterProperties("Email", ImmutableList.of("Subject", "body"))- .build());- assertThat(e).hasMessageThat().contains("The schema: Email exists in the "- + "property filter but doesn't exist in the schema filter.");- }-- @Test- public void testRebuild_withPropertyFilter() throws Exception {- SearchSuggestionSpec.Builder builder =- new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)- .addFilterSchemas("Person", "Email")- .addFilterProperties("Email", ImmutableList.of("Subject", "body"));-- SearchSuggestionSpec original = builder.build();-- builder.addFilterSchemas("Message", "Foo")- .addFilterProperties("Foo", ImmutableList.of("Bar"));- SearchSuggestionSpec rebuild = builder.build();-- assertThat(original.getMaximumResultCount()).isEqualTo(123);- assertThat(original.getFilterSchemas())- .containsExactly("Person", "Email");- assertThat(original.getFilterProperties())- .containsExactly("Email", ImmutableList.of("Subject", "body"));-- assertThat(rebuild.getMaximumResultCount()).isEqualTo(123);- assertThat(rebuild.getFilterSchemas())- .containsExactly("Person", "Email", "Message", "Foo");- assertThat(rebuild.getFilterProperties())- .containsExactly("Email", ImmutableList.of("Subject", "body"),- "Foo", ImmutableList.of("Bar"));- }-}
@@ -0,0 +1,104 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.cts.app;++import static com.google.common.truth.Truth.assertThat;++import androidx.appsearch.app.PackageIdentifier;+import androidx.appsearch.app.SchemaVisibilityConfig;++import com.google.common.collect.ImmutableSet;++import org.junit.Test;++import java.util.Arrays;++public class SchemaVisibilityConfigCtsTest {++ @Test+ public void testBuildVisibilityConfig() {+ byte[] cert1 = new byte[32];+ Arrays.fill(cert1, (byte) 1);+ byte[] cert2 = new byte[32];+ Arrays.fill(cert2, (byte) 2);+ SchemaVisibilityConfig schemaVisibilityConfig = new SchemaVisibilityConfig.Builder()+ .addAllowedPackage(new PackageIdentifier("pkg1", cert1))+ .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", cert2))+ .addRequiredPermissions(ImmutableSet.of(1, 2))+ .build();++ assertThat(schemaVisibilityConfig.getRequiredPermissions())+ .containsExactly(ImmutableSet.of(1, 2));+ assertThat(schemaVisibilityConfig.getAllowedPackages())+ .containsExactly(new PackageIdentifier("pkg1", cert1));+ assertThat(schemaVisibilityConfig.getPubliclyVisibleTargetPackage())+ .isEqualTo(new PackageIdentifier("pkg2", cert2));+ }++ @Test+ public void testVisibilityConfigEquals() {+ // Create two VisibilityConfig instances with the same properties+ SchemaVisibilityConfig visibilityConfig1 = new SchemaVisibilityConfig.Builder()+ .addAllowedPackage(new PackageIdentifier("pkg1", new byte[32]))+ .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", new byte[32]))+ .addRequiredPermissions(ImmutableSet.of(1, 2))+ .build();++ SchemaVisibilityConfig visibilityConfig2 = new SchemaVisibilityConfig.Builder()+ .addAllowedPackage(new PackageIdentifier("pkg1", new byte[32]))+ .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", new byte[32]))+ .addRequiredPermissions(ImmutableSet.of(1, 2))+ .build();++ // Test equals method+ assertThat(visibilityConfig1).isEqualTo(visibilityConfig2);+ assertThat(visibilityConfig2).isEqualTo(visibilityConfig1);+ }++ @Test+ public void testVisibilityConfig_rebuild() {+ String visibleToPackage = "com.example.package";+ byte[] visibleToPackageCert = new byte[32];++ String publiclyVisibleTarget = "com.example.test";+ byte[] publiclyVisibleTargetCert = new byte[32];++ SchemaVisibilityConfig.Builder builder = new SchemaVisibilityConfig.Builder()+ .addAllowedPackage(new PackageIdentifier(visibleToPackage, visibleToPackageCert))+ .setPubliclyVisibleTargetPackage(new PackageIdentifier(+ publiclyVisibleTarget, publiclyVisibleTargetCert))+ .addRequiredPermissions(ImmutableSet.of(1, 2));++ // Create a VisibilityConfig using the Builder+ SchemaVisibilityConfig original = builder.build();++ SchemaVisibilityConfig rebuild = builder.clearAllowedPackages()+ .setPubliclyVisibleTargetPackage(null)+ .clearRequiredPermissions().build();++ // Check if the properties are set correctly+ assertThat(original.getAllowedPackages()).containsExactly(+ new PackageIdentifier(visibleToPackage, visibleToPackageCert));+ assertThat(original.getPubliclyVisibleTargetPackage()).isEqualTo(+ new PackageIdentifier(publiclyVisibleTarget, publiclyVisibleTargetCert));+ assertThat(original.getRequiredPermissions()).containsExactly(ImmutableSet.of(1, 2));++ assertThat(rebuild.getAllowedPackages()).isEmpty();+ assertThat(rebuild.getPubliclyVisibleTargetPackage()).isNull();+ assertThat(rebuild.getRequiredPermissions()).isEmpty();+ }+}
@@ -0,0 +1,126 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+// @exportToFramework:skipFile()++package androidx.appsearch.cts.usagereporting;++import static com.google.common.truth.Truth.assertThat;++import androidx.appsearch.app.GenericDocument;+import androidx.appsearch.usagereporting.ActionConstants;+import androidx.appsearch.usagereporting.ClickAction;+import androidx.appsearch.usagereporting.TakenAction;++import org.junit.Test;++public class ClickActionCtsTest {+ @Test+ public void testBuilder() {+ ClickAction clickAction =+ new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)+ .setDocumentTtlMillis(456)+ .setQuery("query")+ .setReferencedQualifiedId("pkg$db/ns#refId")+ .setResultRankInBlock(1)+ .setResultRankGlobal(3)+ .setTimeStayOnResultMillis(65536)+ .build();++ assertThat(clickAction.getNamespace()).isEqualTo("namespace");+ assertThat(clickAction.getId()).isEqualTo("id");+ assertThat(clickAction.getActionTimestampMillis()).isEqualTo(123);+ assertThat(clickAction.getDocumentTtlMillis()).isEqualTo(456);+ assertThat(clickAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_CLICK);+ assertThat(clickAction.getQuery()).isEqualTo("query");+ assertThat(clickAction.getReferencedQualifiedId()).isEqualTo("pkg$db/ns#refId");+ assertThat(clickAction.getResultRankInBlock()).isEqualTo(1);+ assertThat(clickAction.getResultRankGlobal()).isEqualTo(3);+ assertThat(clickAction.getTimeStayOnResultMillis()).isEqualTo(65536);+ }++ @Test+ public void testBuilder_defaultValues() {+ ClickAction clickAction =+ new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)+ .build();++ assertThat(clickAction.getNamespace()).isEqualTo("namespace");+ assertThat(clickAction.getId()).isEqualTo("id");+ assertThat(clickAction.getActionTimestampMillis()).isEqualTo(123);+ assertThat(clickAction.getDocumentTtlMillis())+ .isEqualTo(TakenAction.DEFAULT_DOCUMENT_TTL_MILLIS);+ assertThat(clickAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_CLICK);+ assertThat(clickAction.getQuery()).isNull();+ assertThat(clickAction.getReferencedQualifiedId()).isNull();+ assertThat(clickAction.getResultRankInBlock()).isEqualTo(-1);+ assertThat(clickAction.getResultRankGlobal()).isEqualTo(-1);+ assertThat(clickAction.getTimeStayOnResultMillis()).isEqualTo(-1);+ }++ @Test+ public void testBuilderCopy_allFieldsAreCopied() {+ ClickAction clickAction1 =+ new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)+ .setDocumentTtlMillis(456)+ .setQuery("query")+ .setReferencedQualifiedId("pkg$db/ns#refId")+ .setResultRankInBlock(1)+ .setResultRankGlobal(3)+ .setTimeStayOnResultMillis(65536)+ .build();+ ClickAction clickAction2 = new ClickAction.Builder(clickAction1).build();++ // All fields should be copied correctly from clickAction1 to the builder and propagates to+ // clickAction2 after calling build().+ assertThat(clickAction2.getNamespace()).isEqualTo("namespace");+ assertThat(clickAction2.getId()).isEqualTo("id");+ assertThat(clickAction2.getActionTimestampMillis()).isEqualTo(123);+ assertThat(clickAction2.getDocumentTtlMillis()).isEqualTo(456);+ assertThat(clickAction2.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_CLICK);+ assertThat(clickAction2.getQuery()).isEqualTo("query");+ assertThat(clickAction2.getReferencedQualifiedId()).isEqualTo("pkg$db/ns#refId");+ assertThat(clickAction2.getResultRankInBlock()).isEqualTo(1);+ assertThat(clickAction2.getResultRankGlobal()).isEqualTo(3);+ assertThat(clickAction2.getTimeStayOnResultMillis()).isEqualTo(65536);+ }++ @Test+ public void testToGenericDocument() throws Exception {+ ClickAction clickAction =+ new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)+ .setDocumentTtlMillis(456)+ .setQuery("query")+ .setReferencedQualifiedId("pkg$db/ns#refId")+ .setResultRankInBlock(1)+ .setResultRankGlobal(3)+ .setTimeStayOnResultMillis(65536)+ .build();++ GenericDocument document = GenericDocument.fromDocumentClass(clickAction);+ assertThat(document.getNamespace()).isEqualTo("namespace");+ assertThat(document.getId()).isEqualTo("id");+ assertThat(document.getCreationTimestampMillis()).isEqualTo(123);+ assertThat(document.getTtlMillis()).isEqualTo(456);+ assertThat(document.getPropertyLong("actionType"))+ .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);+ assertThat(document.getPropertyString("query")).isEqualTo("query");+ assertThat(document.getPropertyString("referencedQualifiedId"))+ .isEqualTo("pkg$db/ns#refId");+ assertThat(document.getPropertyLong("resultRankInBlock")).isEqualTo(1);+ assertThat(document.getPropertyLong("resultRankGlobal")).isEqualTo(3);+ assertThat(document.getPropertyLong("timeStayOnResultMillis")).isEqualTo(65536);+ }+}
@@ -0,0 +1,102 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+// @exportToFramework:skipFile()++package androidx.appsearch.cts.usagereporting;++import static com.google.common.truth.Truth.assertThat;++import androidx.appsearch.app.GenericDocument;+import androidx.appsearch.usagereporting.ActionConstants;+import androidx.appsearch.usagereporting.SearchAction;+import androidx.appsearch.usagereporting.TakenAction;++import org.junit.Test;++public class SearchActionCtsTest {+ @Test+ public void testBuilder() {+ SearchAction searchAction =+ new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)+ .setDocumentTtlMillis(456)+ .setQuery("query")+ .setFetchedResultCount(1)+ .build();++ assertThat(searchAction.getNamespace()).isEqualTo("namespace");+ assertThat(searchAction.getId()).isEqualTo("id");+ assertThat(searchAction.getActionTimestampMillis()).isEqualTo(123);+ assertThat(searchAction.getDocumentTtlMillis()).isEqualTo(456);+ assertThat(searchAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);+ assertThat(searchAction.getQuery()).isEqualTo("query");+ assertThat(searchAction.getFetchedResultCount()).isEqualTo(1);+ }++ @Test+ public void testBuilder_defaultValues() {+ SearchAction searchAction =+ new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)+ .build();++ assertThat(searchAction.getNamespace()).isEqualTo("namespace");+ assertThat(searchAction.getId()).isEqualTo("id");+ assertThat(searchAction.getActionTimestampMillis()).isEqualTo(123);+ assertThat(searchAction.getDocumentTtlMillis())+ .isEqualTo(TakenAction.DEFAULT_DOCUMENT_TTL_MILLIS);+ assertThat(searchAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);+ assertThat(searchAction.getQuery()).isNull();+ assertThat(searchAction.getFetchedResultCount()).isEqualTo(-1);+ }++ @Test+ public void testBuilderCopy_allFieldsAreCopied() {+ SearchAction searchAction1 =+ new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)+ .setDocumentTtlMillis(456)+ .setQuery("query")+ .setFetchedResultCount(1)+ .build();+ SearchAction searchAction2 = new SearchAction.Builder(searchAction1).build();++ assertThat(searchAction2.getNamespace()).isEqualTo("namespace");+ assertThat(searchAction2.getId()).isEqualTo("id");+ assertThat(searchAction2.getActionTimestampMillis()).isEqualTo(123);+ assertThat(searchAction2.getDocumentTtlMillis()).isEqualTo(456);+ assertThat(searchAction2.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);+ assertThat(searchAction2.getQuery()).isEqualTo("query");+ assertThat(searchAction2.getFetchedResultCount()).isEqualTo(1);+ }++ @Test+ public void testToGenericDocument() throws Exception {+ SearchAction searchAction =+ new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)+ .setDocumentTtlMillis(456)+ .setQuery("query")+ .setFetchedResultCount(1)+ .build();++ GenericDocument document = GenericDocument.fromDocumentClass(searchAction);+ assertThat(document.getNamespace()).isEqualTo("namespace");+ assertThat(document.getId()).isEqualTo("id");+ assertThat(document.getCreationTimestampMillis()).isEqualTo(123);+ assertThat(document.getTtlMillis()).isEqualTo(456);+ assertThat(document.getPropertyLong("actionType"))+ .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);+ assertThat(document.getPropertyString("query")).isEqualTo("query");+ assertThat(document.getPropertyLong("fetchedResultCount")).isEqualTo(1);+ }+}
@@ -0,0 +1,33 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+// @exportToFramework:skipFile()+package androidx.appsearch.flags;++import org.junit.rules.TestRule;+import org.junit.runner.Description;+import org.junit.runners.model.Statement;++/**+ * Shim for real CheckFlagsRule defined in Framework.+ *+ *
In Jetpack, this shim does nothing and exists only for code sync purpose.
+ */+public final class CheckFlagsRule implements TestRule {+ @Override+ public Statement apply(Statement base, Description description) {+ return base;+ }+}
@@ -0,0 +1,31 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+// @exportToFramework:skipFile()+package androidx.appsearch.flags;++/**+ * Shim for real DeviceFlagsValueProvider defined in Framework.+ *+ *
In Jetpack, this shim does nothing and exists only for code sync purpose.
+ */+public final class DeviceFlagsValueProvider {+ private DeviceFlagsValueProvider() {}++ /** Provides a shim rule that can be used to check the status of flags on device */+ public static CheckFlagsRule createCheckFlagsRule() {+ return new CheckFlagsRule();+ }+}
@@ -0,0 +1,123 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.flags;++import static com.google.common.truth.Truth.assertThat;++import org.junit.Test;++public class FlagsTest {+ @Test+ public void testFlagValue_enableSafeParcelable2() {+ assertThat(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2).isEqualTo(+ "com.android.appsearch.flags.enable_safe_parcelable_2");+ }++ @Test+ public void testFlagValue_enableListFilterHasPropertyFunction() {+ assertThat(Flags.FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION).isEqualTo(+ "com.android.appsearch.flags.enable_list_filter_has_property_function");+ }++ @Test+ public void testFlagValue_enableGroupingTypePerSchema() {+ assertThat(Flags.FLAG_ENABLE_GROUPING_TYPE_PER_SCHEMA).isEqualTo(+ "com.android.appsearch.flags.enable_grouping_type_per_schema");+ }++ @Test+ public void testFlagValue_enableGenericDocumentCopyConstructor() {+ assertThat(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR).isEqualTo("com.android"+ + ".appsearch.flags.enable_generic_document_copy_constructor");+ }++ @Test+ public void testFlagValue_enableSearchSpecFilterProperties() {+ assertThat(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES).isEqualTo(+ "com.android.appsearch.flags.enable_search_spec_filter_properties");+ }++ @Test+ public void testFlagValue_enableSearchSpecSetSearchSourceLogTag() {+ assertThat(Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG).isEqualTo(+ "com.android.appsearch.flags.enable_search_spec_set_search_source_log_tag");+ }++ @Test+ public void testFlagValue_enableSetSchemaVisibleToConfigs() {+ assertThat(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS).isEqualTo("com"+ + ".android.appsearch.flags.enable_set_schema_visible_to_configs");+ }++ @Test+ public void testFlagValue_enablePutDocumentsRequestAddTakenActions() {+ assertThat(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS).isEqualTo(+ "com.android.appsearch.flags.enable_put_documents_request_add_taken_actions");+ }++ @Test+ public void testFlagValue_enableGenericDocumentBuilderHiddenMethods() {+ assertThat(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS).isEqualTo("com"+ + ".android.appsearch.flags.enable_generic_document_builder_hidden_methods");+ }++ @Test+ public void testFlagValue_enableSetPubliclyVisibleSchema() {+ assertThat(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)+ .isEqualTo(+ "com.android.appsearch.flags.enable_set_publicly_visible_schema");+ }++ @Test+ public void testFlagValue_enableEnterpriseGlobalSearchSession() {+ assertThat(Flags.FLAG_ENABLE_ENTERPRISE_GLOBAL_SEARCH_SESSION)+ .isEqualTo("com.android.appsearch.flags.enable_enterprise_global_search_session");+ }++ @Test+ public void testFlagValue_enableResultDeniedAndResultRateLimited() {+ assertThat(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED)+ .isEqualTo(+ "com.android.appsearch.flags.enable_result_denied_and_result_rate_limited");+ }++ @Test+ public void testFlagValue_enableGetParentTypesAndIndexableNestedProperties() {+ assertThat(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)+ .isEqualTo(+ "com.android.appsearch.flags"+ + ".enable_get_parent_types_and_indexable_nested_properties");+ }++ @Test+ public void testFlagValue_enableSchemaEmbeddingPropertyConfig() {+ assertThat(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ .isEqualTo("com.android.appsearch.flags.enable_schema_embedding_property_config");+ }++ @Test+ public void testFlagValue_enableListFilterTokenizeFunction() {+ assertThat(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)+ .isEqualTo("com.android.appsearch.flags.enable_list_filter_tokenize_function");+ }++ @Test+ public void testFlagValue_enableInformationalRankingExpressions() {+ assertThat(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)+ .isEqualTo("com.android.appsearch.flags.enable_informational_ranking_expressions");+ }+}
@@ -0,0 +1,58 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+// @exportToFramework:skipFile()+package androidx.appsearch.flags;++import java.lang.annotation.ElementType;+import java.lang.annotation.Retention;+import java.lang.annotation.RetentionPolicy;+import java.lang.annotation.Target;++/**+ * Shim for real RequiresFlagsEnabled defined in Framework.+ *+ *
In Jetpack, this shim does nothing and exists only for code sync purpose.
+ *+ *
In Framework, indicates that a specific test or class should be run only if all of the given
+ * feature flags are enabled in the device's current state. Enforced by the {@code CheckFlagsRule}.+ *+ *
This annotation works together with RequiresFlagsDisabled to define the value that is
+ * required of the flag by the test for the test to run. It is an error for either a method or class+ * to require that a particular flag be both enabled and disabled.+ *+ *
If the value of a particular flag is required (by either {@code RequiresFlagsEnabled} or
+ * {@code RequiresFlagsDisabled}) by both the class and test method, then the values must be+ * consistent.+ *+ *
If the value of a one flag is required by an annotation on the class, and the value of a
+ * different flag is required by an annotation of the method, then both requirements apply.+ *+ *
With {@code CheckFlagsRule}, test(s) will be skipped with 'assumption failed' when any of the
+ * required flag on the target Android platform is disabled.+ *+ *
Both {@code SetFlagsRule} and {@code CheckFlagsRule} will fail the test if a particular flag
+ * is both set (with {@code EnableFlags} or {@code DisableFlags}) and required (with {@code+ * RequiresFlagsEnabled} or {@code RequiresFlagsDisabled}).+ */+@Retention(RetentionPolicy.RUNTIME)+@Target({ElementType.METHOD, ElementType.TYPE})+public @interface RequiresFlagsEnabled {+ /**+ * The list of the feature flags that require to be enabled. Each item is the full flag name+ * with the format {package_name}.{flag_name}.+ */+ String[] value();+}
@@ -0,0 +1,52 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.safeparcel;++import static com.google.common.truth.Truth.assertThat;++import android.os.Parcel;++import org.junit.Test;++public class PropertyParcelTest {+ @Test+ public void testTwoDimensionByteArray_serializationSupported() {+ int row = 20;+ int col = 10;+ byte[][] bytesArray = new byte[row][col];+ for (int i = 0; i < row; ++i) {+ for (int j = 0; j < col; ++j) {+ bytesArray[i][j] = (byte) (i + j);+ }+ }++ String propertyName = "propertyName";+ PropertyParcel expectedPropertyParcel =+ new PropertyParcel.Builder(propertyName).setBytesValues(bytesArray).build();+ Parcel data = Parcel.obtain();+ try {+ data.writeParcelable(expectedPropertyParcel, /* flags= */ 0);+ data.setDataPosition(0);+ @SuppressWarnings("deprecation")+ PropertyParcel actualPropertyParcel = data.readParcelable(+ PropertyParcelTest.class.getClassLoader());+ assertThat(expectedPropertyParcel).isEqualTo(actualPropertyParcel);+ } finally {+ data.recycle();+ }+ }+}
* AppSearchSchema.StringPropertyConfig#INDEXING_TYPE_NONE} (the field will not be indexed * and cannot be queried).- * TODO(b/171857731) renamed to TermMatchType when using String-specific indexing config. */ @AppSearchSchema.StringPropertyConfig.IndexingType int indexingType() default AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;@@ -540,6 +540,42 @@
} /**+ * Configures an {@link EmbeddingVector} field of a class as a property known to AppSearch.+ */+ @Documented+ @Retention(RetentionPolicy.CLASS)+ @Target({ElementType.FIELD, ElementType.METHOD})+ @interface EmbeddingProperty {+ /**+ * The name of this property. This string is used to query against this property.+ *+ *
If not specified, the name of the field in the code will be used instead.
+ */+ String name() default "";++ /**+ * Configures how a property should be indexed so that it can be retrieved by queries.+ *+ *
If not specified, defaults to
+ * {@link AppSearchSchema.EmbeddingPropertyConfig#INDEXING_TYPE_NONE} (the field will not be+ * indexed and cannot be queried).+ */+ @AppSearchSchema.EmbeddingPropertyConfig.IndexingType int indexingType()+ default AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;++ /**+ * Configures whether this property must be specified for the document to be valid.+ *+ *
This attribute does not apply to properties of a repeated type (e.g. a list).
+ *+ *
Please make sure you understand the consequences of required fields on
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before+ * setting this attribute to {@code true}.+ */+ boolean required() default false;+ }++ /** * Marks a static method or a builder class directly as a builder producer. A builder class * should contain a "build()" method to construct the AppSearch document object and setter * methods to set field values.
@@ -0,0 +1,73 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.app;++import android.content.Context;+import android.os.UserHandle;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RestrictTo;++import java.io.File;+import java.util.concurrent.BlockingQueue;+import java.util.concurrent.ExecutorService;+import java.util.concurrent.TimeUnit;++/**+ * An interface which exposes environment specific methods for AppSearch.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public interface AppSearchEnvironment {++ /** Returns the directory to initialize appsearch based on the environment. */+ @NonNull+ File getAppSearchDir(@NonNull Context context, @Nullable UserHandle userHandle);++ /** Returns the correct context for the user based on the environment. */+ @NonNull+ Context createContextAsUser(@NonNull Context context, @NonNull UserHandle userHandle);++ /** Returns an ExecutorService based on given parameters. */+ @NonNull+ ExecutorService createExecutorService(+ int corePoolSize,+ int maxConcurrency,+ long keepAliveTime,+ @NonNull TimeUnit unit,+ @NonNull BlockingQueue workQueue,+ int priority);++ /** Returns an ExecutorService with a single thread. */+ @NonNull+ ExecutorService createSingleThreadExecutor();++ /** Creates and returns an Executor with cached thread pools. */+ @NonNull+ ExecutorService createCachedThreadPoolExecutor();++ /**+ * Returns a cache directory for creating temporary files like in case of migrating documents.+ */+ @Nullable+ File getCacheDir(@NonNull Context context);++ /** Returns if we can log INFO level logs. */+ boolean isInfoLoggingEnabled();+}
@@ -0,0 +1,59 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+// @exportToFramework:skipFile()+package androidx.appsearch.app;++import androidx.annotation.NonNull;+import androidx.annotation.RestrictTo;+import androidx.annotation.VisibleForTesting;++/**+ * This is a factory class for implementations needed based on the environment.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public class AppSearchEnvironmentFactory {+ private static volatile AppSearchEnvironment sAppSearchEnvironment;++ /** Returns the singleton instance of {@link AppSearchEnvironment}. */+ @NonNull+ public static AppSearchEnvironment getEnvironmentInstance() {+ AppSearchEnvironment localRef = sAppSearchEnvironment;+ if (localRef == null) {+ synchronized (AppSearchEnvironmentFactory.class) {+ localRef = sAppSearchEnvironment;+ if (localRef == null) {+ sAppSearchEnvironment = localRef =+ new JetpackAppSearchEnvironment();+ }+ }+ }+ return localRef;+ }++ /** Sets an instance of {@link AppSearchEnvironment}. for testing.*/+ @VisibleForTesting+ public static void setEnvironmentInstanceForTest(+ @NonNull AppSearchEnvironment appSearchEnvironment) {+ synchronized (AppSearchEnvironmentFactory.class) {+ sAppSearchEnvironment = appSearchEnvironment;+ }+ }++ private AppSearchEnvironmentFactory() {+ }+}
@@ -22,6 +22,8 @@
import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.appsearch.exceptions.AppSearchException;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags; import androidx.appsearch.util.LogUtil; import androidx.core.util.ObjectsCompat; import androidx.core.util.Preconditions;@@ -54,6 +56,7 @@
RESULT_SECURITY_ERROR, RESULT_DENIED, RESULT_RATE_LIMITED,+ RESULT_TIMED_OUT }) @RestrictTo(RestrictTo.Scope.LIBRARY) @Retention(RetentionPolicy.SOURCE)@@ -101,20 +104,22 @@
/** * The requested operation is denied for the caller. This error is logged and returned for * denylist rejections.- * */- // TODO(b/279047435): unhide this the next time we can make API changes+ @FlaggedApi(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED) public static final int RESULT_DENIED = 9; /**- * The caller has hit AppSearch's rate limit and the requested operation has been rejected.- * + * The caller has hit AppSearch's rate limit and the requested operation has been rejected. The+ * caller is recommended to reschedule tasks with exponential backoff. */- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- // TODO(b/279047435): unhide this the next time we can make API changes+ @FlaggedApi(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED) public static final int RESULT_RATE_LIMITED = 10;- private final @ResultCode int mResultCode;+ /** The operation was timed out. */+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)+ public static final int RESULT_TIMED_OUT = 11;++ @ResultCode private final int mResultCode; @Nullable private final ValueType mResultValue; @Nullable private final String mErrorMessage;
@@ -18,6 +18,8 @@
import android.annotation.SuppressLint; import android.os.Bundle;+import android.os.Parcel;+import android.os.Parcelable; import androidx.annotation.IntDef; import androidx.annotation.NonNull;@@ -27,7 +29,15 @@
import androidx.appsearch.annotation.CanIgnoreReturnValue; import androidx.appsearch.exceptions.AppSearchException; import androidx.appsearch.exceptions.IllegalSchemaException;-import androidx.appsearch.util.BundleUtil;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags;+import androidx.appsearch.safeparcel.AbstractSafeParcelable;+import androidx.appsearch.safeparcel.PropertyConfigParcel;+import androidx.appsearch.safeparcel.PropertyConfigParcel.DocumentIndexingConfigParcel;+import androidx.appsearch.safeparcel.PropertyConfigParcel.JoinableConfigParcel;+import androidx.appsearch.safeparcel.PropertyConfigParcel.StringIndexingConfigParcel;+import androidx.appsearch.safeparcel.SafeParcelable;+import androidx.appsearch.safeparcel.stub.StubCreators.AppSearchSchemaCreator; import androidx.appsearch.util.IndentingStringBuilder; import androidx.collection.ArraySet; import androidx.core.util.ObjectsCompat;@@ -41,6 +51,7 @@
import java.util.Collections; import java.util.LinkedHashSet; import java.util.List;+import java.util.Objects; import java.util.Set; /**@@ -52,28 +63,36 @@
* * @see AppSearchSession#setSchemaAsync */-public final class AppSearchSchema {- private static final String SCHEMA_TYPE_FIELD = "schemaType";- private static final String PROPERTIES_FIELD = "properties";- private static final String PARENT_TYPES_FIELD = "parentTypes";-- private final Bundle mBundle;-- /** @exportToFramework:hide */[email protected](creator = "AppSearchSchemaCreator")+@SuppressWarnings("HiddenSuperclass")+public final class AppSearchSchema extends AbstractSafeParcelable { @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- public AppSearchSchema(@NonNull Bundle bundle) {- Preconditions.checkNotNull(bundle);- mBundle = bundle;- }-- /**- * Returns the {@link Bundle} populated by this builder.- * @exportToFramework:hide- */- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) @NonNull- public Bundle getBundle() {- return mBundle;+ public static final Parcelable.Creator CREATOR = new AppSearchSchemaCreator();++ @Field(id = 1, getter = "getSchemaType")+ private final String mSchemaType;++ @Field(id = 2)+ final List mPropertyConfigParcels;++ @Field(id = 3, getter = "getParentTypes")+ private final List mParentTypes;++ @Field(id = 4, getter = "getDescription")+ private final String mDescription;++ @Constructor+ AppSearchSchema(+ @Param(id = 1) @NonNull String schemaType,+ @Param(id = 2) @NonNull List propertyConfigParcels,+ @Param(id = 3) @NonNull List parentTypes,+ @Param(id = 4) @NonNull String description) {+ mSchemaType = Objects.requireNonNull(schemaType);+ mPropertyConfigParcels = Objects.requireNonNull(propertyConfigParcels);+ mParentTypes = Objects.requireNonNull(parentTypes);+ mDescription = Objects.requireNonNull(description); } @Override@@ -88,7 +107,7 @@
* Appends a debugging string for the {@link AppSearchSchema} instance to the given string * builder. *- * @param builder the builder to append to.+ * @param builder the builder to append to. */ private void appendAppSearchSchemaString(@NonNull IndentingStringBuilder builder) { Preconditions.checkNotNull(builder);@@ -96,6 +115,7 @@
builder.append("{\n"); builder.increaseIndentLevel(); builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");+ builder.append("description: \"").append(getDescription()).append("\",\n"); builder.append("properties: [\n"); AppSearchSchema.PropertyConfig[] sortedProperties = getProperties()@@ -121,7 +141,22 @@
/** Returns the name of this schema type, such as Email. */ @NonNull public String getSchemaType() {- return mBundle.getString(SCHEMA_TYPE_FIELD, "");+ return mSchemaType;+ }++ /**+ * Returns a natural language description of this schema type.+ *+ *
Ex. The description for an Email type could be "A type of electronic message".
+ *+ *
This information is purely to help apps consuming this type to understand its semantic
+ * meaning. This field has no effect in AppSearch - it is just stored with the AppSearchSchema.+ * If {@link Builder#setDescription} is uncalled, then this method will return an empty string.+ */+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)+ @NonNull+ public String getDescription() {+ return mDescription; } /**@@ -130,35 +165,25 @@
*
This method creates a new list when called.
*/ @NonNull- @SuppressWarnings({"MixedMutabilityReturnType", "deprecation"})+ @SuppressWarnings({"MixedMutabilityReturnType"}) public List getProperties() {- ArrayList propertyBundles =- mBundle.getParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD);- if (propertyBundles == null || propertyBundles.isEmpty()) {+ if (mPropertyConfigParcels.isEmpty()) { return Collections.emptyList(); }- List ret = new ArrayList<>(propertyBundles.size());- for (int i = 0; i < propertyBundles.size(); i++) {- ret.add(PropertyConfig.fromBundle(propertyBundles.get(i)));+ List ret = new ArrayList<>(mPropertyConfigParcels.size());+ for (int i = 0; i < mPropertyConfigParcels.size(); i++) {+ ret.add(PropertyConfig.fromParcel(mPropertyConfigParcels.get(i))); } return ret; } /** * Returns the list of parent types of this schema for polymorphism.- *- * */+ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES) @NonNull public List getParentTypes() {- List parentTypes = mBundle.getStringArrayList(AppSearchSchema.PARENT_TYPES_FIELD);- if (parentTypes == null) {- return Collections.emptyList();- }- return Collections.unmodifiableList(parentTypes);+ return Collections.unmodifiableList(mParentTypes); } @Override@@ -173,6 +198,9 @@
if (!getSchemaType().equals(otherSchema.getSchemaType())) { return false; }+ if (!getDescription().equals(otherSchema.getDescription())) {+ return false;+ } if (!getParentTypes().equals(otherSchema.getParentTypes())) { return false; }@@ -181,21 +209,51 @@
@Override public int hashCode() {- return ObjectsCompat.hash(getSchemaType(), getProperties(), getParentTypes());+ return ObjectsCompat.hash(+ getSchemaType(),+ getProperties(),+ getParentTypes(),+ getDescription());+ }++ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @Override+ public void writeToParcel(@NonNull Parcel dest, int flags) {+ AppSearchSchemaCreator.writeToParcel(this, dest, flags); } /** Builder for {@link AppSearchSchema objects}. */ public static final class Builder { private final String mSchemaType;- private ArrayList mPropertyBundles = new ArrayList<>();+ private String mDescription = "";+ private ArrayList mPropertyConfigParcels = new ArrayList<>(); private LinkedHashSet mParentTypes = new LinkedHashSet<>(); private final Set mPropertyNames = new ArraySet<>(); private boolean mBuilt = false; /** Creates a new {@link AppSearchSchema.Builder}. */ public Builder(@NonNull String schemaType) {- Preconditions.checkNotNull(schemaType);- mSchemaType = schemaType;+ mSchemaType = Preconditions.checkNotNull(schemaType);+ }++ /**+ * Sets a natural language description of this schema type.+ *+ *
For more details about the description field, see {@link
+ * AppSearchSchema#getDescription}.+ */+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_SET_DESCRIPTION)+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)+ @CanIgnoreReturnValue+ @NonNull+ public AppSearchSchema.Builder setDescription(@NonNull String description) {+ Objects.requireNonNull(description);+ resetIfBuilt();+ mDescription = description;+ return this; } /** Adds a property to the given type. */@@ -208,7 +266,7 @@
if (!mPropertyNames.add(name)) { throw new IllegalSchemaException("Property defined more than once: " + name); }- mPropertyBundles.add(propertyConfig.mBundle);+ mPropertyConfigParcels.add(propertyConfig.mPropertyConfigParcel); return this; }@@ -273,11 +331,9 @@
*/ @CanIgnoreReturnValue @NonNull- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SCHEMA_ADD_PARENT_TYPE)- // @exportToFramework:endStrip() public AppSearchSchema.Builder addParentType(@NonNull String parentSchemaType) { Preconditions.checkNotNull(parentSchemaType); resetIfBuilt();@@ -288,18 +344,16 @@
/** Constructs a new {@link AppSearchSchema} from the contents of this builder. */ @NonNull public AppSearchSchema build() {- Bundle bundle = new Bundle();- bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType);- bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles);- bundle.putStringArrayList(AppSearchSchema.PARENT_TYPES_FIELD,- new ArrayList<>(mParentTypes)); mBuilt = true;- return new AppSearchSchema(bundle);+ return new AppSearchSchema(mSchemaType,+ mPropertyConfigParcels,+ new ArrayList<>(mParentTypes),+ mDescription); } private void resetIfBuilt() { if (mBuilt) {- mPropertyBundles = new ArrayList<>(mPropertyBundles);+ mPropertyConfigParcels = new ArrayList<>(mPropertyConfigParcels); mParentTypes = new LinkedHashSet<>(mParentTypes); mBuilt = false; }@@ -313,10 +367,6 @@
* a property. */ public abstract static class PropertyConfig {- static final String NAME_FIELD = "name";- static final String DATA_TYPE_FIELD = "dataType";- static final String CARDINALITY_FIELD = "cardinality";- /** * Physical data-types of the contents of the property. *@@ -333,28 +383,47 @@
DATA_TYPE_BOOLEAN, DATA_TYPE_BYTES, DATA_TYPE_DOCUMENT,+ DATA_TYPE_EMBEDDING, }) @Retention(RetentionPolicy.SOURCE)- public @interface DataType {}+ public @interface DataType {+ }- /** @exportToFramework:hide */+ /**+ * Constant value for String data type.+ *+ * @exportToFramework:hide+ */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static final int DATA_TYPE_STRING = 1;- /** @exportToFramework:hide */+ /**+ * Constant value for Long data type.+ *+ * @exportToFramework:hide+ */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static final int DATA_TYPE_LONG = 2;- /** @exportToFramework:hide */+ /**+ * Constant value for Double data type.+ *+ * @exportToFramework:hide+ */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static final int DATA_TYPE_DOUBLE = 3;- /** @exportToFramework:hide */+ /**+ * Constant value for Boolean data type.+ *+ * @exportToFramework:hide+ */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static final int DATA_TYPE_BOOLEAN = 4; /** * Unstructured BLOB.+ * * @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)@@ -364,12 +433,21 @@
* Indicates that the property is itself a {@link GenericDocument}, making it part of a * hierarchical schema. Any property using this DataType MUST have a valid * {@link PropertyConfig#getSchemaType}.+ * * @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static final int DATA_TYPE_DOCUMENT = 6; /**+ * Indicates that the property is an {@link EmbeddingVector}.+ *+ * @exportToFramework:hide+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ public static final int DATA_TYPE_EMBEDDING = 7;++ /** * The cardinality of the property (whether it is required, optional or repeated). * *
NOTE: The integer values of these constants must match the proto enum constants in
@@ -384,7 +462,8 @@
CARDINALITY_REQUIRED, }) @Retention(RetentionPolicy.SOURCE)- public @interface Cardinality {}+ public @interface Cardinality {+ } /** Any number of items (including zero) [0...*]. */ public static final int CARDINALITY_REPEATED = 1;@@ -395,13 +474,10 @@
/** Exactly one value [1]. */ public static final int CARDINALITY_REQUIRED = 3;- final Bundle mBundle;+ final PropertyConfigParcel mPropertyConfigParcel;- @Nullable- private Integer mHashCode;-- PropertyConfig(@NonNull Bundle bundle) {- mBundle = Preconditions.checkNotNull(bundle);+ PropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {+ mPropertyConfigParcel = Preconditions.checkNotNull(propertyConfigParcel); } @Override@@ -416,7 +492,7 @@
* Appends a debug string for the {@link AppSearchSchema.PropertyConfig} instance to the * given string builder. *- * @param builder the builder to append to.+ * @param builder the builder to append to. */ void appendPropertyConfigString(@NonNull IndentingStringBuilder builder) { Preconditions.checkNotNull(builder);@@ -424,6 +500,7 @@
builder.append("{\n"); builder.increaseIndentLevel(); builder.append("name: \"").append(getName()).append("\",\n");+ builder.append("description: \"").append(getDescription()).append("\",\n"); if (this instanceof AppSearchSchema.StringPropertyConfig) { ((StringPropertyConfig) this)@@ -469,6 +546,9 @@
case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT: builder.append("dataType: DATA_TYPE_DOCUMENT,\n"); break;+ case PropertyConfig.DATA_TYPE_EMBEDDING:+ builder.append("dataType: DATA_TYPE_EMBEDDING,\n");+ break; default: builder.append("dataType: DATA_TYPE_UNKNOWN,\n"); }@@ -479,7 +559,24 @@
/** Returns the name of this property. */ @NonNull public String getName() {- return mBundle.getString(NAME_FIELD, "");+ return mPropertyConfigParcel.getName();+ }++ /**+ * Returns a natural language description of this property.+ *+ *
Ex. The description for the "homeAddress" property of a "Person" type could be "the
+ * address at which this person lives".+ *+ *
This information is purely to help apps consuming this type the semantic meaning of
+ * its properties. This field has no effect in AppSearch - it is just stored with the+ * AppSearchSchema. If the description is not set, then this method will return an empty+ * string.+ */+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)+ @NonNull+ public String getDescription() {+ return mPropertyConfigParcel.getDescription(); } /**@@ -490,7 +587,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY) @DataType public int getDataType() {- return mBundle.getInt(DATA_TYPE_FIELD, -1);+ return mPropertyConfigParcel.getDataType(); } /**@@ -498,7 +595,7 @@
*/ @Cardinality public int getCardinality() {- return mBundle.getInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);+ return mPropertyConfigParcel.getCardinality(); } @Override@@ -510,15 +607,12 @@
return false; } PropertyConfig otherProperty = (PropertyConfig) other;- return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle);+ return ObjectsCompat.equals(mPropertyConfigParcel, otherProperty.mPropertyConfigParcel); } @Override public int hashCode() {- if (mHashCode == null) {- mHashCode = BundleUtil.deepHashCode(mBundle);- }- return mHashCode;+ return mPropertyConfigParcel.hashCode(); } /**@@ -528,41 +622,40 @@
*
The bundle is not cloned.
* * @throws IllegalArgumentException if the bundle does no contain a recognized- * value in its {@code DATA_TYPE_FIELD}.+ * value in its {@code DATA_TYPE_FIELD}. * @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @NonNull- public static PropertyConfig fromBundle(@NonNull Bundle propertyBundle) {- switch (propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)) {+ public static PropertyConfig fromParcel(+ @NonNull PropertyConfigParcel propertyConfigParcel) {+ Preconditions.checkNotNull(propertyConfigParcel);+ switch (propertyConfigParcel.getDataType()) { case PropertyConfig.DATA_TYPE_STRING:- return new StringPropertyConfig(propertyBundle);+ return new StringPropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_LONG:- return new LongPropertyConfig(propertyBundle);+ return new LongPropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_DOUBLE:- return new DoublePropertyConfig(propertyBundle);+ return new DoublePropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_BOOLEAN:- return new BooleanPropertyConfig(propertyBundle);+ return new BooleanPropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_BYTES:- return new BytesPropertyConfig(propertyBundle);+ return new BytesPropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_DOCUMENT:- return new DocumentPropertyConfig(propertyBundle);+ return new DocumentPropertyConfig(propertyConfigParcel);+ case PropertyConfig.DATA_TYPE_EMBEDDING:+ return new EmbeddingPropertyConfig(propertyConfigParcel); default: throw new IllegalArgumentException( "Unsupported property bundle of type "- + propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)- + "; contents: " + propertyBundle);+ + propertyConfigParcel.getDataType()+ + "; contents: " + propertyConfigParcel); } } } /** Configuration for a property of type String in a Document. */ public static final class StringPropertyConfig extends PropertyConfig {- private static final String INDEXING_TYPE_FIELD = "indexingType";- private static final String TOKENIZER_TYPE_FIELD = "tokenizerType";- private static final String JOINABLE_VALUE_TYPE_FIELD = "joinableValueType";- private static final String DELETION_PROPAGATION_FIELD = "deletionPropagation";- /** * Encapsulates the configurations on how AppSearch should query/index these terms. * @exportToFramework:hide@@ -574,7 +667,8 @@
INDEXING_TYPE_PREFIXES, }) @Retention(RetentionPolicy.SOURCE)- public @interface IndexingType {}+ public @interface IndexingType {+ } /** Content in this property will not be tokenized or indexed. */ public static final int INDEXING_TYPE_NONE = 0;@@ -611,7 +705,8 @@
TOKENIZER_TYPE_RFC822 }) @Retention(RetentionPolicy.SOURCE)- public @interface TokenizerType {}+ public @interface TokenizerType {+ } /** * This value indicates that no tokens should be extracted from this property.@@ -646,11 +741,9 @@
*
It is only valid for tokenizer_type to be 'VERBATIM' if {@link #getIndexingType} is
* {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}. */-// @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.VERBATIM_SEARCH)-// @exportToFramework:endStrip() public static final int TOKENIZER_TYPE_VERBATIM = 2; /**@@ -663,11 +756,9 @@
*
It is only valid for tokenizer_type to be 'RFC822' if {@link #getIndexingType} is
* {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}. */-// @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.TOKENIZER_TYPE_RFC822)-// @exportToFramework:endStrip() public static final int TOKENIZER_TYPE_RFC822 = 3; /**@@ -684,7 +775,8 @@
}) @RestrictTo(RestrictTo.Scope.LIBRARY) @Retention(RetentionPolicy.SOURCE)- public @interface JoinableValueType {}+ public @interface JoinableValueType {+ } /** Content in this property is not joinable. */ public static final int JOINABLE_VALUE_TYPE_NONE = 0;@@ -700,27 +792,37 @@
* {@link PropertyConfig#CARDINALITY_REQUIRED}. * */- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.JOIN_SPEC_AND_QUALIFIED_ID)- // @exportToFramework:endStrip() public static final int JOINABLE_VALUE_TYPE_QUALIFIED_ID = 1;- StringPropertyConfig(@NonNull Bundle bundle) {- super(bundle);+ StringPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {+ super(propertyConfigParcel); } /** Returns how the property is indexed. */- @IndexingType+ @StringPropertyConfig.IndexingType public int getIndexingType() {- return mBundle.getInt(INDEXING_TYPE_FIELD);+ StringIndexingConfigParcel indexingConfigParcel =+ mPropertyConfigParcel.getStringIndexingConfigParcel();+ if (indexingConfigParcel == null) {+ return INDEXING_TYPE_NONE;+ }++ return indexingConfigParcel.getIndexingType(); } /** Returns how this property is tokenized (split into words). */ @TokenizerType public int getTokenizerType() {- return mBundle.getInt(TOKENIZER_TYPE_FIELD);+ StringIndexingConfigParcel indexingConfigParcel =+ mPropertyConfigParcel.getStringIndexingConfigParcel();+ if (indexingConfigParcel == null) {+ return TOKENIZER_TYPE_NONE;+ }++ return indexingConfigParcel.getTokenizerType(); } /**@@ -728,27 +830,27 @@
*/ @JoinableValueType public int getJoinableValueType() {- return mBundle.getInt(JOINABLE_VALUE_TYPE_FIELD, JOINABLE_VALUE_TYPE_NONE);- }+ JoinableConfigParcel joinableConfigParcel = mPropertyConfigParcel+ .getJoinableConfigParcel();+ if (joinableConfigParcel == null) {+ return JOINABLE_VALUE_TYPE_NONE;+ }- /**- * Returns whether or not documents in this schema should be deleted when the document- * referenced by this field is deleted.- *- * @see JoinSpec- * @- */- public boolean getDeletionPropagation() {- return mBundle.getBoolean(DELETION_PROPAGATION_FIELD, false);+ return joinableConfigParcel.getJoinableValueType(); } /** Builder for {@link StringPropertyConfig}. */ public static final class Builder { private final String mPropertyName;- @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;- @IndexingType private int mIndexingType = INDEXING_TYPE_NONE;- @TokenizerType private int mTokenizerType = TOKENIZER_TYPE_NONE;- @JoinableValueType private int mJoinableValueType = JOINABLE_VALUE_TYPE_NONE;+ private String mDescription = "";+ @Cardinality+ private int mCardinality = CARDINALITY_OPTIONAL;+ @StringPropertyConfig.IndexingType+ private int mIndexingType = INDEXING_TYPE_NONE;+ @TokenizerType+ private int mTokenizerType = TOKENIZER_TYPE_NONE;+ @JoinableValueType+ private int mJoinableValueType = JOINABLE_VALUE_TYPE_NONE; private boolean mDeletionPropagation = false; /** Creates a new {@link StringPropertyConfig.Builder}. */@@ -757,6 +859,24 @@
} /**+ * Sets a natural language description of this property.+ *+ *
For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.+ */+ @CanIgnoreReturnValue+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_SET_DESCRIPTION)+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass+ @NonNull+ public StringPropertyConfig.Builder setDescription(@NonNull String description) {+ mDescription = Objects.requireNonNull(description);+ return this;+ }++ /** * Sets the cardinality of the property (whether it is optional, required or repeated). * *
If this method is not called, the default cardinality is
@@ -781,7 +901,8 @@
*/ @CanIgnoreReturnValue @NonNull- public StringPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {+ public StringPropertyConfig.Builder setIndexingType(+ @StringPropertyConfig.IndexingType int indexingType) { Preconditions.checkArgumentInRange( indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType"); mIndexingType = indexingType;@@ -830,25 +951,6 @@
} /**- * Configures whether or not documents in this schema will be removed when the document- * referred to by this property is deleted.- *- *
Requires that a joinable value type is set.
- * @- */- @SuppressWarnings("MissingGetterMatchingBuilder") // getDeletionPropagation- @NonNull- // @exportToFramework:startStrip()- @RequiresFeature(- enforcement = "androidx.appsearch.app.Features#isFeatureSupported",- name = Features.SCHEMA_SET_DELETION_PROPAGATION)- // @exportToFramework:endStrip()- public Builder setDeletionPropagation(boolean deletionPropagation) {- mDeletionPropagation = deletionPropagation;- return this;- }-- /** * Constructs a new {@link StringPropertyConfig} from the contents of this builder. */ @NonNull@@ -868,15 +970,17 @@
Preconditions.checkState(!mDeletionPropagation, "Cannot set deletion " + "propagation without setting a joinable value type"); }- Bundle bundle = new Bundle();- bundle.putString(NAME_FIELD, mPropertyName);- bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_STRING);- bundle.putInt(CARDINALITY_FIELD, mCardinality);- bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType);- bundle.putInt(TOKENIZER_TYPE_FIELD, mTokenizerType);- bundle.putInt(JOINABLE_VALUE_TYPE_FIELD, mJoinableValueType);- bundle.putBoolean(DELETION_PROPAGATION_FIELD, mDeletionPropagation);- return new StringPropertyConfig(bundle);+ PropertyConfigParcel.StringIndexingConfigParcel stringConfigParcel =+ new StringIndexingConfigParcel(mIndexingType, mTokenizerType);+ JoinableConfigParcel joinableConfigParcel =+ new JoinableConfigParcel(mJoinableValueType, mDeletionPropagation);+ return new StringPropertyConfig(+ PropertyConfigParcel.createForString(+ mPropertyName,+ mDescription,+ mCardinality,+ stringConfigParcel,+ joinableConfigParcel)); } }@@ -886,7 +990,7 @@
* *
This appends fields specific to a {@link StringPropertyConfig} instance.
*- * @param builder the builder to append to.+ * @param builder the builder to append to. */ void appendStringPropertyConfigFields(@NonNull IndentingStringBuilder builder) { switch (getIndexingType()) {@@ -935,11 +1039,10 @@
/** Configuration for a property containing a 64-bit integer. */ public static final class LongPropertyConfig extends PropertyConfig {- private static final String INDEXING_TYPE_FIELD = "indexingType";- /** * Encapsulates the configurations on how AppSearch should query/index these 64-bit * integers.+ * * @exportToFramework:hide */ @IntDef(value = {@@ -948,7 +1051,8 @@
}) @RestrictTo(RestrictTo.Scope.LIBRARY) @Retention(RetentionPolicy.SOURCE)- public @interface IndexingType {}+ public @interface IndexingType {+ } /** Content in this property will not be indexed. */ public static final int INDEXING_TYPE_NONE = 0;@@ -959,28 +1063,34 @@
* *
For example, a property with 1024 should match numeric search range query [0, 2000].
*/- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.NUMERIC_SEARCH)- // @exportToFramework:endStrip() public static final int INDEXING_TYPE_RANGE = 1;- LongPropertyConfig(@NonNull Bundle bundle) {- super(bundle);+ LongPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {+ super(propertyConfigParcel); } /** Returns how the property is indexed. */- @IndexingType+ @LongPropertyConfig.IndexingType public int getIndexingType() {- return mBundle.getInt(INDEXING_TYPE_FIELD, INDEXING_TYPE_NONE);+ PropertyConfigParcel.IntegerIndexingConfigParcel indexingConfigParcel =+ mPropertyConfigParcel.getIntegerIndexingConfigParcel();+ if (indexingConfigParcel == null) {+ return INDEXING_TYPE_NONE;+ }+ return indexingConfigParcel.getIndexingType(); } /** Builder for {@link LongPropertyConfig}. */ public static final class Builder { private final String mPropertyName;- @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;- @IndexingType private int mIndexingType = INDEXING_TYPE_NONE;+ private String mDescription = "";+ @Cardinality+ private int mCardinality = CARDINALITY_OPTIONAL;+ @LongPropertyConfig.IndexingType+ private int mIndexingType = INDEXING_TYPE_NONE; /** Creates a new {@link LongPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) {@@ -988,6 +1098,24 @@
} /**+ * Sets a natural language description of this property.+ *+ *
For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.+ */+ @CanIgnoreReturnValue+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_SET_DESCRIPTION)+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass+ @NonNull+ public LongPropertyConfig.Builder setDescription(@NonNull String description) {+ mDescription = Objects.requireNonNull(description);+ return this;+ }++ /** * Sets the cardinality of the property (whether it is optional, required or repeated). * *
If this method is not called, the default cardinality is
@@ -1012,7 +1140,8 @@
*/ @CanIgnoreReturnValue @NonNull- public LongPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {+ public LongPropertyConfig.Builder setIndexingType(+ @LongPropertyConfig.IndexingType int indexingType) { Preconditions.checkArgumentInRange( indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_RANGE, "indexingType"); mIndexingType = indexingType;@@ -1022,12 +1151,9 @@
/** Constructs a new {@link LongPropertyConfig} from the contents of this builder. */ @NonNull public LongPropertyConfig build() {- Bundle bundle = new Bundle();- bundle.putString(NAME_FIELD, mPropertyName);- bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_LONG);- bundle.putInt(CARDINALITY_FIELD, mCardinality);- bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType);- return new LongPropertyConfig(bundle);+ return new LongPropertyConfig(+ PropertyConfigParcel.createForLong(+ mPropertyName, mDescription, mCardinality, mIndexingType)); } }@@ -1037,7 +1163,7 @@
* *
This appends fields specific to a {@link LongPropertyConfig} instance.
*- * @param builder the builder to append to.+ * @param builder the builder to append to. */ void appendLongPropertyConfigFields(@NonNull IndentingStringBuilder builder) { switch (getIndexingType()) {@@ -1055,14 +1181,16 @@
/** Configuration for a property containing a double-precision decimal number. */ public static final class DoublePropertyConfig extends PropertyConfig {- DoublePropertyConfig(@NonNull Bundle bundle) {- super(bundle);+ DoublePropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {+ super(propertyConfigParcel); } /** Builder for {@link DoublePropertyConfig}. */ public static final class Builder { private final String mPropertyName;- @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;+ private String mDescription = "";+ @Cardinality+ private int mCardinality = CARDINALITY_OPTIONAL; /** Creates a new {@link DoublePropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) {@@ -1070,6 +1198,24 @@
} /**+ * Sets a natural language description of this property.+ *+ *
For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.+ */+ @CanIgnoreReturnValue+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_SET_DESCRIPTION)+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass+ @NonNull+ public DoublePropertyConfig.Builder setDescription(@NonNull String description) {+ mDescription = Objects.requireNonNull(description);+ return this;+ }++ /** * Sets the cardinality of the property (whether it is optional, required or repeated). * *
If this method is not called, the default cardinality is
@@ -1088,25 +1234,25 @@
/** Constructs a new {@link DoublePropertyConfig} from the contents of this builder. */ @NonNull public DoublePropertyConfig build() {- Bundle bundle = new Bundle();- bundle.putString(NAME_FIELD, mPropertyName);- bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOUBLE);- bundle.putInt(CARDINALITY_FIELD, mCardinality);- return new DoublePropertyConfig(bundle);+ return new DoublePropertyConfig(+ PropertyConfigParcel.createForDouble(+ mPropertyName, mDescription, mCardinality)); } } } /** Configuration for a property containing a boolean. */ public static final class BooleanPropertyConfig extends PropertyConfig {- BooleanPropertyConfig(@NonNull Bundle bundle) {- super(bundle);+ BooleanPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {+ super(propertyConfigParcel); } /** Builder for {@link BooleanPropertyConfig}. */ public static final class Builder { private final String mPropertyName;- @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;+ private String mDescription = "";+ @Cardinality+ private int mCardinality = CARDINALITY_OPTIONAL; /** Creates a new {@link BooleanPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) {@@ -1114,6 +1260,24 @@
} /**+ Sets a natural language description of this property.+ *+ *
For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.+ */+ @CanIgnoreReturnValue+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_SET_DESCRIPTION)+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass+ @NonNull+ public BooleanPropertyConfig.Builder setDescription(@NonNull String description) {+ mDescription = Objects.requireNonNull(description);+ return this;+ }++ /** * Sets the cardinality of the property (whether it is optional, required or repeated). * *
If this method is not called, the default cardinality is
@@ -1132,25 +1296,25 @@
/** Constructs a new {@link BooleanPropertyConfig} from the contents of this builder. */ @NonNull public BooleanPropertyConfig build() {- Bundle bundle = new Bundle();- bundle.putString(NAME_FIELD, mPropertyName);- bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BOOLEAN);- bundle.putInt(CARDINALITY_FIELD, mCardinality);- return new BooleanPropertyConfig(bundle);+ return new BooleanPropertyConfig(+ PropertyConfigParcel.createForBoolean(+ mPropertyName, mDescription, mCardinality)); } } } /** Configuration for a property containing a byte array. */ public static final class BytesPropertyConfig extends PropertyConfig {- BytesPropertyConfig(@NonNull Bundle bundle) {- super(bundle);+ BytesPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {+ super(propertyConfigParcel); } /** Builder for {@link BytesPropertyConfig}. */ public static final class Builder { private final String mPropertyName;- @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;+ private String mDescription = "";+ @Cardinality+ private int mCardinality = CARDINALITY_OPTIONAL; /** Creates a new {@link BytesPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) {@@ -1158,6 +1322,24 @@
} /**+ * Sets a natural language description of this property.+ *+ *
For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.+ */+ @CanIgnoreReturnValue+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_SET_DESCRIPTION)+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass+ @NonNull+ public BytesPropertyConfig.Builder setDescription(@NonNull String description) {+ mDescription = Objects.requireNonNull(description);+ return this;+ }++ /** * Sets the cardinality of the property (whether it is optional, required or repeated). * *
If this method is not called, the default cardinality is
@@ -1178,30 +1360,23 @@
*/ @NonNull public BytesPropertyConfig build() {- Bundle bundle = new Bundle();- bundle.putString(NAME_FIELD, mPropertyName);- bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BYTES);- bundle.putInt(CARDINALITY_FIELD, mCardinality);- return new BytesPropertyConfig(bundle);+ return new BytesPropertyConfig(+ PropertyConfigParcel.createForBytes(+ mPropertyName, mDescription, mCardinality)); } } } /** Configuration for a property containing another Document. */ public static final class DocumentPropertyConfig extends PropertyConfig {- private static final String SCHEMA_TYPE_FIELD = "schemaType";- private static final String INDEX_NESTED_PROPERTIES_FIELD = "indexNestedProperties";- private static final String INDEXABLE_NESTED_PROPERTIES_LIST_FIELD =- "indexableNestedPropertiesList";-- DocumentPropertyConfig(@NonNull Bundle bundle) {- super(bundle);+ DocumentPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {+ super(propertyConfigParcel); } /** Returns the logical schema-type of the contents of this document property. */ @NonNull public String getSchemaType() {- return Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD));+ return Preconditions.checkNotNull(mPropertyConfigParcel.getSchemaType()); } /**@@ -1215,24 +1390,33 @@
* indexing a subset of properties from the nested document. */ public boolean shouldIndexNestedProperties() {- return mBundle.getBoolean(INDEX_NESTED_PROPERTIES_FIELD);+ DocumentIndexingConfigParcel indexingConfigParcel =+ mPropertyConfigParcel.getDocumentIndexingConfigParcel();+ if (indexingConfigParcel == null) {+ return false;+ }++ return indexingConfigParcel.shouldIndexNestedProperties(); } /** * Returns the list of indexable nested properties for the nested document.- *- * */+ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES) @NonNull public List getIndexableNestedProperties() {+ DocumentIndexingConfigParcel indexingConfigParcel =+ mPropertyConfigParcel.getDocumentIndexingConfigParcel();+ if (indexingConfigParcel == null) {+ return Collections.emptyList();+ }+ List indexableNestedPropertiesList =- mBundle.getStringArrayList(INDEXABLE_NESTED_PROPERTIES_LIST_FIELD);+ indexingConfigParcel.getIndexableNestedPropertiesList(); if (indexableNestedPropertiesList == null) { return Collections.emptyList(); }+ return Collections.unmodifiableList(indexableNestedPropertiesList); }@@ -1240,7 +1424,9 @@
public static final class Builder { private final String mPropertyName; private final String mSchemaType;- @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;+ private String mDescription = "";+ @Cardinality+ private int mCardinality = CARDINALITY_OPTIONAL; private boolean mShouldIndexNestedProperties = false; private final Set mIndexableNestedPropertiesList = new ArraySet<>();@@ -1250,9 +1436,9 @@
* @param propertyName The logical name of the property in the schema, which will be * used as the key for this property in * {@link GenericDocument.Builder#setPropertyDocument}.- * @param schemaType The type of documents which will be stored in this property.- * Documents of different types cannot be mixed into a single- * property.+ * @param schemaType The type of documents which will be stored in this property.+ * Documents of different types cannot be mixed into a single+ * property. */ public Builder(@NonNull String propertyName, @NonNull String schemaType) { mPropertyName = Preconditions.checkNotNull(propertyName);@@ -1260,6 +1446,24 @@
} /**+ * Sets a natural language description of this property.+ *+ *
For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.+ */+ @CanIgnoreReturnValue+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_SET_DESCRIPTION)+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass+ @NonNull+ public DocumentPropertyConfig.Builder setDescription(@NonNull String description) {+ mDescription = Objects.requireNonNull(description);+ return this;+ }++ /** * Sets the cardinality of the property (whether it is optional, required or repeated). * *
If this method is not called, the default cardinality is
@@ -1297,19 +1501,13 @@
* Adds one or more properties for indexing from the nested document property. * * @see #addIndexableNestedProperties(Collection)- *- * */+ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES) @CanIgnoreReturnValue @NonNull- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)- // @exportToFramework:endStrip() public DocumentPropertyConfig.Builder addIndexableNestedProperties( @NonNull String... indexableNestedProperties) { Preconditions.checkNotNull(indexableNestedProperties);@@ -1320,20 +1518,14 @@
* Adds one or more property paths for indexing from the nested document property. * * @see #addIndexableNestedProperties(Collection)- *- * */+ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES) @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") @NonNull- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)- // @exportToFramework:endStrip() public DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths( @NonNull PropertyPath... indexableNestedPropertyPaths) { Preconditions.checkNotNull(indexableNestedPropertyPaths);@@ -1371,11 +1563,9 @@
*/ @CanIgnoreReturnValue @NonNull- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)- // @exportToFramework:endStrip() public DocumentPropertyConfig.Builder addIndexableNestedProperties( @NonNull Collection indexableNestedProperties) { Preconditions.checkNotNull(indexableNestedProperties);@@ -1387,20 +1577,14 @@
* Adds one or more property paths for indexing from the nested document property. * * @see #addIndexableNestedProperties(Collection)- *- * */+ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES) @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") @NonNull- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)- // @exportToFramework:endStrip() public DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths( @NonNull Collection indexableNestedPropertyPaths) { Preconditions.checkNotNull(indexableNestedPropertyPaths);@@ -1426,15 +1610,14 @@
+ "to be false when one or more indexableNestedProperties are " + "provided."); }- Bundle bundle = new Bundle();- bundle.putString(NAME_FIELD, mPropertyName);- bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOCUMENT);- bundle.putInt(CARDINALITY_FIELD, mCardinality);- bundle.putBoolean(INDEX_NESTED_PROPERTIES_FIELD, mShouldIndexNestedProperties);- bundle.putStringArrayList(INDEXABLE_NESTED_PROPERTIES_LIST_FIELD,- new ArrayList<>(mIndexableNestedPropertiesList));- bundle.putString(SCHEMA_TYPE_FIELD, mSchemaType);- return new DocumentPropertyConfig(bundle);+ return new DocumentPropertyConfig(+ PropertyConfigParcel.createForDocument(+ mPropertyName,+ mDescription,+ mCardinality,+ mSchemaType,+ new DocumentIndexingConfigParcel(mShouldIndexNestedProperties,+ new ArrayList<>(mIndexableNestedPropertiesList)))); } }@@ -1444,7 +1627,7 @@
* *
This appends fields specific to a {@link DocumentPropertyConfig} instance.
*- * @param builder the builder to append to.+ * @param builder the builder to append to. */ void appendDocumentPropertyConfigFields(@NonNull IndentingStringBuilder builder) { builder@@ -1459,4 +1642,133 @@
builder.append("schemaType: \"").append(getSchemaType()).append("\",\n"); } }++ /**+ * Configuration for a property of type {@link EmbeddingVector} in a Document.+ */+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public static final class EmbeddingPropertyConfig extends PropertyConfig {+ /**+ * Encapsulates the configurations on how AppSearch should query/index these embedding+ * vectors.+ *+ * @exportToFramework:hide+ */+ @IntDef(value = {+ INDEXING_TYPE_NONE,+ INDEXING_TYPE_SIMILARITY+ })+ @RestrictTo(RestrictTo.Scope.LIBRARY)+ @Retention(RetentionPolicy.SOURCE)+ public @interface IndexingType {+ }++ /** Content in this property will not be indexed. */+ public static final int INDEXING_TYPE_NONE = 0;++ /**+ * Embedding vectors in this property will be indexed.+ *+ *
The index offers 100% accuracy, but has linear time complexity based on the number
+ * of embedding vectors within the index.+ */+ public static final int INDEXING_TYPE_SIMILARITY = 1;++ EmbeddingPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {+ super(propertyConfigParcel);+ }++ /** Returns how the property is indexed. */+ @EmbeddingPropertyConfig.IndexingType+ public int getIndexingType() {+ PropertyConfigParcel.EmbeddingIndexingConfigParcel indexingConfigParcel =+ mPropertyConfigParcel.getEmbeddingIndexingConfigParcel();+ if (indexingConfigParcel == null) {+ return INDEXING_TYPE_NONE;+ }+ return indexingConfigParcel.getIndexingType();+ }++ /** Builder for {@link EmbeddingPropertyConfig}. */+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public static final class Builder {+ private final String mPropertyName;+ private String mDescription = "";+ @Cardinality+ private int mCardinality = CARDINALITY_OPTIONAL;+ @EmbeddingPropertyConfig.IndexingType+ private int mIndexingType = INDEXING_TYPE_NONE;++ /** Creates a new {@link EmbeddingPropertyConfig.Builder}. */+ public Builder(@NonNull String propertyName) {+ mPropertyName = Preconditions.checkNotNull(propertyName);+ }++ /**+ * Sets a natural language description of this property.+ *+ *
For more details about the description field, see {@link
+ * AppSearchSchema.PropertyConfig#getDescription}.+ */+ @CanIgnoreReturnValue+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_SET_DESCRIPTION)+ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass+ @NonNull+ public EmbeddingPropertyConfig.Builder setDescription(@NonNull String description) {+ mDescription = Objects.requireNonNull(description);+ return this;+ }++ /**+ * Sets the cardinality of the property (whether it is optional, required or repeated).+ *+ *
If this method is not called, the default cardinality is
+ * {@link PropertyConfig#CARDINALITY_OPTIONAL}.+ */+ @CanIgnoreReturnValue+ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass+ @NonNull+ public EmbeddingPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {+ Preconditions.checkArgumentInRange(+ cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");+ mCardinality = cardinality;+ return this;+ }++ /**+ * Configures how a property should be indexed so that it can be retrieved by queries.+ *+ *
If this method is not called, the default indexing type is
+ * {@link EmbeddingPropertyConfig#INDEXING_TYPE_NONE}, so that it will not be indexed+ * and cannot be matched by queries.+ */+ @CanIgnoreReturnValue+ @NonNull+ public EmbeddingPropertyConfig.Builder setIndexingType(+ @EmbeddingPropertyConfig.IndexingType int indexingType) {+ Preconditions.checkArgumentInRange(+ indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_SIMILARITY,+ "indexingType");+ mIndexingType = indexingType;+ return this;+ }++ /**+ * Constructs a new {@link EmbeddingPropertyConfig} from the contents of this+ * builder.+ */+ @NonNull+ public EmbeddingPropertyConfig build() {+ return new EmbeddingPropertyConfig(+ PropertyConfigParcel.createForEmbedding(+ mPropertyName, mDescription, mCardinality, mIndexingType));+ }+ }+ } }
The newly added custom functions covered by this feature are:
*
*
createList(String...)
- *
search(String, List)
+ *
search(String, {@code List})
*
propertyDefined(String)
* *@@ -192,8 +192,9 @@
*
propertyDefined takes a string specifying the property of interest and matches all
* documents of any type that defines the specified property * (ex. `propertyDefined("sender.name")`). Note that propertyDefined will match so long as- * the document's type defines the specified property. It does NOT require that the document- * actually hold any values for this property.+ * the document's type defines the specified property. Unlike the "hasProperty" function+ * below, this function does NOT require that the document actually hold any values for this+ * property. * *
{@link Features#NUMERIC_SEARCH}: This feature covers numeric search expressions. In the
* query language, the values of properties that have@@ -209,6 +210,68 @@
* *
Ex. `"foo/bar" OR baz` will ensure that 'foo/bar' is treated as a single 'verbatim' token.
*+ *
{@link Features#LIST_FILTER_HAS_PROPERTY_FUNCTION}: This feature covers the
+ * "hasProperty" function in query expressions, which takes a string specifying the property+ * of interest and matches all documents that hold values for this property. Not to be+ * confused with the "propertyDefined" function, which checks whether a document's schema+ * has defined the property, instead of whether a document itself has this property.+ *+ *
Ex. `foo hasProperty("sender.name")` will return all documents that have the term "foo"
+ * AND have values in the property "sender.name". Consider two documents, documentA and+ * documentB, of the same schema with an optional property "sender.name". If documentA sets+ * "foo" in this property but documentB does not, then `hasProperty("sender.name")` will only+ * match documentA. However, `propertyDefined("sender.name")` will match both documentA and+ * documentB, regardless of whether a value is actually set.+ *+ *
{@link Features#SCHEMA_EMBEDDING_PROPERTY_CONFIG}: This feature covers the
+ * "semanticSearch" and "getSearchSpecEmbedding" functions in query expressions, which are+ * used for semantic search.+ *+ *
semanticSearch matches all documents that have at least one embedding vector with
+ * a matching model signature (see {@link EmbeddingVector#getModelSignature()}) and a+ * similarity score within the range specified based on the provided metric.+ *
getSearchSpecEmbedding({embedding_index}) retrieves the embedding search passed in
+ * {@link SearchSpec.Builder#addSearchEmbeddings} based on the index specified, which+ * starts from 0.+ *
"low" and "high" are floating point numbers that specify the similarity score
+ * range. If omitted, they default to negative and positive infinity, respectively.+ *
"metric" is a string value that specifies how embedding similarities should be
+ * calculated. If omitted, it defaults to the metric specified in+ * {@link SearchSpec.Builder#setDefaultEmbeddingSearchMetricType(int)}. Possible+ * values:+ *
Hybrid: foo OR semanticSearch(getSearchSpecEmbedding(0), 0.5, 1)
+ *
Complex: (foo OR semanticSearch(getSearchSpecEmbedding(0), 0.5, 1)) AND bar
+ * + *+ *
{@link Features#LIST_FILTER_TOKENIZE_FUNCTION}: This feature covers the
+ * "tokenize" function in query expressions, which takes a string and treats the entire string+ * as plain text. This string is then segmented, normalized and stripped of punctuation-only+ * segments. The remaining tokens are then AND'd together. This function is useful for callers+ * who wish to provide user input, but want to ensure that that user input does not invoke any+ * query operators.+ *+ *
Ex. `foo OR tokenize("bar OR baz.")`. The string "bar OR baz." will be segmented into
+ * "bar", "OR", "baz", ".". Punctuation is removed and the segments are normalized to "bar",+ * "or", "baz". This query will be equivalent to `foo OR (bar AND or AND baz)`.+ *+ *
Ex. `tokenize("\"bar\" OR \\baz")`. Quotation marks and escape characters must be escaped.
+ * This query will be segmented into "\"", "bar", "\"", "OR", "\", "baz". Once stripped of+ * punctuation and normalized, this will be equivalent to the query `bar AND or AND baz`.+ * *
The availability of each of these features can be checked by calling
* {@link Features#isFeatureSupported} with the desired feature. *@@ -223,6 +286,8 @@
* match type, etc. * @return a {@link SearchResults} object for retrieved matched documents. */+ // TODO(b/326656531): Refine the javadoc to provide guidance on the best practice of+ // embedding searches and how to select an appropriate metric. @NonNull SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
@@ -0,0 +1,125 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.app;++import android.os.Parcel;+import android.os.Parcelable;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RequiresFeature;+import androidx.annotation.RestrictTo;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags;+import androidx.appsearch.safeparcel.AbstractSafeParcelable;+import androidx.appsearch.safeparcel.SafeParcelable;+import androidx.appsearch.safeparcel.stub.StubCreators.EmbeddingVectorCreator;+import androidx.core.util.Preconditions;++import java.util.Arrays;+import java.util.Objects;++/**+ * Embeddings are vector representations of data, such as text, images, and audio, which can be+ * generated by machine learning models and used for semantic search. This class represents an+ * embedding vector, which wraps a float array for the values of the embedding vector and a model+ * signature that can be any string to distinguish between embedding vectors generated by+ * different models.+ *+ *
For more details on how embedding search works, check {@link AppSearchSession#search} and
+ * {@link SearchSpec.Builder#setRankingStrategy(String)}.+ *+ * @see SearchSpec.Builder#addSearchEmbeddings+ * @see GenericDocument.Builder#setPropertyEmbedding+ */+@RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)+@FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)[email protected](creator = "EmbeddingVectorCreator")+@SuppressWarnings("HiddenSuperclass")+public final class EmbeddingVector extends AbstractSafeParcelable {+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @NonNull+ public static final Parcelable.Creator CREATOR =+ new EmbeddingVectorCreator();+ @NonNull+ @Field(id = 1, getter = "getValues")+ private final float[] mValues;+ @NonNull+ @Field(id = 2, getter = "getModelSignature")+ private final String mModelSignature;+ @Nullable+ private Integer mHashCode;++ /**+ * Creates a new {@link EmbeddingVector}.+ *+ * @throws IllegalArgumentException if {@code values} is empty.+ */+ @Constructor+ public EmbeddingVector(+ @Param(id = 1) @NonNull float[] values,+ @Param(id = 2) @NonNull String modelSignature) {+ mValues = Preconditions.checkNotNull(values);+ if (mValues.length == 0) {+ throw new IllegalArgumentException("Embedding values cannot be empty.");+ }+ mModelSignature = Preconditions.checkNotNull(modelSignature);+ }++ /**+ * Returns the values of this embedding vector.+ */+ @NonNull+ public float[] getValues() {+ return mValues;+ }++ /**+ * Returns the model signature of this embedding vector, which is an arbitrary string to+ * distinguish between embedding vectors generated by different models.+ */+ @NonNull+ public String getModelSignature() {+ return mModelSignature;+ }++ @Override+ public boolean equals(Object o) {+ if (this == o) return true;+ if (o == null) return false;+ if (!(o instanceof EmbeddingVector)) return false;+ EmbeddingVector that = (EmbeddingVector) o;+ return Arrays.equals(mValues, that.mValues)+ && mModelSignature.equals(that.mModelSignature);+ }++ @Override+ public int hashCode() {+ if (mHashCode == null) {+ mHashCode = Objects.hash(Arrays.hashCode(mValues), mModelSignature);+ }+ return mHashCode;+ }++ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @Override+ public void writeToParcel(@NonNull Parcel dest, int flags) {+ EmbeddingVectorCreator.writeToParcel(this, dest, flags);+ }+}
@@ -0,0 +1,121 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+// @exportToFramework:skipFile()+package androidx.appsearch.app;++import android.annotation.SuppressLint;++import androidx.annotation.NonNull;+import androidx.annotation.RequiresFeature;++import com.google.common.util.concurrent.ListenableFuture;++/**+ * Provides a connection to all enterprise (work profile) AppSearch databases the querying+ * application has been granted access to.+ *+ *
This session can be created from any user profile but will only properly return results when
+ * created from the main profile. If the user is not the main profile or an associated work profile+ * does not exist, queries will still successfully complete but with empty results.+ *+ *
Schemas must be explicitly tagged enterprise and may require additional permissions to be
+ * visible from an enterprise session. Retrieved documents may also have certain fields restricted+ * or modified unlike if they were retrieved directly from {@link GlobalSearchSession} on the work+ * profile.+ *+ *
All implementations of this interface must be thread safe.
+ *+ * @see GlobalSearchSession+ */+@RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.ENTERPRISE_GLOBAL_SEARCH_SESSION)+public interface EnterpriseGlobalSearchSession {+ /**+ * Retrieves {@link GenericDocument} documents, belonging to the specified package name and+ * database name and identified by the namespace and ids in the request, from the+ * {@link EnterpriseGlobalSearchSession} database. When a call is successful, the result will be+ * returned in the successes section of the {@link AppSearchBatchResult} object in the callback.+ * If the package doesn't exist, database doesn't exist, or if the calling package doesn't have+ * access, these failures will be reflected as {@link AppSearchResult} objects with a+ * RESULT_NOT_FOUND status code in the failures section of the {@link AppSearchBatchResult}+ * object.+ *+ * @param packageName the name of the package to get from+ * @param databaseName the name of the database to get from+ * @param request a request containing a namespace and IDs of the documents to retrieve.+ */+ @NonNull+ ListenableFuture> getByDocumentIdAsync(+ @NonNull String packageName,+ @NonNull String databaseName,+ @NonNull GetByDocumentIdRequest request);++ /**+ * Retrieves documents from all enterprise (work profile) AppSearch databases that the querying+ * application has access to.+ *+ *
Applications can be granted access to documents by specifying
+ * {@link SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}, or+ * {@link SetSchemaRequest.Builder#setDocumentClassVisibilityForPackage} when building a schema.+ *+ *
Document access can also be granted to system UIs by specifying
+ * {@link SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}, or+ * {@link SetSchemaRequest.Builder#setDocumentClassDisplayedBySystem}+ * when building a schema.+ *+ *
See {@link AppSearchSession#search} for a detailed explanation on
+ * forming a query string.+ *+ *
This method is lightweight. The heavy work will be done in
+ * {@link SearchResults#getNextPageAsync}.+ *+ * @param queryExpression query string to search.+ * @param searchSpec spec for setting document filters, adding projection, setting term+ * match type, etc.+ * @return a {@link SearchResults} object for retrieved matched documents.+ */+ @NonNull+ SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);++ /**+ * Retrieves the collection of schemas most recently successfully provided to+ * {@link AppSearchSession#setSchemaAsync} for any types belonging to the requested package and+ * database that the caller has been granted access to.+ *+ *
If the requested package/database combination does not exist or the caller has not been
+ * granted access to it, then an empty GetSchemaResponse will be returned.+ *+ *+ * @param packageName the package that owns the requested {@link AppSearchSchema} instances.+ * @param databaseName the database that owns the requested {@link AppSearchSchema} instances.+ * @return The pending {@link GetSchemaResponse} containing the schemas that the caller has+ * access to or an empty GetSchemaResponse if the request package and database does not+ * exist, has not set a schema or contains no schemas that are accessible to the caller.+ */+ // This call hits disk; async API prevents us from treating these calls as properties.+ @SuppressLint("KotlinPropertyAccess")+ @NonNull+ ListenableFuture getSchemaAsync(@NonNull String packageName,+ @NonNull String databaseName);++ /**+ * Returns the {@link Features} to check for the availability of certain features+ * for this session.+ */+ @NonNull+ Features getFeatures();+}
@@ -27,13 +27,25 @@
* @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY)-public interface FeatureConstants {+public final class FeatureConstants { /** Feature constants for {@link Features#NUMERIC_SEARCH}. */- String NUMERIC_SEARCH = "NUMERIC_SEARCH";+ public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH"; /** Feature constants for {@link Features#VERBATIM_SEARCH}. */- String VERBATIM_SEARCH = "VERBATIM_SEARCH";+ public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH"; /** Feature constants for {@link Features#LIST_FILTER_QUERY_LANGUAGE}. */- String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";+ public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";++ /** Feature constants for {@link Features#LIST_FILTER_HAS_PROPERTY_FUNCTION}. */+ public static final String LIST_FILTER_HAS_PROPERTY_FUNCTION =+ "LIST_FILTER_HAS_PROPERTY_FUNCTION";++ /** A feature constant for the "semanticSearch" function in {@link AppSearchSession#search}. */+ public static final String EMBEDDING_SEARCH = "EMBEDDING_SEARCH";++ /** A feature constant for the "tokenize" function in {@link AppSearchSession#search}. */+ public static final String LIST_FILTER_TOKENIZE_FUNCTION = "TOKENIZE";++ private FeatureConstants() {} }
@@ -16,7 +16,8 @@
package androidx.appsearch.app; import androidx.annotation.NonNull;-import androidx.annotation.RestrictTo;++import java.util.Set; /** * A class that encapsulates all features that are only supported in certain cases (e.g. only on@@ -29,6 +30,8 @@
*/ // @exportToFramework:copyToPath(../../../cts/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/Features.java)+// Note: When adding new fields, The @RequiresFeature is needed in setters but could be skipped in+// getters if call the getter won't send unsupported requests to the AppSearch-framework-impl. public interface Features { /**@@ -60,8 +63,8 @@
/** * Feature for {@link #isFeatureSupported(String)}. This feature covers- * {@link SetSchemaRequest.Builder#addAllowedRoleForSchemaTypeVisibility},- * {@link SetSchemaRequest.Builder#clearAllowedRolesForSchemaTypeVisibility},+ * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set)},+ * {@link SetSchemaRequest.Builder#clearRequiredPermissionsForSchemaTypeVisibility(String)}, * {@link GetSchemaResponse#getSchemaTypesNotDisplayedBySystem()}, * {@link GetSchemaResponse#getSchemaTypesVisibleToPackages()}, * {@link GetSchemaResponse#getRequiredPermissionsForSchemaTypeVisibility()},@@ -84,6 +87,7 @@
*
For details on the numeric search expressions in the query language, see
* {@link AppSearchSession#search}. */+ // Note: The preferred name of this feature should have been LIST_FILTER_NUMERIC_SEARCH. String NUMERIC_SEARCH = FeatureConstants.NUMERIC_SEARCH; /**@@ -94,6 +98,7 @@
* *
For details on the verbatim string operator, see {@link AppSearchSession#search}.
*/+ // Note: The preferred name of this feature should have been LIST_FILTER_VERBATIM_SEARCH. String VERBATIM_SEARCH = FeatureConstants.VERBATIM_SEARCH; /**@@ -106,6 +111,42 @@
String LIST_FILTER_QUERY_LANGUAGE = FeatureConstants.LIST_FILTER_QUERY_LANGUAGE; /**+ * Feature for {@link #isFeatureSupported(String)}. This feature covers the use of the+ * "hasProperty" function in query expressions.+ *+ *
For details on the "hasProperty" function in the query language, see
+ * {@link AppSearchSession#search}.+ */+ String LIST_FILTER_HAS_PROPERTY_FUNCTION = FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION;++ /**+ * Feature for {@link #isFeatureSupported(String)}. This feature covers the use of the+ * "tokenize" function in query expressions.+ *+ *
For details on the "tokenize" function in the query language, see
+ * {@link AppSearchSession#search}.+ */+ String LIST_FILTER_TOKENIZE_FUNCTION = "LIST_FILTER_TOKENIZE_FUNCTION";++ /**+ * Feature for {@link #isFeatureSupported(String)}. This feature covers whether or not the+ * AppSearch backend can store the descriptions returned by+ * {@link AppSearchSchema#getDescription} and+ * {@link AppSearchSchema.PropertyConfig#getDescription}.+ */+ String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION";++ /**+ * Feature for {@link #isFeatureSupported(String)}. This feature covers+ * {@link AppSearchSchema.EmbeddingPropertyConfig}.+ *+ *
For details on the embedding search expressions, see {@link AppSearchSession#search} for
+ * the query language and {@link SearchSpec.Builder#setRankingStrategy(String)} for the ranking+ * language.+ */+ String SCHEMA_EMBEDDING_PROPERTY_CONFIG = "SCHEMA_EMBEDDING_PROPERTY_CONFIG";++ /** * Feature for {@link #isFeatureSupported(String)}. This feature covers * {@link SearchSpec#GROUPING_TYPE_PER_SCHEMA} */@@ -119,10 +160,9 @@
/** * Feature for {@link #isFeatureSupported(String)}. This feature covers- * {@link SearchSpec.Builder#addFilterProperties}.- * @exportToFramework:hide+ * {@link SearchSpec.Builder#addFilterProperties} and+ * {@link SearchSuggestionSpec.Builder#addFilterProperties}. */- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES"; /**@@ -145,12 +185,6 @@
String SEARCH_SUGGESTION = "SEARCH_SUGGESTION"; /**- * Feature for {@link #isFeatureSupported(String)}. This feature covers- * {@link AppSearchSchema.StringPropertyConfig.Builder#setDeletionPropagation}.- */- String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";-- /** * Feature for {@link #isFeatureSupported(String)}. This feature covers setting schemas with * circular references for {@link AppSearchSession#setSchemaAsync}. */@@ -170,6 +204,38 @@
String SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES = "SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES"; /**+ * Feature for {@link #isFeatureSupported(String)}. This feature covers+ * {@link SearchSpec.Builder#setSearchSourceLogTag(String)}.+ */+ String SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG = "SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG";++ /**+ * Feature for {@link #isFeatureSupported(String)}. This feature covers+ * {@link SetSchemaRequest.Builder#setPubliclyVisibleSchema(String, PackageIdentifier)}.+ */+ String SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE = "SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE";++ /**+ * Feature for {@link #isFeatureSupported(String)}. This feature covers+ * {@link SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig}.+ */+ String SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG =+ "SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG";++ /**+ * Feature for {@link #isFeatureSupported(String)}. This feature covers+ * {@link EnterpriseGlobalSearchSession}+ */+ String ENTERPRISE_GLOBAL_SEARCH_SESSION = "ENTERPRISE_GLOBAL_SEARCH_SESSION";++ /**+ * Feature for {@link #isFeatureSupported(String)}. This feature covers+ * {@link SearchSpec.Builder#addInformationalRankingExpressions}.+ */+ String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS =+ "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS";++ /** * Returns whether a feature is supported at run-time. Feature support depends on the * feature in question, the AppSearch backend being used and the Android version of the * device.
@@ -17,8 +17,6 @@
package androidx.appsearch.app; import android.annotation.SuppressLint;-import android.os.Bundle;-import android.os.Parcelable; import android.util.Log; import androidx.annotation.IntRange;@@ -27,9 +25,11 @@
import androidx.annotation.RestrictTo; import androidx.appsearch.annotation.CanIgnoreReturnValue; import androidx.appsearch.annotation.Document;-import androidx.appsearch.app.PropertyPath.PathSegment; import androidx.appsearch.exceptions.AppSearchException;-import androidx.appsearch.util.BundleUtil;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags;+import androidx.appsearch.safeparcel.GenericDocumentParcel;+import androidx.appsearch.safeparcel.PropertyParcel; import androidx.appsearch.util.IndentingStringBuilder; import androidx.core.util.Preconditions;@@ -39,6 +39,7 @@
import java.util.Collections; import java.util.List; import java.util.Map;+import java.util.Objects; import java.util.Set; /**@@ -64,23 +65,9 @@
/** The maximum number of indexed properties a document can have. */ private static final int MAX_INDEXED_PROPERTIES = 16;- /** The default score of document. */- private static final int DEFAULT_SCORE = 0;-- /** The default time-to-live in millisecond of a document, which is infinity. */- private static final long DEFAULT_TTL_MILLIS = 0L;-- private static final String PROPERTIES_FIELD = "properties";- private static final String BYTE_ARRAY_FIELD = "byteArray";- private static final String SCHEMA_TYPE_FIELD = "schemaType";- private static final String ID_FIELD = "id";- private static final String SCORE_FIELD = "score";- private static final String TTL_MILLIS_FIELD = "ttlMillis";- private static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis";- private static final String NAMESPACE_FIELD = "namespace";- private static final String PARENT_TYPES_FIELD = "parentTypes";- /**+ * Fixed constant synthetic property for parent types.+ * * */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)@@ -104,6 +91,7 @@
* {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE}. * * + * * @deprecated This is no longer a static value, but depends on SDK version and what AppSearch * implementation is being used. Use {@link Features#getMaxIndexedProperties} instead. * @@ -138,43 +126,20 @@
} // @exportToFramework:endStrip()- /**- * Contains all {@link GenericDocument} information in a packaged format.- *- *
Keys are the {@code *_FIELD} constants in this class.
- */- @NonNull- final Bundle mBundle;-- /** Contains all properties in {@link GenericDocument} to support getting properties via name */- @NonNull- private final Bundle mProperties;-- @NonNull- private final String mId;- @NonNull- private final String mSchemaType;- private final long mCreationTimestampMillis;- @Nullable- private Integer mHashCode;+ /** The class to hold all meta data and properties for this {@link GenericDocument}. */+ private final GenericDocumentParcel mDocumentParcel; /**- * Rebuilds a {@link GenericDocument} from a bundle.+ * Rebuilds a {@link GenericDocument} from a {@link GenericDocumentParcel}. *- * @param bundle Packaged {@link GenericDocument} data, such as the result of- * {@link #getBundle}.+ * @param documentParcel Packaged {@link GenericDocument} data, such as the result of+ * {@link #getDocumentParcel()}. * @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @SuppressWarnings("deprecation")- public GenericDocument(@NonNull Bundle bundle) {- Preconditions.checkNotNull(bundle);- mBundle = bundle;- mProperties = Preconditions.checkNotNull(bundle.getParcelable(PROPERTIES_FIELD));- mId = Preconditions.checkNotNull(mBundle.getString(ID_FIELD));- mSchemaType = Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD));- mCreationTimestampMillis = mBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD,- System.currentTimeMillis());+ public GenericDocument(@NonNull GenericDocumentParcel documentParcel) {+ mDocumentParcel = Objects.requireNonNull(documentParcel); } /**@@ -183,36 +148,37 @@
*
This method should be only used by constructor of a subclass.
*/ protected GenericDocument(@NonNull GenericDocument document) {- this(document.mBundle);+ this(document.mDocumentParcel); } /**- * Returns the {@link Bundle} populated by this builder.+ * Returns the {@link GenericDocumentParcel} holding the values for this+ * {@link GenericDocument}. * * @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @NonNull- public Bundle getBundle() {- return mBundle;+ public GenericDocumentParcel getDocumentParcel() {+ return mDocumentParcel; } /** Returns the unique identifier of the {@link GenericDocument}. */ @NonNull public String getId() {- return mId;+ return mDocumentParcel.getId(); } /** Returns the namespace of the {@link GenericDocument}. */ @NonNull public String getNamespace() {- return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/ "");+ return mDocumentParcel.getNamespace(); } /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */ @NonNull public String getSchemaType() {- return mSchemaType;+ return mDocumentParcel.getSchemaType(); } /**@@ -224,7 +190,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @Nullable public List getParentTypes() {- List result = mBundle.getStringArrayList(PARENT_TYPES_FIELD);+ List result = mDocumentParcel.getParentTypes(); if (result == null) { return null; }@@ -238,7 +204,7 @@
*/ /*@exportToFramework:CurrentTimeMillisLong*/ public long getCreationTimestampMillis() {- return mCreationTimestampMillis;+ return mDocumentParcel.getCreationTimestampMillis(); } /**@@ -252,7 +218,7 @@
* until the app is uninstalled or {@link AppSearchSession#removeAsync} is called. */ public long getTtlMillis() {- return mBundle.getLong(TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);+ return mDocumentParcel.getTtlMillis(); } /**@@ -267,13 +233,13 @@
*
Any non-negative integer can be used a score.
*/ public int getScore() {- return mBundle.getInt(SCORE_FIELD, DEFAULT_SCORE);+ return mDocumentParcel.getScore(); } /** Returns the names of all properties defined in this document. */ @NonNull public Set getPropertyNames() {- return Collections.unmodifiableSet(mProperties.keySet());+ return Collections.unmodifiableSet(mDocumentParcel.getPropertyNames()); } /**@@ -334,61 +300,36 @@
* * @param path The path to look for. * @return The entry with the given path as an object or {@code null} if there is no such path.- * The returned object will be one of the following types: {@code String[]}, {@code long[]},- * {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}.+ * The returned object will be one of the following types: {@code String[]}, {@code long[]},+ * {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}. */ @Nullable public Object getProperty(@NonNull String path) {- Preconditions.checkNotNull(path);+ Objects.requireNonNull(path); Object rawValue =- getRawPropertyFromRawDocument(new PropertyPath(path), /*pathIndex=*/0, mBundle);+ getRawPropertyFromRawDocument(new PropertyPath(path), /*pathIndex=*/ 0,+ mDocumentParcel.getPropertyMap()); // Unpack the raw value into the types the user expects, if required.- if (rawValue instanceof Bundle) {- // getRawPropertyFromRawDocument may return a document as a bare Bundle as a performance- // optimization for lookups.- GenericDocument document = new GenericDocument((Bundle) rawValue);+ if (rawValue instanceof GenericDocumentParcel) {+ // getRawPropertyFromRawDocument may return a document as a bare documentParcel+ // as a performance optimization for lookups.+ GenericDocument document = new GenericDocument((GenericDocumentParcel) rawValue); return new GenericDocument[]{document}; }- if (rawValue instanceof List) {- // byte[][] fields are packed into List where each Bundle contains just a single- // entry: BYTE_ARRAY_FIELD -> byte[].- @SuppressWarnings("unchecked")- List bundles = (List) rawValue;- byte[][] bytes = new byte[bundles.size()][];- for (int i = 0; i < bundles.size(); i++) {- Bundle bundle = bundles.get(i);- if (bundle == null) {- Log.e(TAG, "The inner bundle is null at " + i + ", for path: " + path);- continue;- }- byte[] innerBytes = bundle.getByteArray(BYTE_ARRAY_FIELD);- if (innerBytes == null) {- Log.e(TAG, "The bundle at " + i + " contains a null byte[].");- continue;- }- bytes[i] = innerBytes;- }- return bytes;- }-- if (rawValue instanceof Parcelable[]) {- // The underlying Bundle of nested GenericDocuments is packed into a Parcelable array.+ if (rawValue instanceof GenericDocumentParcel[]) {+ // The underlying parcelable of nested GenericDocuments is packed into+ // a Parcelable array. // We must unpack it into GenericDocument instances.- Parcelable[] bundles = (Parcelable[]) rawValue;- GenericDocument[] documents = new GenericDocument[bundles.length];- for (int i = 0; i < bundles.length; i++) {- if (bundles[i] == null) {- Log.e(TAG, "The inner bundle is null at " + i + ", for path: " + path);+ GenericDocumentParcel[] docParcels = (GenericDocumentParcel[]) rawValue;+ GenericDocument[] documents = new GenericDocument[docParcels.length];+ for (int i = 0; i < docParcels.length; i++) {+ if (docParcels[i] == null) {+ Log.e(TAG, "The inner parcel is null at " + i + ", for path: " + path); continue; }- if (!(bundles[i] instanceof Bundle)) {- Log.e(TAG, "The inner element at " + i + " is a " + bundles[i].getClass()- + ", not a Bundle for path: " + path);- continue;- }- documents[i] = new GenericDocument((Bundle) bundles[i]);+ documents[i] = new GenericDocument(docParcels[i]); } return documents; }@@ -407,25 +348,21 @@
* But in the case where we collect documents across repeated nested documents, we need to * recurse back into this method, and so we also keep track of the index into the path. *- * @param path the PropertyPath object representing the path- * @param pathIndex the index into the path we start at- * @param documentBundle the bundle that contains the path we are looking up+ * @param path the PropertyPath object representing the path+ * @param pathIndex the index into the path we start at+ * @param propertyMap the map containing the path we are looking up * @return the raw property */ @Nullable @SuppressWarnings("deprecation") private static Object getRawPropertyFromRawDocument(- @NonNull PropertyPath path, int pathIndex, @NonNull Bundle documentBundle) {- Preconditions.checkNotNull(path);- Preconditions.checkNotNull(documentBundle);- Bundle properties = Preconditions.checkNotNull(documentBundle.getBundle(PROPERTIES_FIELD));--+ @NonNull PropertyPath path, int pathIndex,+ @NonNull Map propertyMap) {+ Objects.requireNonNull(path);+ Objects.requireNonNull(propertyMap); for (int i = pathIndex; i < path.size(); i++) {- PathSegment segment = path.get(i);-- Object currentElementValue = properties.get(segment.getPropertyName());-+ PropertyPath.PathSegment segment = path.get(i);+ Object currentElementValue = propertyMap.get(segment.getPropertyName()); if (currentElementValue == null) { return null; }@@ -435,61 +372,77 @@
// "recipients[0]", currentElementValue now contains the value of "recipients" while we // need the value of "recipients[0]". int index = segment.getPropertyIndex();- if (index != PathSegment.NON_REPEATED_CARDINALITY) {+ if (index != PropertyPath.PathSegment.NON_REPEATED_CARDINALITY) {+ // For properties bundle, now we will only get PropertyParcel as the value.+ PropertyParcel propertyParcel = (PropertyParcel) currentElementValue;+ // Extract the right array element Object extractedValue = null;- if (currentElementValue instanceof String[]) {- String[] stringValues = (String[]) currentElementValue;- if (index < stringValues.length) {+ if (propertyParcel.getStringValues() != null) {+ String[] stringValues = propertyParcel.getStringValues();+ if (stringValues != null && index < stringValues.length) { extractedValue = Arrays.copyOfRange(stringValues, index, index + 1); }- } else if (currentElementValue instanceof long[]) {- long[] longValues = (long[]) currentElementValue;- if (index < longValues.length) {+ } else if (propertyParcel.getLongValues() != null) {+ long[] longValues = propertyParcel.getLongValues();+ if (longValues != null && index < longValues.length) { extractedValue = Arrays.copyOfRange(longValues, index, index + 1); }- } else if (currentElementValue instanceof double[]) {- double[] doubleValues = (double[]) currentElementValue;- if (index < doubleValues.length) {+ } else if (propertyParcel.getDoubleValues() != null) {+ double[] doubleValues = propertyParcel.getDoubleValues();+ if (doubleValues != null && index < doubleValues.length) { extractedValue = Arrays.copyOfRange(doubleValues, index, index + 1); }- } else if (currentElementValue instanceof boolean[]) {- boolean[] booleanValues = (boolean[]) currentElementValue;- if (index < booleanValues.length) {+ } else if (propertyParcel.getBooleanValues() != null) {+ boolean[] booleanValues = propertyParcel.getBooleanValues();+ if (booleanValues != null && index < booleanValues.length) { extractedValue = Arrays.copyOfRange(booleanValues, index, index + 1); }- } else if (currentElementValue instanceof List) {- @SuppressWarnings("unchecked")- List bundles = (List) currentElementValue;- if (index < bundles.size()) {- extractedValue = bundles.subList(index, index + 1);+ } else if (propertyParcel.getBytesValues() != null) {+ byte[][] bytesValues = propertyParcel.getBytesValues();+ if (bytesValues != null && index < bytesValues.length) {+ extractedValue = Arrays.copyOfRange(bytesValues, index, index + 1); }- } else if (currentElementValue instanceof Parcelable[]) {+ } else if (propertyParcel.getDocumentValues() != null) { // Special optimization: to avoid creating new singleton arrays for traversing- // paths we return the bare document Bundle in this particular case.- Parcelable[] bundles = (Parcelable[]) currentElementValue;- if (index < bundles.length) {- extractedValue = bundles[index];+ // paths we return the bare document parcel in this particular case.+ GenericDocumentParcel[] docValues = propertyParcel.getDocumentValues();+ if (docValues != null && index < docValues.length) {+ extractedValue = docValues[index];+ }+ } else if (propertyParcel.getEmbeddingValues() != null) {+ EmbeddingVector[] embeddingValues = propertyParcel.getEmbeddingValues();+ if (embeddingValues != null && index < embeddingValues.length) {+ extractedValue = Arrays.copyOfRange(embeddingValues, index, index + 1); } } else {- throw new IllegalStateException("Unsupported value type: "- + currentElementValue);+ throw new IllegalStateException(+ "Unsupported value type: " + currentElementValue); } currentElementValue = extractedValue; } // at the end of the path, either something like "...foo" or "...foo[1]" if (currentElementValue == null || i == path.size() - 1) {+ if (currentElementValue != null && currentElementValue instanceof PropertyParcel) {+ // Unlike previous bundle-based implementation, now each+ // value is wrapped in PropertyParcel.+ // Here we need to get and return the actual value for non-repeated fields.+ currentElementValue = ((PropertyParcel) currentElementValue).getValues();+ } return currentElementValue; }- // currentElementValue is now a Bundle or Parcelable[], we can continue down the path- if (currentElementValue instanceof Bundle) {- properties = ((Bundle) currentElementValue).getBundle(PROPERTIES_FIELD);- } else if (currentElementValue instanceof Parcelable[]) {- Parcelable[] parcelables = (Parcelable[]) currentElementValue;- if (parcelables.length == 1) {- properties = ((Bundle) parcelables[0]).getBundle(PROPERTIES_FIELD);+ // currentElementValue is now a GenericDocumentParcel or PropertyParcel,+ // we can continue down the path.+ if (currentElementValue instanceof GenericDocumentParcel) {+ propertyMap = ((GenericDocumentParcel) currentElementValue).getPropertyMap();+ } else if (currentElementValue instanceof PropertyParcel+ && ((PropertyParcel) currentElementValue).getDocumentValues() != null) {+ GenericDocumentParcel[] docParcels =+ ((PropertyParcel) currentElementValue).getDocumentValues();+ if (docParcels != null && docParcels.length == 1) {+ propertyMap = docParcels[0].getPropertyMap(); continue; }@@ -516,17 +469,21 @@
// repeated values. The implementation is optimized for these two cases, requiring // no additional allocations. So we've decided that the above performance // characteristics are OK for the less used path.- List- for (Parcelable parcelable : parcelables) {- // recurse as we need to branch- Object value = getRawPropertyFromRawDocument(path, /*pathIndex=*/i + 1,- (Bundle) parcelable);- if (value != null) {- accumulator.add(value);+ if (docParcels != null) {+ List+ for (GenericDocumentParcel docParcel : docParcels) {+ // recurse as we need to branch+ Object value =+ getRawPropertyFromRawDocument(+ path, /*pathIndex=*/ i + 1,+ ((GenericDocumentParcel) docParcel).getPropertyMap());+ if (value != null) {+ accumulator.add(value);+ } }+ // Break the path traversing loop+ return flattenAccumulator(accumulator); }- // Break the path traversing loop- return flattenAccumulator(accumulator); } else { Log.e(TAG, "Failed to apply path to document; no nested value found: " + path); return null;@@ -540,10 +497,10 @@
* Combines accumulated repeated properties from multiple documents into a single array. * * @param accumulator List containing objects of the following types: {@code String[]},- * {@code long[]}, {@code double[]}, {@code boolean[]}, {@code List},- * or {@code Parcelable[]}.+ * {@code long[]}, {@code double[]}, {@code boolean[]}, {@code byte[][]},+ * or {@code GenericDocumentParcelable[]}. * @return The result of concatenating each individual list element into a larger array/list of- * the same type.+ * the same type. */ @Nullable private static Object flattenAccumulator(@NonNull List@@ -607,28 +564,29 @@
} return result; }- if (first instanceof List) {+ if (first instanceof byte[][]) { int length = 0; for (int i = 0; i < accumulator.size(); i++) {- length += ((List>) accumulator.get(i)).size();+ length += ((byte[][]) accumulator.get(i)).length; }- List result = new ArrayList<>(length);+ byte[][] result = new byte[length][];+ int total = 0; for (int i = 0; i < accumulator.size(); i++) {- @SuppressWarnings("unchecked")- List castValue = (List) accumulator.get(i);- result.addAll(castValue);+ byte[][] castValue = (byte[][]) accumulator.get(i);+ System.arraycopy(castValue, 0, result, total, castValue.length);+ total += castValue.length; } return result; }- if (first instanceof Parcelable[]) {+ if (first instanceof GenericDocumentParcel[]) { int length = 0; for (int i = 0; i < accumulator.size(); i++) {- length += ((Parcelable[]) accumulator.get(i)).length;+ length += ((GenericDocumentParcel[]) accumulator.get(i)).length; }- Parcelable[] result = new Parcelable[length];+ GenericDocumentParcel[] result = new GenericDocumentParcel[length]; int total = 0; for (int i = 0; i < accumulator.size(); i++) {- Parcelable[] castValue = (Parcelable[]) accumulator.get(i);+ GenericDocumentParcel[] castValue = (GenericDocumentParcel[]) accumulator.get(i); System.arraycopy(castValue, 0, result, total, castValue.length); total += castValue.length; }@@ -754,6 +712,27 @@
return propertyArray[0]; }+ /**+ * Retrieves an {@code EmbeddingVector} property by path.+ *+ *
See {@link #getProperty} for a detailed description of the path syntax.
+ *+ * @param path The path to look for.+ * @return The first {@code EmbeddingVector[]} associated with the given path or+ * {@code null} if there is no such value or the value is of a different type.+ */+ @Nullable+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public EmbeddingVector getPropertyEmbedding(@NonNull String path) {+ Preconditions.checkNotNull(path);+ EmbeddingVector[] propertyArray = getPropertyEmbeddingArray(path);+ if (propertyArray == null || propertyArray.length == 0) {+ return null;+ }+ warnIfSinglePropertyTooLong("Embedding", path, propertyArray.length);+ return propertyArray[0];+ }+ /** Prints a warning to logcat if the given propertyLength is greater than 1. */ private static void warnIfSinglePropertyTooLong( @NonNull String propertyType, @NonNull String path, int propertyLength) {@@ -862,13 +841,13 @@
* returns {@code null}. * * - *
If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
- * this method returns an empty {@code byte[][]}.+ *
If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
+ * this method returns an empty {@code byte[][]}. * * * @param path The path to look for.@@ -892,13 +871,13 @@
* returns {@code null}. * * - *
If it has been set via {@link Builder#setPropertyDocument} to an empty
- * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]}.+ *
If it has been set via {@link Builder#setPropertyDocument} to an empty
+ * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]}. * * * @param path The path to look for.@@ -914,11 +893,36 @@
} /**+ * Retrieves a repeated {@code EmbeddingVector[]} property by path.+ *+ *
See {@link #getProperty} for a detailed description of the path syntax.
+ *+ *
If the property has not been set via {@link Builder#setPropertyEmbedding}, this method
+ * returns {@code null}.+ *+ *
If it has been set via {@link Builder#setPropertyEmbedding} to an empty
+ * {@code EmbeddingVector[]}, this method returns an empty+ * {@code EmbeddingVector[]}.+ *+ * @param path The path to look for.+ * @return The {@code EmbeddingVector[]} associated with the given path, or+ * {@code null} if no value is set or the value is of a different type.+ */+ @SuppressLint({"ArrayReturn", "NullableCollection"})+ @Nullable+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public EmbeddingVector[] getPropertyEmbeddingArray(@NonNull String path) {+ Preconditions.checkNotNull(path);+ Object value = getProperty(path);+ return safeCastProperty(path, value, EmbeddingVector[].class);+ }++ /** * Casts a repeated property to the provided type, logging an error and returning {@code null} * if the cast fails. *- * @param path Path to the property within the document. Used for logging.- * @param value Value of the property+ * @param path Path to the property within the document. Used for logging.+ * @param value Value of the property * @param tClass Class to cast the value into */ @Nullable@@ -1056,6 +1060,7 @@
* {@link GenericDocument.Builder}. * *
The returned builder is a deep copy whose data is separate from this document.
+ * * @deprecated This API is not compliant with API guidelines. * Use {@link Builder#Builder(GenericDocument)} instead. * @@ -1064,8 +1069,7 @@
@NonNull @Deprecated public GenericDocument.Builder> toBuilder() {- Bundle clonedBundle = BundleUtil.deepCopy(mBundle);- return new GenericDocument.Builder<>(clonedBundle);+ return new Builder<>(new GenericDocumentParcel.Builder(mDocumentParcel)); } @Override@@ -1077,15 +1081,12 @@
return false; } GenericDocument otherDocument = (GenericDocument) other;- return BundleUtil.deepEquals(this.mBundle, otherDocument.mBundle);+ return mDocumentParcel.equals(otherDocument.mDocumentParcel); } @Override public int hashCode() {- if (mHashCode == null) {- mHashCode = BundleUtil.deepHashCode(mBundle);- }- return mHashCode;+ return mDocumentParcel.hashCode(); } @Override@@ -1099,7 +1100,7 @@
/** * Appends a debug string for the {@link GenericDocument} instance to the given string builder. *- * @param builder the builder to append to.+ * @param builder the builder to append to. */ void appendGenericDocumentString(@NonNull IndentingStringBuilder builder) { Preconditions.checkNotNull(builder);@@ -1147,9 +1148,9 @@
/** * Appends a debug string for the given document property to the given string builder. *- * @param propertyName name of property to create string for.- * @param property property object to create string for.- * @param builder the builder to append to.+ * @param propertyName name of property to create string for.+ * @param property property object to create string for.+ * @param builder the builder to append to. */ private void appendPropertyString(@NonNull String propertyName, @NonNull Object property, @NonNull IndentingStringBuilder builder) {@@ -1178,7 +1179,7 @@
builder.append("\"").append((String) propertyElement).append("\""); } else if (propertyElement instanceof byte[]) { builder.append(Arrays.toString((byte[]) propertyElement));- } else {+ } else if (propertyElement != null) { builder.append(propertyElement.toString()); } if (i != propertyArrLength - 1) {@@ -1197,11 +1198,10 @@
// This builder is specifically designed to be extended by classes deriving from // GenericDocument. @SuppressLint("StaticFinalBuilder")+ @SuppressWarnings("rawtypes") public static class Builder {- private Bundle mBundle;- private Bundle mProperties;+ private final GenericDocumentParcel.Builder mDocumentParcelBuilder; private final BuilderType mBuilderTypeInstance;- private boolean mBuilt = false; /** * Creates a new {@link GenericDocument.Builder}.@@ -1228,41 +1228,31 @@
Preconditions.checkNotNull(id); Preconditions.checkNotNull(schemaType);- mBundle = new Bundle(); mBuilderTypeInstance = (BuilderType) this;- mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);- mBundle.putString(GenericDocument.ID_FIELD, id);- mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType);- mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);- mBundle.putInt(GenericDocument.SCORE_FIELD, DEFAULT_SCORE);-- mProperties = new Bundle();- mBundle.putBundle(PROPERTIES_FIELD, mProperties);+ mDocumentParcelBuilder = new GenericDocumentParcel.Builder(namespace, id, schemaType); } /**- * Creates a new {@link GenericDocument.Builder} from the given Bundle.+ * Creates a new {@link GenericDocument.Builder} from the given+ * {@link GenericDocumentParcel.Builder}. * *
The bundle is NOT copied.
*/ @SuppressWarnings("unchecked")- Builder(@NonNull Bundle bundle) {- mBundle = Preconditions.checkNotNull(bundle);- // mProperties is NonNull and initialized to empty Bundle() in builder.- mProperties = Preconditions.checkNotNull(mBundle.getBundle(PROPERTIES_FIELD));+ Builder(@NonNull GenericDocumentParcel.Builder documentParcelBuilder) {+ mDocumentParcelBuilder = Objects.requireNonNull(documentParcelBuilder); mBuilderTypeInstance = (BuilderType) this; } /** * Creates a new {@link GenericDocument.Builder} from the given GenericDocument. *- *
The GenericDocument is deep copied, i.e. changes to the new GenericDocument
- * returned by this function will NOT affect the original GenericDocument.- * + *
The GenericDocument is deep copied, that is, it changes to a new GenericDocument
+ * returned by this function and will NOT affect the original GenericDocument. */- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR) public Builder(@NonNull GenericDocument document) {- this(BundleUtil.deepCopy(document.getBundle()));+ this(new GenericDocumentParcel.Builder(document.mDocumentParcel)); } /**@@ -1272,14 +1262,13 @@
*
Document IDs are unique within a namespace.
* *
The number of namespaces per app should be kept small for efficiency reasons.
- * */+ @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) @CanIgnoreReturnValue @NonNull public BuilderType setNamespace(@NonNull String namespace) { Preconditions.checkNotNull(namespace);- resetIfBuilt();- mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);+ mDocumentParcelBuilder.setNamespace(namespace); return mBuilderTypeInstance; }@@ -1287,15 +1276,17 @@
* Sets the ID of this document, changing the value provided in the constructor. No * special values are reserved or understood by the infrastructure. *- *
Document IDs are unique within a namespace.
- * + *
Document IDs are unique within the combination of package, database, and namespace.
+ *+ *
Setting a document with a duplicate id will overwrite the original document with
+ * the new document, enforcing uniqueness within the above constraint. */+ @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) @CanIgnoreReturnValue @NonNull public BuilderType setId(@NonNull String id) { Preconditions.checkNotNull(id);- resetIfBuilt();- mBundle.putString(GenericDocument.ID_FIELD, id);+ mDocumentParcelBuilder.setId(id); return mBuilderTypeInstance; }@@ -1303,15 +1294,15 @@
* Sets the schema type of this document, changing the value provided in the constructor. * *
To successfully index a document, the schema type must match the name of an
- * {@link AppSearchSchema} object previously provided to {@link AppSearchSession#setSchemaAsync}.- * + * {@link AppSearchSchema} object previously provided to+ * {@link AppSearchSession#setSchemaAsync}. */+ @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) @CanIgnoreReturnValue @NonNull public BuilderType setSchemaType(@NonNull String schemaType) { Preconditions.checkNotNull(schemaType);- resetIfBuilt();- mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType);+ mDocumentParcelBuilder.setSchemaType(schemaType); return mBuilderTypeInstance; }@@ -1326,9 +1317,7 @@
@NonNull public BuilderType setParentTypes(@NonNull List parentTypes) { Preconditions.checkNotNull(parentTypes);- resetIfBuilt();- mBundle.putStringArrayList(GenericDocument.PARENT_TYPES_FIELD,- new ArrayList<>(parentTypes));+ mDocumentParcelBuilder.setParentTypes(parentTypes); return mBuilderTypeInstance; }@@ -1352,8 +1341,7 @@
if (score < 0) { throw new IllegalArgumentException("Document score cannot be negative."); }- resetIfBuilt();- mBundle.putInt(GenericDocument.SCORE_FIELD, score);+ mDocumentParcelBuilder.setScore(score); return mBuilderTypeInstance; }@@ -1371,9 +1359,7 @@
@NonNull public BuilderType setCreationTimestampMillis( /*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {- resetIfBuilt();- mBundle.putLong(- GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, creationTimestampMillis);+ mDocumentParcelBuilder.setCreationTimestampMillis(creationTimestampMillis); return mBuilderTypeInstance; }@@ -1397,8 +1383,7 @@
if (ttlMillis < 0) { throw new IllegalArgumentException("Document ttlMillis cannot be negative."); }- resetIfBuilt();- mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, ttlMillis);+ mDocumentParcelBuilder.setTtlMillis(ttlMillis); return mBuilderTypeInstance; }@@ -1406,9 +1391,9 @@
* Sets one or multiple {@code String} values for a property, replacing its previous * values. *- * @param name the name associated with the {@code values}. Must match the name- * for this property as given in- * {@link AppSearchSchema.PropertyConfig#getName}.+ * @param name the name associated with the {@code values}. Must match the name+ * for this property as given in+ * {@link AppSearchSchema.PropertyConfig#getName}. * @param values the {@code String} values of the property. * @throws IllegalArgumentException if no values are provided, or if a passed in * {@code String} is {@code null} or "".@@ -1418,8 +1403,13 @@
public BuilderType setPropertyString(@NonNull String name, @NonNull String... values) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(values);- resetIfBuilt();- putInPropertyBundle(name, values);+ validatePropertyName(name);+ for (int i = 0; i < values.length; i++) {+ if (values[i] == null) {+ throw new IllegalArgumentException("The String at " + i + " is null.");+ }+ }+ mDocumentParcelBuilder.putInPropertyMap(name, values); return mBuilderTypeInstance; }@@ -1427,9 +1417,9 @@
* Sets one or multiple {@code boolean} values for a property, replacing its previous * values. *- * @param name the name associated with the {@code values}. Must match the name- * for this property as given in- * {@link AppSearchSchema.PropertyConfig#getName}.+ * @param name the name associated with the {@code values}. Must match the name+ * for this property as given in+ * {@link AppSearchSchema.PropertyConfig#getName}. * @param values the {@code boolean} values of the property. * @throws IllegalArgumentException if the name is empty or {@code null}. */@@ -1438,8 +1428,8 @@
public BuilderType setPropertyBoolean(@NonNull String name, @NonNull boolean... values) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(values);- resetIfBuilt();- putInPropertyBundle(name, values);+ validatePropertyName(name);+ mDocumentParcelBuilder.putInPropertyMap(name, values); return mBuilderTypeInstance; }@@ -1447,9 +1437,9 @@
* Sets one or multiple {@code long} values for a property, replacing its previous * values. *- * @param name the name associated with the {@code values}. Must match the name- * for this property as given in- * {@link AppSearchSchema.PropertyConfig#getName}.+ * @param name the name associated with the {@code values}. Must match the name+ * for this property as given in+ * {@link AppSearchSchema.PropertyConfig#getName}. * @param values the {@code long} values of the property. * @throws IllegalArgumentException if the name is empty or {@code null}. */@@ -1458,8 +1448,8 @@
public BuilderType setPropertyLong(@NonNull String name, @NonNull long... values) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(values);- resetIfBuilt();- putInPropertyBundle(name, values);+ validatePropertyName(name);+ mDocumentParcelBuilder.putInPropertyMap(name, values); return mBuilderTypeInstance; }@@ -1467,9 +1457,9 @@
* Sets one or multiple {@code double} values for a property, replacing its previous * values. *- * @param name the name associated with the {@code values}. Must match the name- * for this property as given in- * {@link AppSearchSchema.PropertyConfig#getName}.+ * @param name the name associated with the {@code values}. Must match the name+ * for this property as given in+ * {@link AppSearchSchema.PropertyConfig#getName}. * @param values the {@code double} values of the property. * @throws IllegalArgumentException if the name is empty or {@code null}. */@@ -1478,17 +1468,17 @@
public BuilderType setPropertyDouble(@NonNull String name, @NonNull double... values) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(values);- resetIfBuilt();- putInPropertyBundle(name, values);+ validatePropertyName(name);+ mDocumentParcelBuilder.putInPropertyMap(name, values); return mBuilderTypeInstance; } /** * Sets one or multiple {@code byte[]} for a property, replacing its previous values. *- * @param name the name associated with the {@code values}. Must match the name- * for this property as given in- * {@link AppSearchSchema.PropertyConfig#getName}.+ * @param name the name associated with the {@code values}. Must match the name+ * for this property as given in+ * {@link AppSearchSchema.PropertyConfig#getName}. * @param values the {@code byte[]} of the property. * @throws IllegalArgumentException if no values are provided, or if a passed in * {@code byte[]} is {@code null}, or if name is empty.@@ -1498,8 +1488,13 @@
public BuilderType setPropertyBytes(@NonNull String name, @NonNull byte[]... values) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(values);- resetIfBuilt();- putInPropertyBundle(name, values);+ validatePropertyName(name);+ for (int i = 0; i < values.length; i++) {+ if (values[i] == null) {+ throw new IllegalArgumentException("The byte[] at " + i + " is null.");+ }+ }+ mDocumentParcelBuilder.putInPropertyMap(name, values); return mBuilderTypeInstance; }@@ -1507,9 +1502,9 @@
* Sets one or multiple {@link GenericDocument} values for a property, replacing its * previous values. *- * @param name the name associated with the {@code values}. Must match the name- * for this property as given in- * {@link AppSearchSchema.PropertyConfig#getName}.+ * @param name the name associated with the {@code values}. Must match the name+ * for this property as given in+ * {@link AppSearchSchema.PropertyConfig#getName}. * @param values the {@link GenericDocument} values of the property. * @throws IllegalArgumentException if no values are provided, or if a passed in * {@link GenericDocument} is {@code null}, or if name@@ -1521,8 +1516,43 @@
@NonNull String name, @NonNull GenericDocument... values) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(values);- resetIfBuilt();- putInPropertyBundle(name, values);+ validatePropertyName(name);+ GenericDocumentParcel[] documentParcels = new GenericDocumentParcel[values.length];+ for (int i = 0; i < values.length; i++) {+ if (values[i] == null) {+ throw new IllegalArgumentException("The document at " + i + " is null.");+ }+ documentParcels[i] = values[i].getDocumentParcel();+ }+ mDocumentParcelBuilder.putInPropertyMap(name, documentParcels);+ return mBuilderTypeInstance;+ }++ /**+ * Sets one or multiple {@code EmbeddingVector} values for a property, replacing+ * its previous values.+ *+ * @param name the name associated with the {@code values}. Must match the name+ * for this property as given in+ * {@link AppSearchSchema.PropertyConfig#getName}.+ * @param values the {@code EmbeddingVector} values of the property.+ * @throws IllegalArgumentException if the name is empty or {@code null}.+ */+ @CanIgnoreReturnValue+ @NonNull+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public BuilderType setPropertyEmbedding(@NonNull String name,+ @NonNull EmbeddingVector... values) {+ Preconditions.checkNotNull(name);+ Preconditions.checkNotNull(values);+ validatePropertyName(name);+ for (int i = 0; i < values.length; i++) {+ if (values[i] == null) {+ throw new IllegalArgumentException(+ "The EmbeddingVector at " + i + " is null.");+ }+ }+ mDocumentParcelBuilder.putInPropertyMap(name, values); return mBuilderTypeInstance; }@@ -1531,95 +1561,27 @@
* *
Note that this method does not support property paths.
*+ *
You should check for the existence of the property in {@link #getPropertyNames} if
+ * you need to make sure the property being cleared actually exists.+ *+ *
If the string passed is an invalid or nonexistent property, no error message or
+ * behavior will be observed.+ * * @param name The name of the property to clear.- * */+ @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) @CanIgnoreReturnValue @NonNull public BuilderType clearProperty(@NonNull String name) { Preconditions.checkNotNull(name);- resetIfBuilt();- mProperties.remove(name);+ mDocumentParcelBuilder.clearProperty(name); return mBuilderTypeInstance; }- private void putInPropertyBundle(@NonNull String name, @NonNull String[] values)- throws IllegalArgumentException {- validatePropertyName(name);- for (int i = 0; i < values.length; i++) {- if (values[i] == null) {- throw new IllegalArgumentException("The String at " + i + " is null.");- }- }- mProperties.putStringArray(name, values);- }-- private void putInPropertyBundle(@NonNull String name, @NonNull boolean[] values) {- validatePropertyName(name);- mProperties.putBooleanArray(name, values);- }-- private void putInPropertyBundle(@NonNull String name, @NonNull double[] values) {- validatePropertyName(name);- mProperties.putDoubleArray(name, values);- }-- private void putInPropertyBundle(@NonNull String name, @NonNull long[] values) {- validatePropertyName(name);- mProperties.putLongArray(name, values);- }-- /**- * Converts and saves a byte[][] into {@link #mProperties}.- *- *
Bundle doesn't support for two dimension array byte[][], we are converting byte[][]
- * into ArrayList, and each elements will contain a one dimension byte[].- */- private void putInPropertyBundle(@NonNull String name, @NonNull byte[][] values) {- validatePropertyName(name);- ArrayList bundles = new ArrayList<>(values.length);- for (int i = 0; i < values.length; i++) {- if (values[i] == null) {- throw new IllegalArgumentException("The byte[] at " + i + " is null.");- }- Bundle bundle = new Bundle();- bundle.putByteArray(BYTE_ARRAY_FIELD, values[i]);- bundles.add(bundle);- }- mProperties.putParcelableArrayList(name, bundles);- }-- private void putInPropertyBundle(@NonNull String name, @NonNull GenericDocument[] values) {- validatePropertyName(name);- Parcelable[] documentBundles = new Parcelable[values.length];- for (int i = 0; i < values.length; i++) {- if (values[i] == null) {- throw new IllegalArgumentException("The document at " + i + " is null.");- }- documentBundles[i] = values[i].mBundle;- }- mProperties.putParcelableArray(name, documentBundles);- }- /** Builds the {@link GenericDocument} object. */ @NonNull public GenericDocument build() {- mBuilt = true;- // Set current timestamp for creation timestamp by default.- if (mBundle.getLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, -1) == -1) {- mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD,- System.currentTimeMillis());- }- return new GenericDocument(mBundle);- }-- private void resetIfBuilt() {- if (mBuilt) {- mBundle = BundleUtil.deepCopy(mBundle);- // mProperties is NonNull and initialized to empty Bundle() in builder.- mProperties = Preconditions.checkNotNull(mBundle.getBundle(PROPERTIES_FIELD));- mBuilt = false;- }+ return new GenericDocument(mDocumentParcelBuilder.build()); } /** Method to ensure property names are not blank */
@@ -16,9 +16,20 @@
package androidx.appsearch.app;+import android.os.Bundle;+import android.os.Parcel;+import android.os.Parcelable;+ import androidx.annotation.NonNull;+import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.appsearch.annotation.CanIgnoreReturnValue;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags;+import androidx.appsearch.safeparcel.AbstractSafeParcelable;+import androidx.appsearch.safeparcel.SafeParcelable;+import androidx.appsearch.safeparcel.stub.StubCreators.GetByDocumentIdRequestCreator;+import androidx.appsearch.util.BundleUtil; import androidx.collection.ArrayMap; import androidx.collection.ArraySet; import androidx.core.util.Preconditions;@@ -29,6 +40,7 @@
import java.util.Collections; import java.util.List; import java.util.Map;+import java.util.Objects; import java.util.Set; /**@@ -37,7 +49,13 @@
* * @see AppSearchSession#getByDocumentIdAsync */-public final class GetByDocumentIdRequest {+@SuppressWarnings("HiddenSuperclass")[email protected](creator = "GetByDocumentIdRequestCreator")+public final class GetByDocumentIdRequest extends AbstractSafeParcelable {+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @NonNull public static final Parcelable.Creator CREATOR =+ new GetByDocumentIdRequestCreator(); /** * Schema type to be used in * {@link GetByDocumentIdRequest.Builder#addProjection}@@ -45,15 +63,30 @@
* property paths set. */ public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";- private final String mNamespace;- private final Set mIds;- private final Map> mTypePropertyPathsMap;- GetByDocumentIdRequest(@NonNull String namespace, @NonNull Set ids, @NonNull Map- List> typePropertyPathsMap) {- mNamespace = Preconditions.checkNotNull(namespace);- mIds = Preconditions.checkNotNull(ids);- mTypePropertyPathsMap = Preconditions.checkNotNull(typePropertyPathsMap);+ @NonNull+ @Field(id = 1, getter = "getNamespace")+ private final String mNamespace;+ @NonNull+ @Field(id = 2)+ final List mIds;+ @NonNull+ @Field(id = 3)+ final Bundle mTypePropertyPaths;++ /**+ * Cache of the ids. Comes from inflating mIds at first use.+ */+ @Nullable private Set mIdsCached;++ @Constructor+ GetByDocumentIdRequest(+ @Param(id = 1) @NonNull String namespace,+ @Param(id = 2) @NonNull List ids,+ @Param(id = 3) @NonNull Bundle typePropertyPaths) {+ mNamespace = Objects.requireNonNull(namespace);+ mIds = Objects.requireNonNull(ids);+ mTypePropertyPaths = Objects.requireNonNull(typePropertyPaths); } /** Returns the namespace attached to the request. */@@ -65,7 +98,10 @@
/** Returns the set of document IDs attached to the request. */ @NonNull public Set getIds() {- return Collections.unmodifiableSet(mIds);+ if (mIdsCached == null) {+ mIdsCached = Collections.unmodifiableSet(new ArraySet<>(mIds));+ }+ return mIdsCached; } /**@@ -78,11 +114,15 @@
*/ @NonNull public Map> getProjections() {- Map> copy = new ArrayMap<>();- for (Map.Entry> entry : mTypePropertyPathsMap.entrySet()) {- copy.put(entry.getKey(), new ArrayList<>(entry.getValue()));+ Set schemas = mTypePropertyPaths.keySet();+ Map> typePropertyPathsMap = new ArrayMap<>(schemas.size());+ for (String schema : schemas) {+ List propertyPaths = mTypePropertyPaths.getStringArrayList(schema);+ if (propertyPaths != null) {+ typePropertyPathsMap.put(schema, Collections.unmodifiableList(propertyPaths));+ } }- return copy;+ return typePropertyPathsMap; } /**@@ -95,38 +135,34 @@
*/ @NonNull public Map> getProjectionPaths() {- Map> copy = new ArrayMap<>(mTypePropertyPathsMap.size());- for (Map.Entry> entry : mTypePropertyPathsMap.entrySet()) {- List propertyPathList = new ArrayList<>(entry.getValue().size());- for (String p: entry.getValue()) {- propertyPathList.add(new PropertyPath(p));+ Set schemas = mTypePropertyPaths.keySet();+ Map> typePropertyPathsMap = new ArrayMap<>(schemas.size());+ for (String schema : schemas) {+ List paths = mTypePropertyPaths.getStringArrayList(schema);+ if (paths != null) {+ int pathsSize = paths.size();+ List propertyPathList = new ArrayList<>(pathsSize);+ for (int i = 0; i < pathsSize; i++) {+ propertyPathList.add(new PropertyPath(paths.get(i)));+ }+ typePropertyPathsMap.put(schema, Collections.unmodifiableList(propertyPathList)); }- copy.put(entry.getKey(), propertyPathList); }- return copy;+ return typePropertyPathsMap; }- /**- * Returns a map from schema type to property paths to be used for projection.- *- *
If the map is empty, then all properties will be retrieved for all results.
- *- *
A more efficient version of {@link #getProjections}, but it returns a modifiable map.
- * This is not meant to be unhidden and should only be used by internal classes.- *- * @exportToFramework:hide- */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- @NonNull- public Map> getProjectionsInternal() {- return mTypePropertyPathsMap;+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @Override+ public void writeToParcel(@NonNull Parcel dest, int flags) {+ GetByDocumentIdRequestCreator.writeToParcel(this, dest, flags); } /** Builder for {@link GetByDocumentIdRequest} objects. */ public static final class Builder { private final String mNamespace;- private ArraySet mIds = new ArraySet<>();- private ArrayMap> mProjectionTypePropertyPaths = new ArrayMap<>();+ private List mIds = new ArrayList<>();+ private Bundle mProjectionTypePropertyPaths = new Bundle(); private boolean mBuilt = false; /** Creates a {@link GetByDocumentIdRequest.Builder} instance. */@@ -176,12 +212,12 @@
Preconditions.checkNotNull(schemaType); Preconditions.checkNotNull(propertyPaths); resetIfBuilt();- List propertyPathsList = new ArrayList<>(propertyPaths.size());+ ArrayList propertyPathsList = new ArrayList<>(propertyPaths.size()); for (String propertyPath : propertyPaths) { Preconditions.checkNotNull(propertyPath); propertyPathsList.add(propertyPath); }- mProjectionTypePropertyPaths.put(schemaType, propertyPathsList);+ mProjectionTypePropertyPaths.putStringArrayList(schemaType, propertyPathsList); return this; }@@ -223,11 +259,11 @@
private void resetIfBuilt() { if (mBuilt) {- mIds = new ArraySet<>(mIds);+ mIds = new ArrayList<>(mIds); // No need to clone each propertyPathsList inside mProjectionTypePropertyPaths since // the builder only replaces it, never adds to it. So even if the builder is used // again, the previous one will remain with the object.- mProjectionTypePropertyPaths = new ArrayMap<>(mProjectionTypePropertyPaths);+ mProjectionTypePropertyPaths = BundleUtil.deepCopy(mProjectionTypePropertyPaths); mBuilt = false; } }
@@ -17,7 +17,8 @@
package androidx.appsearch.app; import android.annotation.SuppressLint;-import android.os.Bundle;+import android.os.Parcel;+import android.os.Parcelable; import androidx.annotation.IntRange; import androidx.annotation.NonNull;@@ -25,6 +26,11 @@
import androidx.annotation.RequiresFeature; import androidx.annotation.RestrictTo; import androidx.appsearch.annotation.CanIgnoreReturnValue;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags;+import androidx.appsearch.safeparcel.AbstractSafeParcelable;+import androidx.appsearch.safeparcel.SafeParcelable;+import androidx.appsearch.safeparcel.stub.StubCreators.GetSchemaResponseCreator; import androidx.collection.ArrayMap; import androidx.collection.ArraySet; import androidx.core.util.Preconditions;@@ -36,58 +42,88 @@
import java.util.Set; /** The response class of {@link AppSearchSession#getSchemaAsync} */-public final class GetSchemaResponse {- private static final String VERSION_FIELD = "version";- private static final String SCHEMAS_FIELD = "schemas";- private static final String SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD =- "schemasNotDisplayedBySystem";- private static final String SCHEMAS_VISIBLE_TO_PACKAGES_FIELD = "schemasVisibleToPackages";- private static final String SCHEMAS_VISIBLE_TO_PERMISSION_FIELD =- "schemasVisibleToPermissions";- private static final String ALL_REQUIRED_PERMISSION_FIELD =- "allRequiredPermission";[email protected](creator = "GetSchemaResponseCreator")+@SuppressWarnings("HiddenSuperclass")+public final class GetSchemaResponse extends AbstractSafeParcelable {+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @NonNull+ public static final Parcelable.Creator CREATOR =+ new GetSchemaResponseCreator();++ @Field(id = 1, getter = "getVersion")+ private final int mVersion;++ @Field(id = 2)+ final List mSchemas;++ /**+ * List of VisibilityConfigs for the current schema. May be {@code null} if retrieving the+ * visibility settings is not possible on the current backend.+ */+ @Field(id = 3)+ @Nullable+ final List mVisibilityConfigs;++ /**+ * This set contains all schemas most recently successfully provided to+ * {@link AppSearchSession#setSchemaAsync}. We do lazy fetch, the object will be created when+ * you first time fetch it.+ */+ @Nullable+ private Set mSchemasCached;+ /** * This Set contains all schemas that are not displayed by the system. All values in the set are * prefixed with the package-database prefix. We do lazy fetch, the object will be created * when you first time fetch it. */ @Nullable- private Set mSchemasNotDisplayedBySystem;+ private Set mSchemasNotDisplayedBySystemCached;+ /** * This map contains all schemas and {@link PackageIdentifier} that has access to the schema. * All keys in the map are prefixed with the package-database prefix. We do lazy fetch, the * object will be created when you first time fetch it. */ @Nullable- private Map> mSchemasVisibleToPackages;+ private Map> mSchemasVisibleToPackagesCached; /** * This map contains all schemas and Android Permissions combinations that are required to * access the schema. All keys in the map are prefixed with the package-database prefix. We * do lazy fetch, the object will be created when you first time fetch it. * The Map is constructed in ANY-ALL cases. The querier could read the {@link GenericDocument}- * objects under the {@code schemaType} if they holds ALL required permissions of ANY+ * objects under the {@code schemaType} if they hold ALL required permissions of ANY * combinations.- * The value set represents- * {@link androidx.appsearch.app.SetSchemaRequest.AppSearchSupportedPermission}.+ * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set) */ @Nullable- private Map>> mSchemasVisibleToPermissions;-- private final Bundle mBundle;-- GetSchemaResponse(@NonNull Bundle bundle) {- mBundle = Preconditions.checkNotNull(bundle);- }+ private Map>> mSchemasVisibleToPermissionsCached; /**- * Returns the {@link Bundle} populated by this builder.- * @exportToFramework:hide+ * This map contains all publicly visible schemas and the {@link PackageIdentifier} specifying+ * the package that the schemas are from. */- @NonNull- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- public Bundle getBundle() {- return mBundle;+ @Nullable+ private Map mPubliclyVisibleSchemasCached;++ /**+ * This map contains all {@link SchemaVisibilityConfig}s that has access to the schema.+ * All keys in the map are prefixed with the package-database prefix. We do lazy fetch, the+ * object will be created when you first time fetch it.+ */+ @Nullable+ private Map> mSchemasVisibleToConfigsCached;++ @Constructor+ GetSchemaResponse(+ @Param(id = 1) int version,+ @Param(id = 2) @NonNull List schemas,+ @Param(id = 3) @Nullable List visibilityConfigs) {+ mVersion = version;+ mSchemas = Preconditions.checkNotNull(schemas);+ mVisibilityConfigs = visibilityConfigs; } /**@@ -97,25 +133,19 @@
*/ @IntRange(from = 0) public int getVersion() {- return mBundle.getInt(VERSION_FIELD);+ return mVersion; } /** * Return the schemas most recently successfully provided to * {@link AppSearchSession#setSchemaAsync}.- *- *
It is inefficient to call this method repeatedly.
*/ @NonNull- @SuppressWarnings("deprecation") public Set getSchemas() {- ArrayList schemaBundles = Preconditions.checkNotNull(- mBundle.getParcelableArrayList(SCHEMAS_FIELD));- Set schemas = new ArraySet<>(schemaBundles.size());- for (int i = 0; i < schemaBundles.size(); i++) {- schemas.add(new AppSearchSchema(schemaBundles.get(i)));+ if (mSchemasCached == null) {+ mSchemasCached = Collections.unmodifiableSet(new ArraySet<>(mSchemas)); }- return schemas;+ return mSchemasCached; } /**@@ -126,21 +156,22 @@
* called with false. * */- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)- // @exportToFramework:endStrip() @NonNull public Set getSchemaTypesNotDisplayedBySystem() {- checkGetVisibilitySettingSupported();- if (mSchemasNotDisplayedBySystem == null) {- List schemasNotDisplayedBySystemList =- mBundle.getStringArrayList(SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD);- mSchemasNotDisplayedBySystem =- Collections.unmodifiableSet(new ArraySet<>(schemasNotDisplayedBySystemList));+ List visibilityConfigs = getVisibilityConfigsOrThrow();+ if (mSchemasNotDisplayedBySystemCached == null) {+ Set copy = new ArraySet<>();+ for (int i = 0; i < visibilityConfigs.size(); i++) {+ if (visibilityConfigs.get(i).isNotDisplayedBySystem()) {+ copy.add(visibilityConfigs.get(i).getSchemaType());+ }+ }+ mSchemasNotDisplayedBySystemCached = Collections.unmodifiableSet(copy); }- return mSchemasNotDisplayedBySystem;+ return mSchemasNotDisplayedBySystemCached; } /**@@ -151,37 +182,32 @@
* called with false. * */- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)- // @exportToFramework:endStrip() @NonNull- @SuppressWarnings("deprecation") public Map> getSchemaTypesVisibleToPackages() {- checkGetVisibilitySettingSupported();- if (mSchemasVisibleToPackages == null) {- Bundle schemaVisibleToPackagesBundle = Preconditions.checkNotNull(- mBundle.getBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD));+ List visibilityConfigs = getVisibilityConfigsOrThrow();+ if (mSchemasVisibleToPackagesCached == null) { Map> copy = new ArrayMap<>();- for (String key : schemaVisibleToPackagesBundle.keySet()) {- List PackageIdentifierBundles = Preconditions.checkNotNull(- schemaVisibleToPackagesBundle.getParcelableArrayList(key));- Set packageIdentifiers =- new ArraySet<>(PackageIdentifierBundles.size());- for (int i = 0; i < PackageIdentifierBundles.size(); i++) {- packageIdentifiers.add(new PackageIdentifier(PackageIdentifierBundles.get(i)));+ for (int i = 0; i < visibilityConfigs.size(); i++) {+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);+ List visibleToPackages =+ visibilityConfig.getVisibilityConfig().getAllowedPackages();+ if (!visibleToPackages.isEmpty()) {+ copy.put(+ visibilityConfig.getSchemaType(),+ Collections.unmodifiableSet(new ArraySet<>(visibleToPackages))); }- copy.put(key, packageIdentifiers); }- mSchemasVisibleToPackages = Collections.unmodifiableMap(copy);+ mSchemasVisibleToPackagesCached = Collections.unmodifiableMap(copy); }- return mSchemasVisibleToPackages;+ return mSchemasVisibleToPackagesCached; } /**- * Returns a mapping of schema types to the Map of {@link android.Manifest.permission}- * combinations that querier must hold to access that schema type.+ * Returns a mapping of schema types to the set of {@link android.Manifest.permission}+ * combination sets that querier must hold to access that schema type. * *
The querier could read the {@link GenericDocument} objects under the {@code schemaType}
* if they holds ALL required permissions of ANY of the individual value sets.@@ -189,12 +215,12 @@
*
For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
A querier holds both PermissionA and PermissionB has access.
- *
A querier holds both PermissionC and PermissionD has access.
- *
A querier holds only PermissionE has access.
- *
A querier holds both PermissionA and PermissionE has access.
- *
A querier holds only PermissionA doesn't have access.
- *
A querier holds both PermissionA and PermissionC doesn't have access.
+ *
A querier holding both PermissionA and PermissionB has access.
+ *
A querier holding both PermissionC and PermissionD has access.
+ *
A querier holding only PermissionE has access.
+ *
A querier holding both PermissionA and PermissionE has access.
+ *
A querier holding only PermissionA doesn't have access.
+ *
A querier holding only PermissionA and PermissionC doesn't have access.
* * * @return The map contains schema type and all combinations of required permission for querier@@ -208,56 +234,118 @@
* called with false. * */- // @exportToFramework:startStrip()+ // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)- // @exportToFramework:endStrip() @NonNull- @SuppressWarnings("deprecation") public Map>> getRequiredPermissionsForSchemaTypeVisibility() {- checkGetVisibilitySettingSupported();- if (mSchemasVisibleToPermissions == null) {+ List visibilityConfigs = getVisibilityConfigsOrThrow();+ if (mSchemasVisibleToPermissionsCached == null) { Map>> copy = new ArrayMap<>();- Bundle schemaVisibleToPermissionBundle = Preconditions.checkNotNull(- mBundle.getBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD));- for (String key : schemaVisibleToPermissionBundle.keySet()) {- ArrayList allRequiredPermissionsBundle =- schemaVisibleToPermissionBundle.getParcelableArrayList(key);- Set> visibleToPermissions = new ArraySet<>();- if (allRequiredPermissionsBundle != null) {- // This should never be null- for (int i = 0; i < allRequiredPermissionsBundle.size(); i++) {- visibleToPermissions.add(new ArraySet<>(allRequiredPermissionsBundle.get(i)- .getIntegerArrayList(ALL_REQUIRED_PERMISSION_FIELD)));- }+ for (int i = 0; i < visibilityConfigs.size(); i++) {+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);+ Set> visibleToPermissions =+ visibilityConfig.getVisibilityConfig().getRequiredPermissions();+ if (!visibleToPermissions.isEmpty()) {+ copy.put(+ visibilityConfig.getSchemaType(),+ Collections.unmodifiableSet(visibleToPermissions)); }- copy.put(key, visibleToPermissions); }- mSchemasVisibleToPermissions = Collections.unmodifiableMap(copy);+ mSchemasVisibleToPermissionsCached = Collections.unmodifiableMap(copy); }- return mSchemasVisibleToPermissions;+ return mSchemasVisibleToPermissionsCached; }- private void checkGetVisibilitySettingSupported() {- if (!mBundle.containsKey(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD)) {+ /**+ * Returns a mapping of publicly visible schemas to the {@link PackageIdentifier} specifying+ * the package the schemas are from.+ *+ *
If no schemas have been set as publicly visible, an empty set will be returned.
+ * + * @throws UnsupportedOperationException if {@link Builder#setVisibilitySettingSupported} was+ * called with false.+ * + */+ @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)+ @NonNull+ public Map getPubliclyVisibleSchemas() {+ List visibilityConfigs = getVisibilityConfigsOrThrow();+ if (mPubliclyVisibleSchemasCached == null) {+ Map copy = new ArrayMap<>();+ for (int i = 0; i < visibilityConfigs.size(); i++) {+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);+ PackageIdentifier publiclyVisibleTargetPackage =+ visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage();+ if (publiclyVisibleTargetPackage != null) {+ copy.put(visibilityConfig.getSchemaType(), publiclyVisibleTargetPackage);+ }+ }+ mPubliclyVisibleSchemasCached = Collections.unmodifiableMap(copy);+ }+ return mPubliclyVisibleSchemasCached;+ }++ /**+ * Returns a mapping of schema types to the set of {@link SchemaVisibilityConfig} that have+ * access to that schema type.+ *+ * @see SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig+ */+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)+ @NonNull+ public Map> getSchemaTypesVisibleToConfigs() {+ List visibilityConfigs = getVisibilityConfigsOrThrow();+ if (mSchemasVisibleToConfigsCached == null) {+ Map> copy = new ArrayMap<>();+ for (int i = 0; i < visibilityConfigs.size(); i++) {+ InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);+ Set nestedVisibilityConfigs =+ visibilityConfig.getVisibleToConfigs();+ if (!nestedVisibilityConfigs.isEmpty()) {+ copy.put(visibilityConfig.getSchemaType(),+ Collections.unmodifiableSet(nestedVisibilityConfigs));+ }+ }+ mSchemasVisibleToConfigsCached = Collections.unmodifiableMap(copy);+ }+ return mSchemasVisibleToConfigsCached;+ }++ @NonNull+ private List getVisibilityConfigsOrThrow() {+ List visibilityConfigs = mVisibilityConfigs;+ if (visibilityConfigs == null) { throw new UnsupportedOperationException("Get visibility setting is not supported with " + "this backend/Android API level combination."); }+ return visibilityConfigs;+ }++ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @Override+ public void writeToParcel(@NonNull Parcel dest, int flags) {+ GetSchemaResponseCreator.writeToParcel(this, dest, flags); } /** Builder for {@link GetSchemaResponse} objects. */ public static final class Builder { private int mVersion = 0;- private ArrayList mSchemaBundles = new ArrayList<>();+ private ArrayList mSchemas = new ArrayList<>(); /** * Creates the object when we actually set them. If we never set visibility settings, we * should throw {@link UnsupportedOperationException} in the visibility getters. */ @Nullable- private ArrayList mSchemasNotDisplayedBySystem;- private Bundle mSchemasVisibleToPackages;- private Bundle mSchemasVisibleToPermissions;+ private Map mVisibilityConfigBuilders; private boolean mBuilt = false; /** Create a {@link Builder} object} */@@ -284,7 +372,7 @@
public Builder addSchema(@NonNull AppSearchSchema schema) { Preconditions.checkNotNull(schema); resetIfBuilt();- mSchemaBundles.add(schema.getBundle());+ mSchemas.add(schema); return this; }@@ -302,10 +390,9 @@
public Builder addSchemaTypeNotDisplayedBySystem(@NonNull String schemaType) { Preconditions.checkNotNull(schemaType); resetIfBuilt();- if (mSchemasNotDisplayedBySystem == null) {- mSchemasNotDisplayedBySystem = new ArrayList<>();- }- mSchemasNotDisplayedBySystem.add(schemaType);+ InternalVisibilityConfig.Builder visibilityConfigBuilder =+ getOrCreateVisibilityConfigBuilder(schemaType);+ visibilityConfigBuilder.setNotDisplayedBySystem(true); return this; }@@ -337,11 +424,11 @@
Preconditions.checkNotNull(schemaType); Preconditions.checkNotNull(packageIdentifiers); resetIfBuilt();- ArrayList bundles = new ArrayList<>(packageIdentifiers.size());+ InternalVisibilityConfig.Builder visibilityConfigBuilder =+ getOrCreateVisibilityConfigBuilder(schemaType); for (PackageIdentifier packageIdentifier : packageIdentifiers) {- bundles.add(packageIdentifier.getBundle());+ visibilityConfigBuilder.addVisibleToPackage(packageIdentifier); }- mSchemasVisibleToPackages.putParcelableArrayList(schemaType, bundles); return this; }@@ -364,41 +451,103 @@
*
A querier holds both PermissionA and PermissionC doesn't have access.
* *+ * @param schemaType The schema type to set visibility on.+ * @param visibleToPermissionSets The Sets of Android permissions that will be required to+ * access the given schema. * @see android.Manifest.permission#READ_SMS * @see android.Manifest.permission#READ_CALENDAR * @see android.Manifest.permission#READ_CONTACTS * @see android.Manifest.permission#READ_EXTERNAL_STORAGE * @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA * @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA- *- * @param schemaType The schema type to set visibility on.- * @param visibleToPermissions The Android permissions that will be required to access- * the given schema. */+ // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden // Getter getRequiredPermissionsForSchemaTypeVisibility returns a map for all schemaTypes. @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder")+ // @SetSchemaRequest is an IntDef annotation applied to Set>.+ @SuppressWarnings("SupportAnnotationUsage") @NonNull public Builder setRequiredPermissionsForSchemaTypeVisibility( @NonNull String schemaType, @SetSchemaRequest.AppSearchSupportedPermission @NonNull- Set> visibleToPermissions) {+ Set> visibleToPermissionSets) { Preconditions.checkNotNull(schemaType);- Preconditions.checkNotNull(visibleToPermissions);+ Preconditions.checkNotNull(visibleToPermissionSets); resetIfBuilt();- ArrayList visibleToPermissionsBundle = new ArrayList<>();- for (Set allRequiredPermissions : visibleToPermissions) {- for (int permission : allRequiredPermissions) {- Preconditions.checkArgumentInRange(permission, SetSchemaRequest.READ_SMS,- SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA, "permission");- }- Bundle allRequiredPermissionsBundle = new Bundle();- allRequiredPermissionsBundle.putIntegerArrayList(- ALL_REQUIRED_PERMISSION_FIELD, new ArrayList<>(allRequiredPermissions));- visibleToPermissionsBundle.add(allRequiredPermissionsBundle);+ InternalVisibilityConfig.Builder visibilityConfigBuilder =+ getOrCreateVisibilityConfigBuilder(schemaType);+ for (Set visibleToPermissions : visibleToPermissionSets) {+ visibilityConfigBuilder.addVisibleToPermissions(visibleToPermissions); }- mSchemasVisibleToPermissions.putParcelableArrayList(schemaType,- visibleToPermissionsBundle);+ return this;+ }++ /**+ * Specify that the schema should be publicly available, to packages which already have+ * visibility to {@code packageIdentifier}.+ *+ * @param schemaType the schema to make publicly accessible.+ * @param packageIdentifier the package from which the document schema is from.+ * @see SetSchemaRequest.Builder#setPubliclyVisibleSchema+ */+ // Merged list available from getPubliclyVisibleSchemas+ @CanIgnoreReturnValue+ @SuppressLint("MissingGetterMatchingBuilder")+ @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)+ @NonNull+ public Builder setPubliclyVisibleSchema(+ @NonNull String schemaType, @NonNull PackageIdentifier packageIdentifier) {+ Preconditions.checkNotNull(schemaType);+ Preconditions.checkNotNull(packageIdentifier);+ resetIfBuilt();+ InternalVisibilityConfig.Builder visibilityConfigBuilder =+ getOrCreateVisibilityConfigBuilder(schemaType);+ visibilityConfigBuilder.setPubliclyVisibleTargetPackage(packageIdentifier);+ return this;+ }++ /**+ * Sets the documents from the provided {@code schemaType} can be read by the caller if they+ * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}.+ *+ *
The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A
+ * caller must match ALL requirements to access the schema. For example, a caller must hold+ * required permissions AND it is a specified package.+ *+ *
The querier could have access if they match ALL requirements in ANY of the given
+ * {@link SchemaVisibilityConfig}s+ *+ *
For example, if the Set contains {@code {% verbatim %}{{PackageA and Permission1},
+ * {PackageB and Permission2}}{% endverbatim %}}.+ *
+ *
A querier from packageA could read if they holds Permission1.
+ *
A querier from packageA could NOT read if they only holds Permission2 instead of
+ * Permission1.+ *
A querier from packageB could read if they holds Permission2.
+ *
A querier from packageC could never read.
+ *
A querier holds both PermissionA and PermissionE has access.
+ * + *+ * @param schemaType The schema type to set visibility on.+ * @param visibleToConfigs The {@link SchemaVisibilityConfig}s hold all requirements that+ * a call must to match to access the schema.+ */+ // Merged map available from getSchemasVisibleToConfigs+ @CanIgnoreReturnValue+ @SuppressLint("MissingGetterMatchingBuilder")+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)+ @NonNull+ public Builder setSchemaTypeVisibleToConfigs(@NonNull String schemaType,+ @NonNull Set visibleToConfigs) {+ Preconditions.checkNotNull(schemaType);+ Preconditions.checkNotNull(visibleToConfigs);+ resetIfBuilt();+ InternalVisibilityConfig.Builder visibilityConfigBuilder =+ getOrCreateVisibilityConfigBuilder(schemaType);+ for (SchemaVisibilityConfig visibleToConfig : visibleToConfigs) {+ visibilityConfigBuilder.addVisibleToConfig(visibleToConfig);+ } return this; }@@ -416,17 +565,14 @@
* @exportToFramework:hide */ // Visibility setting is determined by SDK version, so it won't be needed in framework+ @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") @NonNull public Builder setVisibilitySettingSupported(boolean visibilitySettingSupported) { if (visibilitySettingSupported) {- mSchemasNotDisplayedBySystem = new ArrayList<>();- mSchemasVisibleToPackages = new Bundle();- mSchemasVisibleToPermissions = new Bundle();+ mVisibilityConfigBuilders = new ArrayMap<>(); } else {- mSchemasNotDisplayedBySystem = null;- mSchemasVisibleToPackages = null;- mSchemasVisibleToPermissions = null;+ mVisibilityConfigBuilders = null; } return this; }@@ -434,33 +580,37 @@
/** Builds a {@link GetSchemaResponse} object. */ @NonNull public GetSchemaResponse build() {- Bundle bundle = new Bundle();- bundle.putInt(VERSION_FIELD, mVersion);- bundle.putParcelableArrayList(SCHEMAS_FIELD, mSchemaBundles);- if (mSchemasNotDisplayedBySystem != null) {- // Only save the visibility fields if it was actually set.- bundle.putStringArrayList(SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD,- mSchemasNotDisplayedBySystem);- bundle.putBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD, mSchemasVisibleToPackages);- bundle.putBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD, mSchemasVisibleToPermissions);+ List visibilityConfigs = null;+ if (mVisibilityConfigBuilders != null) {+ visibilityConfigs = new ArrayList<>();+ for (InternalVisibilityConfig.Builder builder :+ mVisibilityConfigBuilders.values()) {+ visibilityConfigs.add(builder.build());+ } } mBuilt = true;- return new GetSchemaResponse(bundle);+ return new GetSchemaResponse(mVersion, mSchemas, visibilityConfigs);+ }++ @NonNull+ private InternalVisibilityConfig.Builder getOrCreateVisibilityConfigBuilder(+ @NonNull String schemaType) {+ if (mVisibilityConfigBuilders == null) {+ throw new IllegalStateException("GetSchemaResponse is not configured with"+ + "visibility setting support");+ }+ InternalVisibilityConfig.Builder builder = mVisibilityConfigBuilders.get(schemaType);+ if (builder == null) {+ builder = new InternalVisibilityConfig.Builder(schemaType);+ mVisibilityConfigBuilders.put(schemaType, builder);+ }+ return builder; } private void resetIfBuilt() { if (mBuilt) {- mSchemaBundles = new ArrayList<>(mSchemaBundles);- if (mSchemasNotDisplayedBySystem != null) {- // Only reset the visibility fields if it was actually set.- mSchemasNotDisplayedBySystem = new ArrayList<>(mSchemasNotDisplayedBySystem);- Bundle copyVisibleToPackages = new Bundle();- copyVisibleToPackages.putAll(mSchemasVisibleToPackages);- mSchemasVisibleToPackages = copyVisibleToPackages;- Bundle copyVisibleToPermissions = new Bundle();- copyVisibleToPermissions.putAll(mSchemasVisibleToPermissions);- mSchemasVisibleToPermissions = copyVisibleToPermissions;- }+ // No need to copy mVisibilityConfigBuilders -- it gets copied during build().+ mSchemas = new ArrayList<>(mSchemas); mBuilt = false; } }
@@ -0,0 +1,382 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.app;++import android.os.Parcel;+import android.os.Parcelable;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RestrictTo;+import androidx.appsearch.annotation.CanIgnoreReturnValue;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags;+import androidx.appsearch.safeparcel.AbstractSafeParcelable;+import androidx.appsearch.safeparcel.SafeParcelable;+import androidx.appsearch.safeparcel.stub.StubCreators.InternalVisibilityConfigCreator;+import androidx.collection.ArraySet;++import java.util.ArrayList;+import java.util.List;+import java.util.Map;+import java.util.Objects;+import java.util.Set;++/**+ * An expanded version of {@link SchemaVisibilityConfig} which includes fields for internal use by+ * AppSearch.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)[email protected](creator = "InternalVisibilityConfigCreator")+public final class InternalVisibilityConfig extends AbstractSafeParcelable {+ @NonNull+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ public static final Parcelable.Creator CREATOR =+ new InternalVisibilityConfigCreator();++ /**+ * Build the List of {@link InternalVisibilityConfig}s from visibility settings.+ */+ @NonNull+ public static List toInternalVisibilityConfigs(+ @NonNull SetSchemaRequest setSchemaRequest) {+ Set searchSchemas = setSchemaRequest.getSchemas();+ Set schemasNotDisplayedBySystem = setSchemaRequest.getSchemasNotDisplayedBySystem();+ Map> schemasVisibleToPackages =+ setSchemaRequest.getSchemasVisibleToPackages();+ Map>> schemasVisibleToPermissions =+ setSchemaRequest.getRequiredPermissionsForSchemaTypeVisibility();+ Map publiclyVisibleSchemas =+ setSchemaRequest.getPubliclyVisibleSchemas();+ Map> schemasVisibleToConfigs =+ setSchemaRequest.getSchemasVisibleToConfigs();++ List result = new ArrayList<>(searchSchemas.size());+ for (AppSearchSchema searchSchema : searchSchemas) {+ String schemaType = searchSchema.getSchemaType();+ InternalVisibilityConfig.Builder builder =+ new InternalVisibilityConfig.Builder(schemaType)+ .setNotDisplayedBySystem(+ schemasNotDisplayedBySystem.contains(schemaType));++ Set visibleToPackages = schemasVisibleToPackages.get(schemaType);+ if (visibleToPackages != null) {+ for (PackageIdentifier packageIdentifier : visibleToPackages) {+ builder.addVisibleToPackage(packageIdentifier);+ }+ }++ Set> visibleToPermissionSets = schemasVisibleToPermissions.get(schemaType);+ if (visibleToPermissionSets != null) {+ for (Set visibleToPermissions : visibleToPermissionSets) {+ builder.addVisibleToPermissions(visibleToPermissions);+ }+ }++ PackageIdentifier publiclyVisibleTargetPackage = publiclyVisibleSchemas.get(schemaType);+ if (publiclyVisibleTargetPackage != null) {+ builder.setPubliclyVisibleTargetPackage(publiclyVisibleTargetPackage);+ }++ Set visibleToConfigs = schemasVisibleToConfigs.get(schemaType);+ if (visibleToConfigs != null) {+ for (SchemaVisibilityConfig schemaVisibilityConfig : visibleToConfigs) {+ builder.addVisibleToConfig(schemaVisibilityConfig);+ }+ }++ result.add(builder.build());+ }+ return result;+ }++ @NonNull+ @Field(id = 1, getter = "getSchemaType")+ private final String mSchemaType;++ @Field(id = 2, getter = "isNotDisplayedBySystem")+ private final boolean mIsNotDisplayedBySystem;++ /** The public visibility settings available in VisibilityConfig. */+ @NonNull+ @Field(id = 3, getter = "getVisibilityConfig")+ private final SchemaVisibilityConfig mVisibilityConfig;++ /** Extended visibility settings from {@link SetSchemaRequest#getSchemasVisibleToConfigs()} */+ @NonNull+ @Field(id = 4)+ final List mVisibleToConfigs;++ @Constructor+ InternalVisibilityConfig(+ @Param(id = 1) @NonNull String schemaType,+ @Param(id = 2) boolean isNotDisplayedBySystem,+ @Param(id = 3) @NonNull SchemaVisibilityConfig schemaVisibilityConfig,+ @Param(id = 4) @NonNull List visibleToConfigs) {+ mIsNotDisplayedBySystem = isNotDisplayedBySystem;+ mSchemaType = Objects.requireNonNull(schemaType);+ mVisibilityConfig = Objects.requireNonNull(schemaVisibilityConfig);+ mVisibleToConfigs = Objects.requireNonNull(visibleToConfigs);+ }++ /**+ * Gets the schemaType for this VisibilityConfig.+ *+ *
This is being used as the document id when we convert a {@link InternalVisibilityConfig}
+ * to a {@link GenericDocument}.+ */+ @NonNull+ public String getSchemaType() {+ return mSchemaType;+ }++ /** Returns whether this schema is visible to the system. */+ public boolean isNotDisplayedBySystem() {+ return mIsNotDisplayedBySystem;+ }++ /**+ * Returns the visibility settings stored in the public {@link SchemaVisibilityConfig} object.+ */+ @NonNull+ public SchemaVisibilityConfig getVisibilityConfig() {+ return mVisibilityConfig;+ }++ /**+ * Returns required {@link SchemaVisibilityConfig} sets for a caller need to match to access the+ * schema this {@link InternalVisibilityConfig} represents.+ */+ @NonNull+ public Set getVisibleToConfigs() {+ return new ArraySet<>(mVisibleToConfigs);+ }++ @Override+ public void writeToParcel(@NonNull Parcel dest, int flags) {+ InternalVisibilityConfigCreator.writeToParcel(this, dest, flags);+ }++ @Override+ public boolean equals(@Nullable Object o) {+ if (this == o) {+ return true;+ }+ if (o == null) {+ return false;+ }+ if (!(o instanceof InternalVisibilityConfig)) {+ return false;+ }+ InternalVisibilityConfig that = (InternalVisibilityConfig) o;+ return mIsNotDisplayedBySystem == that.mIsNotDisplayedBySystem+ && Objects.equals(mSchemaType, that.mSchemaType)+ && Objects.equals(mVisibilityConfig, that.mVisibilityConfig)+ && Objects.equals(mVisibleToConfigs, that.mVisibleToConfigs);+ }++ @Override+ public int hashCode() {+ return Objects.hash(mIsNotDisplayedBySystem, mSchemaType, mVisibilityConfig,+ mVisibleToConfigs);+ }++ /** The builder class of {@link InternalVisibilityConfig}. */+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)+ public static final class Builder {+ private String mSchemaType;+ private boolean mIsNotDisplayedBySystem;+ private SchemaVisibilityConfig.Builder mVisibilityConfigBuilder;+ private List mVisibleToConfigs = new ArrayList<>();+ private boolean mBuilt;++ /**+ * Creates a {@link Builder} for a {@link InternalVisibilityConfig}.+ *+ * @param schemaType The SchemaType of the {@link AppSearchSchema} that this {@link+ * InternalVisibilityConfig} represents. The package and database prefix+ * will be added in server side. We are using prefixed schema type to be+ * the final id of this {@link InternalVisibilityConfig}. This will be+ * used as as an AppSearch id.+ * @see GenericDocument#getId+ */+ public Builder(@NonNull String schemaType) {+ mSchemaType = Objects.requireNonNull(schemaType);+ mVisibilityConfigBuilder = new SchemaVisibilityConfig.Builder();+ }++ /** Creates a {@link Builder} from an existing {@link InternalVisibilityConfig} */+ public Builder(@NonNull InternalVisibilityConfig internalVisibilityConfig) {+ Objects.requireNonNull(internalVisibilityConfig);+ mSchemaType = internalVisibilityConfig.mSchemaType;+ mIsNotDisplayedBySystem = internalVisibilityConfig.mIsNotDisplayedBySystem;+ mVisibilityConfigBuilder = new SchemaVisibilityConfig.Builder(+ internalVisibilityConfig.getVisibilityConfig());+ mVisibleToConfigs = internalVisibilityConfig.mVisibleToConfigs;+ }++ /** Sets schemaType, which will be as the id when converting to {@link GenericDocument}. */+ @NonNull+ @CanIgnoreReturnValue+ public Builder setSchemaType(@NonNull String schemaType) {+ resetIfBuilt();+ mSchemaType = Objects.requireNonNull(schemaType);+ return this;+ }++ /**+ * Resets all values contained in the VisibilityConfig with the values from the given+ * VisibiltiyConfig.+ */+ @NonNull+ @CanIgnoreReturnValue+ public Builder setVisibilityConfig(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) {+ resetIfBuilt();+ mVisibilityConfigBuilder = new SchemaVisibilityConfig.Builder(schemaVisibilityConfig);+ return this;+ }++ /**+ * Sets whether this schema has opted out of platform surfacing.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {+ resetIfBuilt();+ mIsNotDisplayedBySystem = notDisplayedBySystem;+ return this;+ }++ /**+ * Add {@link PackageIdentifier} of packages which has access to this schema.+ *+ * @see SchemaVisibilityConfig.Builder#addAllowedPackage+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {+ resetIfBuilt();+ mVisibilityConfigBuilder.addAllowedPackage(packageIdentifier);+ return this;+ }++ /**+ * Clears the list of packages which have access to this schema.+ *+ * @see SchemaVisibilityConfig.Builder#clearAllowedPackages+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder clearVisibleToPackages() {+ resetIfBuilt();+ mVisibilityConfigBuilder.clearAllowedPackages();+ return this;+ }++ /**+ * Adds a set of required Android {@link android.Manifest.permission} combination a package+ * needs to hold to access the schema.+ *+ * @see SchemaVisibilityConfig.Builder#addRequiredPermissions+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder addVisibleToPermissions(@NonNull Set visibleToPermissions) {+ resetIfBuilt();+ mVisibilityConfigBuilder.addRequiredPermissions(visibleToPermissions);+ return this;+ }++ /**+ * Clears all required permissions combinations set to this {@link SchemaVisibilityConfig}.+ *+ * @see SchemaVisibilityConfig.Builder#clearRequiredPermissions+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder clearVisibleToPermissions() {+ resetIfBuilt();+ mVisibilityConfigBuilder.clearRequiredPermissions();+ return this;+ }++ /**+ * Specify that this schema should be publicly available, to the same packages that have+ * visibility to the package passed as a parameter. This visibility is determined by the+ * result of {@link android.content.pm.PackageManager#canPackageQuery}.+ *+ * @see SchemaVisibilityConfig.Builder#setPubliclyVisibleTargetPackage+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setPubliclyVisibleTargetPackage(+ @Nullable PackageIdentifier packageIdentifier) {+ resetIfBuilt();+ mVisibilityConfigBuilder.setPubliclyVisibleTargetPackage(packageIdentifier);+ return this;+ }++ /**+ * Add the {@link SchemaVisibilityConfig} for a caller need to match to access the schema+ * this {@link InternalVisibilityConfig} represents.+ *+ *
You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig},
+ * and the querier will have access if they match ANY of the+ * {@link SchemaVisibilityConfig}.+ *+ * @param schemaVisibilityConfig The {@link SchemaVisibilityConfig} hold all requirements+ * that a call must match to access the schema.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder addVisibleToConfig(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) {+ Objects.requireNonNull(schemaVisibilityConfig);+ resetIfBuilt();+ mVisibleToConfigs.add(schemaVisibilityConfig);+ return this;+ }++ /** Clears the set of {@link SchemaVisibilityConfig} which have access to this schema. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder clearVisibleToConfig() {+ resetIfBuilt();+ mVisibleToConfigs.clear();+ return this;+ }++ private void resetIfBuilt() {+ if (mBuilt) {+ mVisibleToConfigs = new ArrayList<>(mVisibleToConfigs);+ mBuilt = false;+ }+ }++ /** Build a {@link InternalVisibilityConfig} */+ @NonNull+ public InternalVisibilityConfig build() {+ mBuilt = true;+ return new InternalVisibilityConfig(+ mSchemaType,+ mIsNotDisplayedBySystem,+ mVisibilityConfigBuilder.build(),+ mVisibleToConfigs);+ }+ }+}
@@ -0,0 +1,105 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+// @exportToFramework:skipFile()+package androidx.appsearch.app;++import android.content.Context;+import android.os.UserHandle;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RestrictTo;++import java.io.File;+import java.util.concurrent.BlockingQueue;+import java.util.concurrent.ExecutorService;+import java.util.concurrent.Executors;+import java.util.concurrent.ThreadPoolExecutor;+import java.util.concurrent.TimeUnit;++/**+ * Contains utility methods for Framework implementation of AppSearch.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public class JetpackAppSearchEnvironment implements AppSearchEnvironment {++ /**+ * Returns AppSearch directory in the credential encrypted system directory for the given user.+ *+ *
This folder should only be accessed after unlock.
+ */+ @Override+ @NonNull+ public File getAppSearchDir(@NonNull Context context, @Nullable UserHandle unused) {+ return new File(context.getFilesDir(), "appsearch");+ }++ /** Creates context for the user based on the userHandle. */+ @Override+ @NonNull+ public Context createContextAsUser(@NonNull Context context, @NonNull UserHandle userHandle) {+ return context;+ }++ /** Creates and returns a ThreadPoolExecutor for given parameters. */+ @Override+ @NonNull+ public ExecutorService createExecutorService(+ int corePoolSize,+ int maxConcurrency,+ long keepAliveTime,+ @NonNull TimeUnit unit,+ @NonNull BlockingQueue workQueue,+ int priority) {+ return new ThreadPoolExecutor(+ corePoolSize,+ maxConcurrency,+ keepAliveTime,+ unit,+ workQueue);+ }++ /** Creates and returns an ExecutorService with a single thread. */+ @Override+ @NonNull+ public ExecutorService createSingleThreadExecutor() {+ return Executors.newSingleThreadExecutor();+ }++ /** Creates and returns an Executor with cached thread pools. */+ @Override+ @NonNull+ public ExecutorService createCachedThreadPoolExecutor() {+ return Executors.newCachedThreadPool();+ }++ /**+ * Returns a cache directory for creating temporary files like in case of migrating documents.+ */+ @Override+ @Nullable+ public File getCacheDir(@NonNull Context context) {+ return context.getCacheDir();+ }++ @Override+ public boolean isInfoLoggingEnabled() {+ // INFO logging is enabled by default in Jetpack AppSearch.+ return true;+ }+}
@@ -16,16 +16,23 @@
package androidx.appsearch.app;-import android.os.Bundle;+import android.os.Parcel;+import android.os.Parcelable; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.appsearch.annotation.CanIgnoreReturnValue;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags;+import androidx.appsearch.safeparcel.AbstractSafeParcelable;+import androidx.appsearch.safeparcel.SafeParcelable;+import androidx.appsearch.safeparcel.stub.StubCreators.JoinSpecCreator; import androidx.core.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy;+import java.util.Objects; /** * This class represents the specifications for the joining operation in search.@@ -117,12 +124,29 @@
* return the signals calculated by scoring the joined documents using the scoring strategy in the * nested {@link SearchSpec}, as in {@link SearchResult#getRankingSignal}. */-public final class JoinSpec {- static final String NESTED_QUERY = "nestedQuery";- static final String NESTED_SEARCH_SPEC = "nestedSearchSpec";- static final String CHILD_PROPERTY_EXPRESSION = "childPropertyExpression";- static final String MAX_JOINED_RESULT_COUNT = "maxJoinedResultCount";- static final String AGGREGATION_SCORING_STRATEGY = "aggregationScoringStrategy";[email protected](creator = "JoinSpecCreator")+@SuppressWarnings("HiddenSuperclass")+public final class JoinSpec extends AbstractSafeParcelable {+ /** Creator class for {@link JoinSpec}. */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @NonNull+ public static final Parcelable.Creator CREATOR = new JoinSpecCreator();++ @Field(id = 1, getter = "getNestedQuery")+ private final String mNestedQuery;++ @Field(id = 2, getter = "getNestedSearchSpec")+ private final SearchSpec mNestedSearchSpec;++ @Field(id = 3, getter = "getChildPropertyExpression")+ private final String mChildPropertyExpression;++ @Field(id = 4, getter = "getMaxJoinedResultCount")+ private final int mMaxJoinedResultCount;++ @Field(id = 5, getter = "getAggregationScoringStrategy")+ private final int mAggregationScoringStrategy; private static final int DEFAULT_MAX_JOINED_RESULT_COUNT = 10;@@ -158,8 +182,10 @@
public @interface AggregationScoringStrategy { }- /** Do not score the aggregation of joined documents. This is for the case where we want to- * perform a join, but keep the parent ranking signal. */+ /**+ * Do not score the aggregation of joined documents. This is for the case where we want to+ * perform a join, but keep the parent ranking signal.+ */ public static final int AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL = 0; /** Score the aggregation of joined documents by counting the number of results. */ public static final int AGGREGATION_SCORING_RESULT_COUNT = 1;@@ -172,33 +198,27 @@
/** Score the aggregation of joined documents using the sum of ranking signal. */ public static final int AGGREGATION_SCORING_SUM_RANKING_SIGNAL = 5;- private final Bundle mBundle;-- /** @exportToFramework:hide */- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- public JoinSpec(@NonNull Bundle bundle) {- Preconditions.checkNotNull(bundle);- mBundle = bundle;+ @Constructor+ JoinSpec(+ @Param(id = 1) @NonNull String nestedQuery,+ @Param(id = 2) @NonNull SearchSpec nestedSearchSpec,+ @Param(id = 3) @NonNull String childPropertyExpression,+ @Param(id = 4) int maxJoinedResultCount,+ @Param(id = 5) @AggregationScoringStrategy int aggregationScoringStrategy) {+ mNestedQuery = Objects.requireNonNull(nestedQuery);+ mNestedSearchSpec = Objects.requireNonNull(nestedSearchSpec);+ mChildPropertyExpression = Objects.requireNonNull(childPropertyExpression);+ mMaxJoinedResultCount = maxJoinedResultCount;+ mAggregationScoringStrategy = aggregationScoringStrategy; }- /**- * Returns the {@link Bundle} populated by this builder.- *- * @exportToFramework:hide- */- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- @NonNull- public Bundle getBundle() {- return mBundle;- } /** * Returns the query to run on the joined documents.- * */ @NonNull public String getNestedQuery() {- return mBundle.getString(NESTED_QUERY);+ return mNestedQuery; } /**@@ -211,7 +231,7 @@
*/ @NonNull public String getChildPropertyExpression() {- return mBundle.getString(CHILD_PROPERTY_EXPRESSION);+ return mChildPropertyExpression; } /**@@ -219,7 +239,7 @@
* with a default of 10 SearchResults. */ public int getMaxJoinedResultCount() {- return mBundle.getInt(MAX_JOINED_RESULT_COUNT);+ return mMaxJoinedResultCount; } /**@@ -231,7 +251,7 @@
*/ @NonNull public SearchSpec getNestedSearchSpec() {- return new SearchSpec(mBundle.getBundle(NESTED_SEARCH_SPEC));+ return mNestedSearchSpec; } /**@@ -244,7 +264,14 @@
*/ @AggregationScoringStrategy public int getAggregationScoringStrategy() {- return mBundle.getInt(AGGREGATION_SCORING_STRATEGY);+ return mAggregationScoringStrategy;+ }++ @Override+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ public void writeToParcel(@NonNull Parcel dest, int flags) {+ JoinSpecCreator.writeToParcel(this, dest, flags); } /** Builder for {@link JoinSpec objects}. */@@ -257,7 +284,8 @@
private SearchSpec mNestedSearchSpec = EMPTY_SEARCH_SPEC; private final String mChildPropertyExpression; private int mMaxJoinedResultCount = DEFAULT_MAX_JOINED_RESULT_COUNT;- @AggregationScoringStrategy private int mAggregationScoringStrategy =+ @AggregationScoringStrategy+ private int mAggregationScoringStrategy = AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL; /**@@ -289,6 +317,17 @@
mChildPropertyExpression = childPropertyExpression; }+ /** @exportToFramework:hide */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ public Builder(@NonNull JoinSpec joinSpec) {+ Preconditions.checkNotNull(joinSpec);+ mNestedQuery = joinSpec.getNestedQuery();+ mNestedSearchSpec = joinSpec.getNestedSearchSpec();+ mChildPropertyExpression = joinSpec.getChildPropertyExpression();+ mMaxJoinedResultCount = joinSpec.getMaxJoinedResultCount();+ mAggregationScoringStrategy = joinSpec.getAggregationScoringStrategy();+ }+ /** * Sets the query and the SearchSpec for the documents being joined. This will score and * rank the joined documents as well as filter the joined documents.@@ -362,13 +401,13 @@
*/ @NonNull public JoinSpec build() {- Bundle bundle = new Bundle();- bundle.putString(NESTED_QUERY, mNestedQuery);- bundle.putBundle(NESTED_SEARCH_SPEC, mNestedSearchSpec.getBundle());- bundle.putString(CHILD_PROPERTY_EXPRESSION, mChildPropertyExpression);- bundle.putInt(MAX_JOINED_RESULT_COUNT, mMaxJoinedResultCount);- bundle.putInt(AGGREGATION_SCORING_STRATEGY, mAggregationScoringStrategy);- return new JoinSpec(bundle);+ return new JoinSpec(+ mNestedQuery,+ mNestedSearchSpec,+ mChildPropertyExpression,+ mMaxJoinedResultCount,+ mAggregationScoringStrategy+ ); } } }
@@ -16,20 +16,18 @@
package androidx.appsearch.app;-import android.os.Bundle;- import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo;-import androidx.appsearch.util.BundleUtil;+import androidx.appsearch.safeparcel.PackageIdentifierParcel; import androidx.core.util.Preconditions;-/** This class represents a uniquely identifiable package. */+/**+ * This class represents a uniquely identifiable package.+ */ public class PackageIdentifier {- private static final String PACKAGE_NAME_FIELD = "packageName";- private static final String SHA256_CERTIFICATE_FIELD = "sha256Certificate";-- private final Bundle mBundle;+ @NonNull+ private final PackageIdentifierParcel mPackageIdentifierParcel; /** * Creates a unique identifier for a package.@@ -43,36 +41,41 @@
* new android.content.pm.Signature(outputDigest).toByteArray(); *
*- * @param packageName Name of the package.+ * @param packageName Name of the package. * @param sha256Certificate SHA-256 certificate digest of the package. */ public PackageIdentifier(@NonNull String packageName, @NonNull byte[] sha256Certificate) {- mBundle = new Bundle();- mBundle.putString(PACKAGE_NAME_FIELD, packageName);- mBundle.putByteArray(SHA256_CERTIFICATE_FIELD, sha256Certificate);+ Preconditions.checkNotNull(packageName);+ Preconditions.checkNotNull(sha256Certificate);+ mPackageIdentifierParcel = new PackageIdentifierParcel(packageName, sha256Certificate); } /** @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- public PackageIdentifier(@NonNull Bundle bundle) {- mBundle = Preconditions.checkNotNull(bundle);+ public PackageIdentifier(@NonNull PackageIdentifierParcel packageIdentifierParcel) {+ mPackageIdentifierParcel = Preconditions.checkNotNull(packageIdentifierParcel); }- /** @exportToFramework:hide */+ /**+ * Returns the {@link PackageIdentifierParcel} holding the values for this+ * {@link PackageIdentifier}.+ *+ * @exportToFramework:hide+ */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @NonNull- public Bundle getBundle() {- return mBundle;+ public PackageIdentifierParcel getPackageIdentifierParcel() {+ return mPackageIdentifierParcel; } @NonNull public String getPackageName() {- return Preconditions.checkNotNull(mBundle.getString(PACKAGE_NAME_FIELD));+ return mPackageIdentifierParcel.getPackageName(); } @NonNull public byte[] getSha256Certificate() {- return Preconditions.checkNotNull(mBundle.getByteArray(SHA256_CERTIFICATE_FIELD));+ return mPackageIdentifierParcel.getSha256Certificate(); } @Override@@ -80,15 +83,15 @@
if (this == obj) { return true; }- if (obj == null || !(obj instanceof PackageIdentifier)) {+ if (!(obj instanceof PackageIdentifier)) { return false; } final PackageIdentifier other = (PackageIdentifier) obj;- return BundleUtil.deepEquals(mBundle, other.mBundle);+ return mPackageIdentifierParcel.equals(other.getPackageIdentifierParcel()); } @Override public int hashCode() {- return BundleUtil.deepHashCode(mBundle);+ return mPackageIdentifierParcel.hashCode(); } }
@@ -19,8 +19,13 @@
import android.annotation.SuppressLint; import androidx.annotation.NonNull;+import androidx.annotation.RestrictTo; import androidx.appsearch.annotation.CanIgnoreReturnValue; import androidx.appsearch.exceptions.AppSearchException;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags;+import androidx.appsearch.usagereporting.TakenAction;+import androidx.collection.ArraySet; import androidx.core.util.Preconditions; import java.util.ArrayList;@@ -28,6 +33,7 @@
import java.util.Collection; import java.util.Collections; import java.util.List;+import java.util.Set; /** * Encapsulates a request to index documents into an {@link AppSearchSession} database.@@ -43,8 +49,11 @@
public final class PutDocumentsRequest { private final List mDocuments;- PutDocumentsRequest(List documents) {+ private final List mTakenActions;++ PutDocumentsRequest(List documents, List takenActions) { mDocuments = documents;+ mTakenActions = takenActions; } /** Returns a list of {@link GenericDocument} objects that are part of this request. */@@ -53,9 +62,27 @@
return Collections.unmodifiableList(mDocuments); }+ /**+ * Returns a list of {@link GenericDocument} objects containing taken action metrics that are+ * part of this request.+ *+ * + *
See {@link Builder#addTakenActions(TakenAction...)}.
+ * + */+ @NonNull+ @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS)+ public List getTakenActionGenericDocuments() {+ return Collections.unmodifiableList(mTakenActions);+ }++ /** Builder for {@link PutDocumentsRequest} objects. */ public static final class Builder { private ArrayList mDocuments = new ArrayList<>();+ private ArrayList mTakenActions = new ArrayList<>(); private boolean mBuilt = false; /** Adds one or more {@link GenericDocument} objects to the request. */@@ -121,18 +148,219 @@
} return addGenericDocuments(genericDocuments); }++ /**+ * Adds one or more {@link TakenAction} objects to the request.+ *+ *
Clients can construct {@link TakenAction} documents to report the user's actions on
+ * search results, and these actions can be used as signals to boost result ranking in+ * future search requests. See {@link TakenAction} for more details.+ *+ *
Clients should report search and click actions together sorted by
+ * {@link TakenAction#getActionTimestampMillis} in chronological order.+ *
For example, if there are 2 search actions, with 1 click action associated with the
+ * first and 2 click actions associated with the second, then clients should report+ * [searchAction1, clickAction1, searchAction2, clickAction2, clickAction3].+ *+ *
Certain anonymized information about actions reported using this API may be uploaded
+ * using statsd and may be used to improve the quality of the search algorithms. Most of+ * the information in this class is already non-identifiable, such as durations and its+ * position in the result set. Identifiable information which you choose to provide, such+ * as the query string, will be anonymized using techniques like Federated Analytics to+ * ensure only the most frequently searched terms across the whole user population are+ * retained and available for study.+ *+ *
You can alternatively use the {@link #addDocuments(Object...)} API with
+ * {@link TakenAction} document to retain the benefits of joining and using it on-device,+ * without triggering any of the anonymized stats uploading described above.+ *+ * @param takenActions one or more {@link TakenAction} objects.+ */+ // Merged list available from getTakenActionGenericDocuments()+ @SuppressWarnings("MissingGetterMatchingBuilder")+ @CanIgnoreReturnValue+ @NonNull+ public Builder addTakenActions(+ @NonNull TakenAction... takenActions) throws AppSearchException {+ Preconditions.checkNotNull(takenActions);+ resetIfBuilt();+ return addTakenActions(Arrays.asList(takenActions));+ }++ /**+ * Adds a collection of {@link TakenAction} objects to the request.+ *+ * @see #addTakenActions(TakenAction...)+ *+ * @param takenActions a collection of {@link TakenAction} objects.+ */+ // Merged list available from getTakenActionGenericDocuments()+ @SuppressWarnings("MissingGetterMatchingBuilder")+ @CanIgnoreReturnValue+ @NonNull+ public Builder addTakenActions(@NonNull Collection extends TakenAction> takenActions)+ throws AppSearchException {+ Preconditions.checkNotNull(takenActions);+ resetIfBuilt();+ List genericDocuments = new ArrayList<>(takenActions.size());+ for (Object takenAction : takenActions) {+ GenericDocument genericDocument = GenericDocument.fromDocumentClass(takenAction);+ genericDocuments.add(genericDocument);+ }+ mTakenActions.addAll(genericDocuments);+ return this;+ } // @exportToFramework:endStrip()- /** Creates a new {@link PutDocumentsRequest} object. */+ /**+ * Adds one or more {@link GenericDocument} objects containing taken action metrics to the+ * request.+ *+ *
It is recommended to use taken action document classes in Jetpack library to construct
+ * taken action documents.+ *+ *
The document creation timestamp of the {@link GenericDocument} should be set to the
+ * actual action timestamp via {@link GenericDocument.Builder#setCreationTimestampMillis}.+ *+ *
Clients should report search and click actions together sorted by
+ * {@link GenericDocument#getCreationTimestampMillis} in chronological order.+ *
For example, if there are 2 search actions, with 1 click action associated with the
+ * first and 2 click actions associated with the second, then clients should report+ * [searchAction1, clickAction1, searchAction2, clickAction2, clickAction3].+ *+ *
Different types of taken actions and metrics to be collected by AppSearch:
+ *
+ * + * Search action+ *
+ *
actionType: LONG, the enum value of the action type.
+ *
Requires to be {@code 1} for search actions.
+ *+ *
query: STRING, the user-entered search input (without any operators or
+ * rewriting).+ *+ *
fetchedResultCount: LONG, the number of {@link SearchResult} documents
+ * fetched from AppSearch in this search action.+ * + * + *+ * + * Click action+ *
+ *
actionType: LONG, the enum value of the action type.
+ *
Requires to be {@code 2} for click actions.
+ *+ *
query: STRING, the user-entered search input (without any operators or
+ * rewriting) that yielded the {@link SearchResult} on which the user took action.+ *+ *
referencedQualifiedId: STRING, the qualified id of the {@link SearchResult}
+ * document that the user takes action on.+ *
A qualified id is a string generated by package, database, namespace, and
resultRankInBlock: LONG, the rank of the {@link SearchResult} document among
+ * the user-defined block.+ *
The client can define its own custom definition for block, for example,
+ * corpus name, group, etc.+ *
For example, a client defines the block as corpus, and AppSearch returns 5
+ * documents with corpus = ["corpus1", "corpus1", "corpus2", "corpus3", "corpus2"].+ * Then the block ranks of them = [1, 2, 1, 1, 2].+ *
If the client is not presenting the results in multiple blocks, they should
+ * set this value to match resultRankGlobal.+ *+ *
resultRankGlobal: LONG, the global rank of the {@link SearchResult}
+ * document.+ *
Global rank reflects the order of {@link SearchResult} documents returned by
+ * AppSearch.+ *
For example, AppSearch returns 2 pages with 10 {@link SearchResult} documents
+ * for each page. Then the global ranks of them will be 1 to 10 for the first page,+ * and 11 to 20 for the second page.+ *+ *
timeStayOnResultMillis: LONG, the time in milliseconds that user stays on
+ * the {@link SearchResult} document after clicking it.+ * + * + * + *+ *
Certain anonymized information about actions reported using this API may be uploaded
+ * using statsd and may be used to improve the quality of the search algorithms. Most of+ * the information in this class is already non-identifiable, such as durations and its+ * position in the result set. Identifiable information which you choose to provide, such+ * as the query string, will be anonymized using techniques like Federated Analytics to+ * ensure only the most frequently searched terms across the whole user population are+ * retained and available for study.+ *+ *
You can alternatively use the {@link #addGenericDocuments(GenericDocument...)} API to
+ * retain the benefits of joining and using it on-device, without triggering any of the+ * anonymized stats uploading described above.+ *+ * @param takenActionGenericDocuments one or more {@link GenericDocument} objects containing+ * taken action metric fields.+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @CanIgnoreReturnValue+ @NonNull+ @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS)+ public Builder addTakenActionGenericDocuments(+ @NonNull GenericDocument... takenActionGenericDocuments) throws AppSearchException {+ Preconditions.checkNotNull(takenActionGenericDocuments);+ resetIfBuilt();+ return addTakenActionGenericDocuments(Arrays.asList(takenActionGenericDocuments));+ }++ /**+ * Adds a collection of {@link GenericDocument} objects containing taken action metrics to+ * the request.+ *+ * @see #addTakenActionGenericDocuments(GenericDocument...)+ *+ * @param takenActionGenericDocuments a collection of {@link GenericDocument} objects+ * containing taken action metric fields.+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @CanIgnoreReturnValue+ @NonNull+ @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS)+ public Builder addTakenActionGenericDocuments(@NonNull Collection+ extends GenericDocument> takenActionGenericDocuments) throws AppSearchException {+ Preconditions.checkNotNull(takenActionGenericDocuments);+ resetIfBuilt();+ mTakenActions.addAll(takenActionGenericDocuments);+ return this;+ }++ /**+ * Creates a new {@link PutDocumentsRequest} object.+ *+ * @throws IllegalArgumentException if there is any id collision between normal and action+ * documents.+ */ @NonNull public PutDocumentsRequest build() { mBuilt = true;- return new PutDocumentsRequest(mDocuments);++ // Verify there is no id collision between normal documents and action documents.+ Set idSet = new ArraySet<>();+ for (int i = 0; i < mDocuments.size(); i++) {+ idSet.add(mDocuments.get(i).getId());+ }+ for (int i = 0; i < mTakenActions.size(); i++) {+ GenericDocument takenAction = mTakenActions.get(i);+ if (idSet.contains(takenAction.getId())) {+ throw new IllegalArgumentException("Document id " + takenAction.getId()+ + " cannot exist in both taken action and normal document");+ }+ }++ return new PutDocumentsRequest(mDocuments, mTakenActions); } private void resetIfBuilt() { if (mBuilt) { mDocuments = new ArrayList<>(mDocuments);+ mTakenActions = new ArrayList<>(mTakenActions); mBuilt = false; } }
@@ -0,0 +1,294 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.app;++import android.os.Parcel;+import android.os.Parcelable;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RestrictTo;+import androidx.appsearch.annotation.CanIgnoreReturnValue;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags;+import androidx.appsearch.safeparcel.AbstractSafeParcelable;+import androidx.appsearch.safeparcel.PackageIdentifierParcel;+import androidx.appsearch.safeparcel.SafeParcelable;+import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityConfigCreator;+import androidx.collection.ArraySet;++import java.util.ArrayList;+import java.util.List;+import java.util.Objects;+import java.util.Set;++/**+ * A class to hold a all necessary Visibility information corresponding to the same schema. This+ * pattern allows for easier association of these documents.+ *+ *
This does not correspond to any schema, the properties held in this class are kept in two
+ * separate schemas, VisibilityConfig and PublicAclOverlay.+ */+@FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)[email protected](creator = "VisibilityConfigCreator")+@SuppressWarnings("HiddenSuperclass")+public final class SchemaVisibilityConfig extends AbstractSafeParcelable {+ @NonNull+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ public static final Parcelable.Creator CREATOR =+ new VisibilityConfigCreator();++ @NonNull+ @Field(id = 1)+ final List mAllowedPackages;++ @NonNull+ @Field(id = 2)+ final List mRequiredPermissions;++ @Nullable+ @Field(id = 3)+ final PackageIdentifierParcel mPubliclyVisibleTargetPackage;++ @Nullable private Integer mHashCode;+ @Nullable private List mAllowedPackagesCached;+ @Nullable private Set> mRequiredPermissionsCached;++ @Constructor+ SchemaVisibilityConfig(+ @Param(id = 1) @NonNull List allowedPackages,+ @Param(id = 2) @NonNull List requiredPermissions,+ @Param(id = 3) @Nullable PackageIdentifierParcel publiclyVisibleTargetPackage) {+ mAllowedPackages = Objects.requireNonNull(allowedPackages);+ mRequiredPermissions = Objects.requireNonNull(requiredPermissions);+ mPubliclyVisibleTargetPackage = publiclyVisibleTargetPackage;+ }++ /** Returns a list of {@link PackageIdentifier}s of packages that can access this schema. */+ @NonNull+ public List getAllowedPackages() {+ if (mAllowedPackagesCached == null) {+ mAllowedPackagesCached = new ArrayList<>(mAllowedPackages.size());+ for (int i = 0; i < mAllowedPackages.size(); i++) {+ mAllowedPackagesCached.add(new PackageIdentifier(mAllowedPackages.get(i)));+ }+ }+ return mAllowedPackagesCached;+ }++ /**+ * Returns an array of Integers representing Android Permissions that the caller must hold to+ * access the schema this {@link SchemaVisibilityConfig} represents.+ * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set)+ */+ @NonNull+ public Set> getRequiredPermissions() {+ if (mRequiredPermissionsCached == null) {+ mRequiredPermissionsCached = new ArraySet<>(mRequiredPermissions.size());+ for (int i = 0; i < mRequiredPermissions.size(); i++) {+ VisibilityPermissionConfig permissionConfig = mRequiredPermissions.get(i);+ Set requiredPermissions = permissionConfig.getAllRequiredPermissions();+ if (mRequiredPermissionsCached != null && requiredPermissions != null) {+ mRequiredPermissionsCached.add(requiredPermissions);+ }+ }+ }+ // Added for nullness checker as it is @Nullable, we initialize it above if it is null.+ return Objects.requireNonNull(mRequiredPermissionsCached);+ }++ /**+ * Returns the {@link PackageIdentifier} of the package that will be used as the target package+ * in a call to {@link android.content.pm.PackageManager#canPackageQuery} to determine which+ * packages can access this publicly visible schema. Returns null if the schema is not publicly+ * visible.+ */+ @Nullable+ public PackageIdentifier getPubliclyVisibleTargetPackage() {+ if (mPubliclyVisibleTargetPackage == null) {+ return null;+ }+ return new PackageIdentifier(mPubliclyVisibleTargetPackage);+ }++ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @Override+ public void writeToParcel(@NonNull Parcel dest, int flags) {+ VisibilityConfigCreator.writeToParcel(this, dest, flags);+ }++ @Override+ public boolean equals(@Nullable Object o) {+ if (this == o) {+ return true;+ }+ if (o == null) {+ return false;+ }+ if (!(o instanceof SchemaVisibilityConfig)) {+ return false;+ }+ SchemaVisibilityConfig that = (SchemaVisibilityConfig) o;+ return Objects.equals(mAllowedPackages, that.mAllowedPackages)+ && Objects.equals(mRequiredPermissions, that.mRequiredPermissions)+ && Objects.equals(+ mPubliclyVisibleTargetPackage, that.mPubliclyVisibleTargetPackage);+ }++ @Override+ public int hashCode() {+ if (mHashCode == null) {+ mHashCode = Objects.hash(+ mAllowedPackages,+ mRequiredPermissions,+ mPubliclyVisibleTargetPackage);+ }+ return mHashCode;+ }++ /** The builder class of {@link SchemaVisibilityConfig}. */+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)+ public static final class Builder {+ private List mAllowedPackages = new ArrayList<>();+ private List mRequiredPermissions = new ArrayList<>();+ @Nullable private PackageIdentifierParcel mPubliclyVisibleTargetPackage;+ private boolean mBuilt;++ /** Creates a {@link Builder} for a {@link SchemaVisibilityConfig}. */+ public Builder() {}++ /**+ * Creates a {@link Builder} copying the values from an existing+ * {@link SchemaVisibilityConfig}.+ *+ * @exportToFramework:hide+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ public Builder(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) {+ Objects.requireNonNull(schemaVisibilityConfig);+ mAllowedPackages = new ArrayList<>(schemaVisibilityConfig.mAllowedPackages);+ mRequiredPermissions = new ArrayList<>(schemaVisibilityConfig.mRequiredPermissions);+ mPubliclyVisibleTargetPackage = schemaVisibilityConfig.mPubliclyVisibleTargetPackage;+ }++ /** Add {@link PackageIdentifier} of packages which has access to this schema. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder addAllowedPackage(@NonNull PackageIdentifier packageIdentifier) {+ Objects.requireNonNull(packageIdentifier);+ resetIfBuilt();+ mAllowedPackages.add(packageIdentifier.getPackageIdentifierParcel());+ return this;+ }++ /** Clears the list of packages which have access to this schema. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder clearAllowedPackages() {+ resetIfBuilt();+ mAllowedPackages.clear();+ return this;+ }++ /**+ * Adds a set of required Android {@link android.Manifest.permission} combination a+ * package needs to hold to access the schema this {@link SchemaVisibilityConfig}+ * represents.+ *+ *
If the querier holds ALL of the required permissions in this combination, they will
+ * have access to read {@link GenericDocument} objects of the given schema type.+ *+ *
You can call this method repeatedly to add multiple permission combinations, and the
+ * querier will have access if they holds ANY of the combinations.+ *+ *
Merged Set available from {@link #getRequiredPermissions()}.
+ *+ * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility for+ * supported Permissions.+ */+ @SuppressWarnings("RequiresPermission") // No permission required to call this method+ @CanIgnoreReturnValue+ @NonNull+ public Builder addRequiredPermissions(@NonNull Set visibleToPermissions) {+ Objects.requireNonNull(visibleToPermissions);+ resetIfBuilt();+ mRequiredPermissions.add(new VisibilityPermissionConfig(visibleToPermissions));+ return this;+ }++ /**+ * Clears all required permissions combinations set to this {@link SchemaVisibilityConfig}.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder clearRequiredPermissions() {+ resetIfBuilt();+ mRequiredPermissions.clear();+ return this;+ }++ /**+ * Specify that this schema should be publicly available, to the same packages that have+ * visibility to the package passed as a parameter. This visibility is determined by the+ * result of {@link android.content.pm.PackageManager#canPackageQuery}.+ *+ *
It is possible for the packageIdentifier parameter to be different from the
+ * package performing the indexing. This might happen in the case of an on-device indexer+ * processing information about various packages. The visibility will be the same+ * regardless of which package indexes the document, as the visibility is based on the+ * packageIdentifier parameter.+ *+ *
Calling this with packageIdentifier set to null is valid, and will remove public
+ * visibility for the schema.+ *+ * @param packageIdentifier the {@link PackageIdentifier} of the package that will be used+ * as the target package in a call to {@link+ * android.content.pm.PackageManager#canPackageQuery} to determine+ * which packages can access this publicly visible schema.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setPubliclyVisibleTargetPackage(+ @Nullable PackageIdentifier packageIdentifier) {+ resetIfBuilt();+ if (packageIdentifier == null) {+ mPubliclyVisibleTargetPackage = null;+ } else {+ mPubliclyVisibleTargetPackage = packageIdentifier.getPackageIdentifierParcel();+ }+ return this;+ }++ private void resetIfBuilt() {+ if (mBuilt) {+ mAllowedPackages = new ArrayList<>(mAllowedPackages);+ mRequiredPermissions = new ArrayList<>(mRequiredPermissions);+ mBuilt = false;+ }+ }++ /** Build a {@link SchemaVisibilityConfig} */+ @NonNull+ public SchemaVisibilityConfig build() {+ mBuilt = true;+ return new SchemaVisibilityConfig(+ mAllowedPackages,+ mRequiredPermissions,+ mPubliclyVisibleTargetPackage);+ }+ }+}
@@ -18,6 +18,8 @@
import android.annotation.SuppressLint; import android.os.Bundle;+import android.os.Parcel;+import android.os.Parcelable; import androidx.annotation.IntDef; import androidx.annotation.IntRange;@@ -28,6 +30,11 @@
import androidx.appsearch.annotation.CanIgnoreReturnValue; import androidx.appsearch.annotation.Document; import androidx.appsearch.exceptions.AppSearchException;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags;+import androidx.appsearch.safeparcel.AbstractSafeParcelable;+import androidx.appsearch.safeparcel.SafeParcelable;+import androidx.appsearch.safeparcel.stub.StubCreators.SearchSpecCreator; import androidx.appsearch.util.BundleUtil; import androidx.collection.ArrayMap; import androidx.collection.ArraySet;@@ -41,49 +48,119 @@
import java.util.Collections; import java.util.List; import java.util.Map;+import java.util.Objects; import java.util.Set; /** * This class represents the specification logic for AppSearch. It can be used to set the type of * search, like prefix or exact only or apply filters to search for a specific schema type only etc. */-public final class SearchSpec {[email protected](creator = "SearchSpecCreator")+@SuppressWarnings("HiddenSuperclass")+public final class SearchSpec extends AbstractSafeParcelable {++ /** Creator class for {@link SearchSpec}. */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @NonNull+ public static final Parcelable.Creator CREATOR =+ new SearchSpecCreator();+ /** * Schema type to be used in {@link SearchSpec.Builder#addProjection} to apply * property paths to all results, excepting any types that have had their own, specific * property paths set.+ *+ * @deprecated use {@link #SCHEMA_TYPE_WILDCARD} instead. */+ @Deprecated public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*"; /** * Schema type to be used in {@link SearchSpec.Builder#addFilterProperties(String, Collection)}- * to apply property paths to all results, excepting any types that have had their own, specific- * property paths set.- * @exportToFramework:hide+ * and {@link SearchSpec.Builder#addProjection} to apply property paths to all results,+ * excepting any types that have had their own, specific property paths set. */- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) public static final String SCHEMA_TYPE_WILDCARD = "*";- static final String TERM_MATCH_TYPE_FIELD = "termMatchType";- static final String SCHEMA_FIELD = "schema";- static final String NAMESPACE_FIELD = "namespace";- static final String PROPERTY_FIELD = "property";- static final String PACKAGE_NAME_FIELD = "packageName";- static final String NUM_PER_PAGE_FIELD = "numPerPage";- static final String RANKING_STRATEGY_FIELD = "rankingStrategy";- static final String ORDER_FIELD = "order";- static final String SNIPPET_COUNT_FIELD = "snippetCount";- static final String SNIPPET_COUNT_PER_PROPERTY_FIELD = "snippetCountPerProperty";- static final String MAX_SNIPPET_FIELD = "maxSnippet";- static final String PROJECTION_TYPE_PROPERTY_PATHS_FIELD = "projectionTypeFieldMasks";- static final String RESULT_GROUPING_TYPE_FLAGS = "resultGroupingTypeFlags";- static final String RESULT_GROUPING_LIMIT = "resultGroupingLimit";- static final String TYPE_PROPERTY_WEIGHTS_FIELD = "typePropertyWeightsField";- static final String JOIN_SPEC = "joinSpec";- static final String ADVANCED_RANKING_EXPRESSION = "advancedRankingExpression";- static final String ENABLED_FEATURES_FIELD = "enabledFeatures";+ @Field(id = 1, getter = "getTermMatch")+ private final int mTermMatchType;- /** @exportToFramework:hide */+ @Field(id = 2, getter = "getFilterSchemas")+ private final List mSchemas;++ @Field(id = 3, getter = "getFilterNamespaces")+ private final List mNamespaces;++ @Field(id = 4)+ final Bundle mTypePropertyFilters;++ @Field(id = 5, getter = "getFilterPackageNames")+ private final List mPackageNames;++ @Field(id = 6, getter = "getResultCountPerPage")+ private final int mResultCountPerPage;++ @Field(id = 7, getter = "getRankingStrategy")+ @RankingStrategy+ private final int mRankingStrategy;++ @Field(id = 8, getter = "getOrder")+ @Order+ private final int mOrder;++ @Field(id = 9, getter = "getSnippetCount")+ private final int mSnippetCount;++ @Field(id = 10, getter = "getSnippetCountPerProperty")+ private final int mSnippetCountPerProperty;++ @Field(id = 11, getter = "getMaxSnippetSize")+ private final int mMaxSnippetSize;++ @Field(id = 12)+ final Bundle mProjectionTypePropertyMasks;++ @Field(id = 13, getter = "getResultGroupingTypeFlags")+ @GroupingType+ private final int mResultGroupingTypeFlags;++ @Field(id = 14, getter = "getResultGroupingLimit")+ private final int mGroupingLimit;++ @Field(id = 15)+ final Bundle mTypePropertyWeightsField;++ @Nullable+ @Field(id = 16, getter = "getJoinSpec")+ private final JoinSpec mJoinSpec;++ @Field(id = 17, getter = "getAdvancedRankingExpression")+ private final String mAdvancedRankingExpression;++ @Field(id = 18, getter = "getEnabledFeatures")+ private final List mEnabledFeatures;++ @Field(id = 19, getter = "getSearchSourceLogTag")+ @Nullable private final String mSearchSourceLogTag;++ @NonNull+ @Field(id = 20, getter = "getSearchEmbeddings")+ private final List mSearchEmbeddings;++ @Field(id = 21, getter = "getDefaultEmbeddingSearchMetricType")+ private final int mDefaultEmbeddingSearchMetricType;++ @NonNull+ @Field(id = 22, getter = "getInformationalRankingExpressions")+ private final List mInformationalRankingExpressions;++ /**+ * Default number of documents per page.+ *+ * @exportToFramework:hide+ */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static final int DEFAULT_NUM_PER_PAGE = 10;@@ -224,43 +301,112 @@
/** * Results should be grouped together by schema type for the purpose of enforcing a limit on the * number of results returned per schema type.- *- * */- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA)- // @exportToFramework:endStrip()+ @FlaggedApi(Flags.FLAG_ENABLE_GROUPING_TYPE_PER_SCHEMA) public static final int GROUPING_TYPE_PER_SCHEMA = 1 << 2;- private final Bundle mBundle;-- /** @exportToFramework:hide */- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- public SearchSpec(@NonNull Bundle bundle) {- Preconditions.checkNotNull(bundle);- mBundle = bundle;- }- /**- * Returns the {@link Bundle} populated by this builder.+ * Type of scoring used to calculate similarity for embedding vectors. For details of each, see+ * comments above each value. * * @exportToFramework:hide */- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- @NonNull- public Bundle getBundle() {- return mBundle;+ // NOTE: The integer values of these constants must match the proto enum constants in+ // {@link SearchSpecProto.EmbeddingQueryMetricType.Code}+ @RestrictTo(RestrictTo.Scope.LIBRARY)+ @IntDef(value = {+ EMBEDDING_SEARCH_METRIC_TYPE_COSINE,+ EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT,+ EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN,+ })+ @Retention(RetentionPolicy.SOURCE)+ public @interface EmbeddingSearchMetricType { }+ /**+ * Cosine similarity as metric for embedding search and ranking.+ */+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public static final int EMBEDDING_SEARCH_METRIC_TYPE_COSINE = 1;+ /**+ * Dot product similarity as metric for embedding search and ranking.+ */+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public static final int EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT = 2;+ /**+ * Euclidean distance as metric for embedding search and ranking.+ */+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public static final int EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN = 3;+++ @Constructor+ SearchSpec(+ @Param(id = 1) int termMatchType,+ @Param(id = 2) @NonNull List schemas,+ @Param(id = 3) @NonNull List namespaces,+ @Param(id = 4) @NonNull Bundle properties,+ @Param(id = 5) @NonNull List packageNames,+ @Param(id = 6) int resultCountPerPage,+ @Param(id = 7) @RankingStrategy int rankingStrategy,+ @Param(id = 8) @Order int order,+ @Param(id = 9) int snippetCount,+ @Param(id = 10) int snippetCountPerProperty,+ @Param(id = 11) int maxSnippetSize,+ @Param(id = 12) @NonNull Bundle projectionTypePropertyMasks,+ @Param(id = 13) int resultGroupingTypeFlags,+ @Param(id = 14) int groupingLimit,+ @Param(id = 15) @NonNull Bundle typePropertyWeightsField,+ @Param(id = 16) @Nullable JoinSpec joinSpec,+ @Param(id = 17) @NonNull String advancedRankingExpression,+ @Param(id = 18) @NonNull List enabledFeatures,+ @Param(id = 19) @Nullable String searchSourceLogTag,+ @Param(id = 20) @Nullable List searchEmbeddings,+ @Param(id = 21) int defaultEmbeddingSearchMetricType,+ @Param(id = 22) @Nullable List informationalRankingExpressions+ ) {+ mTermMatchType = termMatchType;+ mSchemas = Collections.unmodifiableList(Preconditions.checkNotNull(schemas));+ mNamespaces = Collections.unmodifiableList(Preconditions.checkNotNull(namespaces));+ mTypePropertyFilters = Preconditions.checkNotNull(properties);+ mPackageNames = Collections.unmodifiableList(Preconditions.checkNotNull(packageNames));+ mResultCountPerPage = resultCountPerPage;+ mRankingStrategy = rankingStrategy;+ mOrder = order;+ mSnippetCount = snippetCount;+ mSnippetCountPerProperty = snippetCountPerProperty;+ mMaxSnippetSize = maxSnippetSize;+ mProjectionTypePropertyMasks = Preconditions.checkNotNull(projectionTypePropertyMasks);+ mResultGroupingTypeFlags = resultGroupingTypeFlags;+ mGroupingLimit = groupingLimit;+ mTypePropertyWeightsField = Preconditions.checkNotNull(typePropertyWeightsField);+ mJoinSpec = joinSpec;+ mAdvancedRankingExpression = Preconditions.checkNotNull(advancedRankingExpression);+ mEnabledFeatures = Collections.unmodifiableList(+ Preconditions.checkNotNull(enabledFeatures));+ mSearchSourceLogTag = searchSourceLogTag;+ if (searchEmbeddings != null) {+ mSearchEmbeddings = Collections.unmodifiableList(searchEmbeddings);+ } else {+ mSearchEmbeddings = Collections.emptyList();+ }+ mDefaultEmbeddingSearchMetricType = defaultEmbeddingSearchMetricType;+ if (informationalRankingExpressions != null) {+ mInformationalRankingExpressions = Collections.unmodifiableList(+ informationalRankingExpressions);+ } else {+ mInformationalRankingExpressions = Collections.emptyList();+ }+ }++ /** Returns how the query terms should match terms in the index. */ @TermMatch public int getTermMatch() {- return mBundle.getInt(TERM_MATCH_TYPE_FIELD, -1);+ return mTermMatchType; } /**@@ -270,11 +416,10 @@
*/ @NonNull public List getFilterSchemas() {- List schemas = mBundle.getStringArrayList(SCHEMA_FIELD);- if (schemas == null) {+ if (mSchemas == null) { return Collections.emptyList(); }- return Collections.unmodifiableList(schemas);+ return mSchemas; } /**@@ -284,19 +429,15 @@
* *
Calling this function repeatedly is inefficient. Prefer to retain the Map returned
* by this function, rather than calling it multiple times.- *- * @exportToFramework:hide */ @NonNull- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) public Map> getFilterProperties() {- Bundle typePropertyPathsBundle = Preconditions.checkNotNull(- mBundle.getBundle(PROPERTY_FIELD));- Set schemas = typePropertyPathsBundle.keySet();+ Set schemas = mTypePropertyFilters.keySet(); Map> typePropertyPathsMap = new ArrayMap<>(schemas.size()); for (String schema : schemas) { typePropertyPathsMap.put(schema, Preconditions.checkNotNull(- typePropertyPathsBundle.getStringArrayList(schema)));+ mTypePropertyFilters.getStringArrayList(schema))); } return typePropertyPathsMap; }@@ -308,11 +449,10 @@
*/ @NonNull public List getFilterNamespaces() {- List namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);- if (namespaces == null) {+ if (mNamespaces == null) { return Collections.emptyList(); }- return Collections.unmodifiableList(namespaces);+ return mNamespaces; } /**@@ -324,45 +464,44 @@
*/ @NonNull public List getFilterPackageNames() {- List packageNames = mBundle.getStringArrayList(PACKAGE_NAME_FIELD);- if (packageNames == null) {+ if (mPackageNames == null) { return Collections.emptyList(); }- return Collections.unmodifiableList(packageNames);+ return mPackageNames; } /** Returns the number of results per page in the result set. */ public int getResultCountPerPage() {- return mBundle.getInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE);+ return mResultCountPerPage; } /** Returns the ranking strategy. */ @RankingStrategy public int getRankingStrategy() {- return mBundle.getInt(RANKING_STRATEGY_FIELD);+ return mRankingStrategy; } /** Returns the order of returned search results (descending or ascending). */ @Order public int getOrder() {- return mBundle.getInt(ORDER_FIELD);+ return mOrder; } /** Returns how many documents to generate snippets for. */ public int getSnippetCount() {- return mBundle.getInt(SNIPPET_COUNT_FIELD);+ return mSnippetCount; } /** * Returns how many matches for each property of a matching document to generate snippets for. */ public int getSnippetCountPerProperty() {- return mBundle.getInt(SNIPPET_COUNT_PER_PROPERTY_FIELD);+ return mSnippetCountPerProperty; } /** Returns the maximum size of a snippet in characters. */ public int getMaxSnippetSize() {- return mBundle.getInt(MAX_SNIPPET_FIELD);+ return mMaxSnippetSize; } /**@@ -377,13 +516,12 @@
*/ @NonNull public Map> getProjections() {- Bundle typePropertyPathsBundle = Preconditions.checkNotNull(- mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD));- Set schemas = typePropertyPathsBundle.keySet();+ Set schemas = mProjectionTypePropertyMasks.keySet(); Map> typePropertyPathsMap = new ArrayMap<>(schemas.size()); for (String schema : schemas) {- typePropertyPathsMap.put(schema, Preconditions.checkNotNull(- typePropertyPathsBundle.getStringArrayList(schema)));+ typePropertyPathsMap.put(schema,+ Objects.requireNonNull(+ mProjectionTypePropertyMasks.getStringArrayList(schema))); } return typePropertyPathsMap; }@@ -400,16 +538,19 @@
*/ @NonNull public Map> getProjectionPaths() {- Bundle typePropertyPathsBundle = mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD);- Set schemas = typePropertyPathsBundle.keySet();+ Set schemas = mProjectionTypePropertyMasks.keySet(); Map> typePropertyPathsMap = new ArrayMap<>(schemas.size()); for (String schema : schemas) {- ArrayList propertyPathList = typePropertyPathsBundle.getStringArrayList(schema);- List copy = new ArrayList<>(propertyPathList.size());- for (String p: propertyPathList) {- copy.add(new PropertyPath(p));+ ArrayList propertyPathList = mProjectionTypePropertyMasks.getStringArrayList(+ schema);+ if (propertyPathList != null) {+ List copy = new ArrayList<>(propertyPathList.size());+ for (int i = 0; i < propertyPathList.size(); i++) {+ String p = propertyPathList.get(i);+ copy.add(new PropertyPath(p));+ }+ typePropertyPathsMap.put(schema, copy); }- typePropertyPathsMap.put(schema, copy); } return typePropertyPathsMap; }@@ -425,18 +566,20 @@
*/ @NonNull public Map> getPropertyWeights() {- Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD);- Set schemaTypes = typePropertyWeightsBundle.keySet();+ Set schemaTypes = mTypePropertyWeightsField.keySet(); Map> typePropertyWeightsMap = new ArrayMap<>( schemaTypes.size()); for (String schemaType : schemaTypes) {- Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType);- Set propertyPaths = propertyPathBundle.keySet();- Map propertyPathWeights = new ArrayMap<>(propertyPaths.size());- for (String propertyPath : propertyPaths) {- propertyPathWeights.put(propertyPath, propertyPathBundle.getDouble(propertyPath));+ Bundle propertyPathBundle = mTypePropertyWeightsField.getBundle(schemaType);+ if (propertyPathBundle != null) {+ Set propertyPaths = propertyPathBundle.keySet();+ Map propertyPathWeights = new ArrayMap<>(propertyPaths.size());+ for (String propertyPath : propertyPaths) {+ propertyPathWeights.put(propertyPath,+ propertyPathBundle.getDouble(propertyPath));+ }+ typePropertyWeightsMap.put(schemaType, propertyPathWeights); }- typePropertyWeightsMap.put(schemaType, propertyPathWeights); } return typePropertyWeightsMap; }@@ -452,19 +595,22 @@
*/ @NonNull public Map> getPropertyWeightPaths() {- Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD);- Set schemaTypes = typePropertyWeightsBundle.keySet();+ Set schemaTypes = mTypePropertyWeightsField.keySet(); Map> typePropertyWeightsMap = new ArrayMap<>( schemaTypes.size()); for (String schemaType : schemaTypes) {- Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType);- Set propertyPaths = propertyPathBundle.keySet();- Map propertyPathWeights = new ArrayMap<>(propertyPaths.size());- for (String propertyPath : propertyPaths) {- propertyPathWeights.put(new PropertyPath(propertyPath),- propertyPathBundle.getDouble(propertyPath));+ Bundle propertyPathBundle = mTypePropertyWeightsField.getBundle(schemaType);+ if (propertyPathBundle != null) {+ Set propertyPaths = propertyPathBundle.keySet();+ Map propertyPathWeights =+ new ArrayMap<>(propertyPaths.size());+ for (String propertyPath : propertyPaths) {+ propertyPathWeights.put(+ new PropertyPath(propertyPath),+ propertyPathBundle.getDouble(propertyPath));+ }+ typePropertyWeightsMap.put(schemaType, propertyPathWeights); }- typePropertyWeightsMap.put(schemaType, propertyPathWeights); } return typePropertyWeightsMap; }@@ -475,7 +621,7 @@
*/ @GroupingType public int getResultGroupingTypeFlags() {- return mBundle.getInt(RESULT_GROUPING_TYPE_FLAGS);+ return mResultGroupingTypeFlags; } /**@@ -485,7 +631,7 @@
* {@link Builder#setResultGrouping(int, int)} was not called. */ public int getResultGroupingLimit() {- return mBundle.getInt(RESULT_GROUPING_LIMIT, Integer.MAX_VALUE);+ return mGroupingLimit; } /**@@ -493,11 +639,7 @@
*/ @Nullable public JoinSpec getJoinSpec() {- Bundle joinSpec = mBundle.getBundle(JOIN_SPEC);- if (joinSpec == null) {- return null;- }- return new JoinSpec(joinSpec);+ return mJoinSpec; } /**@@ -506,28 +648,104 @@
*/ @NonNull public String getAdvancedRankingExpression() {- return mBundle.getString(ADVANCED_RANKING_EXPRESSION, "");+ return mAdvancedRankingExpression;+ }+++ /**+ * Gets a tag to indicate the source of this search, or {@code null} if+ * {@link Builder#setSearchSourceLogTag(String)} was not called.+ *+ *
Some AppSearch implementations may log a hash of this tag using statsd. This tag may be
+ * used for tracing performance issues and crashes to a component of an app.+ *+ *
Call {@link Builder#setSearchSourceLogTag} and give a unique value if you want to
+ * distinguish this search scenario with other search scenarios during performance analysis.+ *+ *
Under no circumstances will AppSearch log the raw String value using statsd, but it
+ * will be provided as-is to custom {@code AppSearchLogger} implementations you have+ * registered in your app.+ */+ @Nullable+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG)+ public String getSearchSourceLogTag() {+ return mSearchSourceLogTag;+ }++ /**+ * Returns the list of {@link EmbeddingVector} for embedding search.+ */+ @NonNull+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public List getSearchEmbeddings() {+ return mSearchEmbeddings;+ }++ /**+ * Returns the default embedding metric type used for embedding search+ * (see {@link AppSearchSession#search}) and ranking+ * (see {@link SearchSpec.Builder#setRankingStrategy(String)}).+ */+ @EmbeddingSearchMetricType+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public int getDefaultEmbeddingSearchMetricType() {+ return mDefaultEmbeddingSearchMetricType;+ }++ /**+ * Returns the informational ranking expressions.+ *+ * @see Builder#addInformationalRankingExpressions+ */+ @NonNull+ @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)+ public List getInformationalRankingExpressions() {+ return mInformationalRankingExpressions; } /** * Returns whether the NUMERIC_SEARCH feature is enabled. */ public boolean isNumericSearchEnabled() {- return getEnabledFeatures().contains(FeatureConstants.NUMERIC_SEARCH);+ return mEnabledFeatures.contains(FeatureConstants.NUMERIC_SEARCH); } /** * Returns whether the VERBATIM_SEARCH feature is enabled. */ public boolean isVerbatimSearchEnabled() {- return getEnabledFeatures().contains(FeatureConstants.VERBATIM_SEARCH);+ return mEnabledFeatures.contains(FeatureConstants.VERBATIM_SEARCH); } /** * Returns whether the LIST_FILTER_QUERY_LANGUAGE feature is enabled. */ public boolean isListFilterQueryLanguageEnabled() {- return getEnabledFeatures().contains(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE);+ return mEnabledFeatures.contains(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE);+ }++ /**+ * Returns whether the LIST_FILTER_HAS_PROPERTY_FUNCTION feature is enabled.+ */+ @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION)+ public boolean isListFilterHasPropertyFunctionEnabled() {+ return mEnabledFeatures.contains(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION);+ }++ /**+ * Returns whether the embedding search feature is enabled.+ */+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public boolean isEmbeddingSearchEnabled() {+ return mEnabledFeatures.contains(FeatureConstants.EMBEDDING_SEARCH);+ }++ /**+ * Returns whether the LIST_FILTER_TOKENIZE_FUNCTION feature is enabled.+ */+ @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)+ public boolean isListFilterTokenizeFunctionEnabled() {+ return mEnabledFeatures.contains(FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION); } /**@@ -539,21 +757,31 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @NonNull public List getEnabledFeatures() {- return mBundle.getStringArrayList(ENABLED_FEATURES_FIELD);+ return mEnabledFeatures;+ }++ @Override+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ public void writeToParcel(@NonNull Parcel dest, int flags) {+ SearchSpecCreator.writeToParcel(this, dest, flags); } /** Builder for {@link SearchSpec objects}. */ public static final class Builder {- private ArrayList mSchemas = new ArrayList<>();- private ArrayList mNamespaces = new ArrayList<>();+ private List mSchemas = new ArrayList<>();+ private List mNamespaces = new ArrayList<>(); private Bundle mTypePropertyFilters = new Bundle();- private ArrayList mPackageNames = new ArrayList<>();+ private List mPackageNames = new ArrayList<>(); private ArraySet mEnabledFeatures = new ArraySet<>(); private Bundle mProjectionTypePropertyMasks = new Bundle(); private Bundle mTypePropertyWeights = new Bundle();+ private List mSearchEmbeddings = new ArrayList<>(); private int mResultCountPerPage = DEFAULT_NUM_PER_PAGE; @TermMatch private int mTermMatchType = TERM_MATCH_PREFIX;+ @EmbeddingSearchMetricType+ private int mDefaultEmbeddingSearchMetricType = EMBEDDING_SEARCH_METRIC_TYPE_COSINE; private int mSnippetCount = 0; private int mSnippetCountPerProperty = MAX_SNIPPET_PER_PROPERTY_COUNT; private int mMaxSnippetSize = 0;@@ -561,10 +789,53 @@
@Order private int mOrder = ORDER_DESCENDING; @GroupingType private int mGroupingTypeFlags = 0; private int mGroupingLimit = 0;- private JoinSpec mJoinSpec;+ @Nullable private JoinSpec mJoinSpec; private String mAdvancedRankingExpression = "";+ private List mInformationalRankingExpressions = new ArrayList<>();+ @Nullable private String mSearchSourceLogTag; private boolean mBuilt = false;+ /** Constructs a new builder for {@link SearchSpec} objects. */+ public Builder() {+ }++ /** @exportToFramework:hide */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ public Builder(@NonNull SearchSpec searchSpec) {+ Objects.requireNonNull(searchSpec);+ mSchemas = new ArrayList<>(searchSpec.getFilterSchemas());+ mNamespaces = new ArrayList<>(searchSpec.getFilterNamespaces());+ for (Map.Entry> entry :+ searchSpec.getFilterProperties().entrySet()) {+ addFilterProperties(entry.getKey(), entry.getValue());+ }+ mPackageNames = new ArrayList<>(searchSpec.getFilterPackageNames());+ mEnabledFeatures = new ArraySet<>(searchSpec.getEnabledFeatures());+ for (Map.Entry> entry : searchSpec.getProjections().entrySet()) {+ addProjection(entry.getKey(), entry.getValue());+ }+ for (Map.Entry> entry :+ searchSpec.getPropertyWeights().entrySet()) {+ setPropertyWeights(entry.getKey(), entry.getValue());+ }+ mSearchEmbeddings = new ArrayList<>(searchSpec.getSearchEmbeddings());+ mResultCountPerPage = searchSpec.getResultCountPerPage();+ mTermMatchType = searchSpec.getTermMatch();+ mDefaultEmbeddingSearchMetricType = searchSpec.getDefaultEmbeddingSearchMetricType();+ mSnippetCount = searchSpec.getSnippetCount();+ mSnippetCountPerProperty = searchSpec.getSnippetCountPerProperty();+ mMaxSnippetSize = searchSpec.getMaxSnippetSize();+ mRankingStrategy = searchSpec.getRankingStrategy();+ mOrder = searchSpec.getOrder();+ mGroupingTypeFlags = searchSpec.getResultGroupingTypeFlags();+ mGroupingLimit = searchSpec.getResultGroupingLimit();+ mJoinSpec = searchSpec.getJoinSpec();+ mAdvancedRankingExpression = searchSpec.getAdvancedRankingExpression();+ mInformationalRankingExpressions = new ArrayList<>(+ searchSpec.getInformationalRankingExpressions());+ mSearchSourceLogTag = searchSpec.getSearchSourceLogTag();+ }+ /** * Sets how the query terms should match {@code TermMatchCode} in the index. *@@ -626,12 +897,13 @@
@SuppressLint("MissingGetterMatchingBuilder") @NonNull public Builder addFilterDocumentClasses(- @NonNull Collection extends Class>> documentClasses) throws AppSearchException {+ @NonNull Collection extends java.lang.Class>> documentClasses)+ throws AppSearchException { Preconditions.checkNotNull(documentClasses); resetIfBuilt(); List schemas = new ArrayList<>(documentClasses.size()); DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();- for (Class> documentClass : documentClasses) {+ for (java.lang.Class> documentClass : documentClasses) { DocumentClassFactory> factory = registry.getOrCreateFactory(documentClass); schemas.add(factory.getSchemaName()); }@@ -655,7 +927,7 @@
@CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") @NonNull- public Builder addFilterDocumentClasses(@NonNull Class>... documentClasses)+ public Builder addFilterDocumentClasses(@NonNull java.lang.Class>... documentClasses) throws AppSearchException { Preconditions.checkNotNull(documentClasses); resetIfBuilt();@@ -685,17 +957,13 @@
* @param schema the {@link AppSearchSchema} that contains the target properties * @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited * sequence of property names.- *- * @exportToFramework:hide */- // TODO(b/296088047) unhide from framework when type property filters are made public.+ @CanIgnoreReturnValue @NonNull- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)- // @exportToFramework:endStrip()+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) public Builder addFilterProperties(@NonNull String schema, @NonNull Collection propertyPaths) { Preconditions.checkNotNull(schema);@@ -720,17 +988,14 @@
* * @param schema the {@link AppSearchSchema} that contains the target properties * @param propertyPaths The {@link PropertyPath} to search search over- *- * @exportToFramework:hide */- // TODO(b/296088047) unhide from framework when type property filters are made public. @NonNull- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- // @exportToFramework:startStrip()+ // Getter method is getFilterProperties+ @SuppressLint("MissingGetterMatchingBuilder") @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)- // @exportToFramework:endStrip()+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) public Builder addFilterPropertyPaths(@NonNull String schema, @NonNull Collection propertyPaths) { Preconditions.checkNotNull(schema);@@ -744,6 +1009,7 @@
// @exportToFramework:startStrip()+ /** * Adds property paths for the specified type to the property filter of * {@link SearchSpec} Entry. Only returns documents that have matches under the specified@@ -758,11 +1024,10 @@
* */ @NonNull- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)- public Builder addFilterProperties(@NonNull Class> documentClass,+ public Builder addFilterProperties(@NonNull java.lang.Class> documentClass, @NonNull Collection propertyPaths) throws AppSearchException { Preconditions.checkNotNull(documentClass); Preconditions.checkNotNull(propertyPaths);@@ -787,11 +1052,12 @@
* */ @NonNull- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ // Getter method is getFilterProperties+ @SuppressLint("MissingGetterMatchingBuilder") @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)- public Builder addFilterPropertyPaths(@NonNull Class> documentClass,+ public Builder addFilterPropertyPaths(@NonNull java.lang.Class> documentClass, @NonNull Collection propertyPaths) throws AppSearchException { Preconditions.checkNotNull(documentClass); Preconditions.checkNotNull(propertyPaths);@@ -966,6 +1232,19 @@
* current document being scored. Property weights come from what's specified in * {@link SearchSpec}. After normalizing, each provided weight will be divided by the * maximum weight, so that each of them will be <= 1.+ *
Returns a list of the matched similarity scores from "semanticSearch" in the query
+ * expression (see also {@link AppSearchSession#search}) based on embedding_index and+ * metric. If metric is omitted, it defaults to the metric specified in+ * {@link SearchSpec.Builder#setDefaultEmbeddingSearchMetricType(int)}. If no+ * "semanticSearch" is called for embedding_index and metric in the query, this+ * function will return an empty list. If multiple "semanticSearch"s are called for+ * the same embedding_index and metric, this function will return a list of their+ * merged scores.+ *
Example: `this.matchedSemanticScores(getSearchSpecEmbedding(0), "COSINE")` will
+ * return a list of matched scores within the range of [0.5, 1], if+ * `semanticSearch(getSearchSpecEmbedding(0), 0.5, 1, "COSINE")` is called in the+ * query expression. * * *
Some errors may occur when using advanced ranking.
@@ -1011,11 +1290,9 @@
*/ @CanIgnoreReturnValue @NonNull- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)- // @exportToFramework:endStrip() public Builder setRankingStrategy(@NonNull String advancedRankingExpression) { Preconditions.checkStringNotEmpty(advancedRankingExpression); resetIfBuilt();@@ -1025,6 +1302,88 @@
} /**+ * Adds informational ranking expressions to be evaluated for each document in the search+ * result. The values of these expressions will be returned to the caller via+ * {@link SearchResult#getInformationalRankingSignals()}. These expressions are purely for+ * the caller to retrieve additional information about the result and have no effect on+ * ranking.+ *+ *
The syntax is exactly the same as specified in
+ * {@link SearchSpec.Builder#setRankingStrategy(String)}.+ */+ @CanIgnoreReturnValue+ @NonNull+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS)+ @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)+ public Builder addInformationalRankingExpressions(+ @NonNull String... informationalRankingExpressions) {+ Preconditions.checkNotNull(informationalRankingExpressions);+ resetIfBuilt();+ return addInformationalRankingExpressions(+ Arrays.asList(informationalRankingExpressions));+ }++ /**+ * Adds informational ranking expressions to be evaluated for each document in the search+ * result. The values of these expressions will be returned to the caller via+ * {@link SearchResult#getInformationalRankingSignals()}. These expressions are purely for+ * the caller to retrieve additional information about the result and have no effect on+ * ranking.+ *+ *
The syntax is exactly the same as specified in
+ * {@link SearchSpec.Builder#setRankingStrategy(String)}.+ */+ @CanIgnoreReturnValue+ @NonNull+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS)+ @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)+ public Builder addInformationalRankingExpressions(+ @NonNull Collection informationalRankingExpressions) {+ Preconditions.checkNotNull(informationalRankingExpressions);+ resetIfBuilt();+ mInformationalRankingExpressions.addAll(informationalRankingExpressions);+ return this;+ }++ /**+ * Sets an optional log tag to indicate the source of this search.+ *+ *
Some AppSearch implementations may log a hash of this tag using statsd. This tag
+ * may be used for tracing performance issues and crashes to a component of an app.+ *+ *
Call this method and give a unique value if you want to distinguish this search
+ * scenario with other search scenarios during performance analysis.+ *+ *
Under no circumstances will AppSearch log the raw String value using statsd, but it
+ * will be provided as-is to custom {@code AppSearchLogger} implementations you have+ * registered in your app.+ *+ * @param searchSourceLogTag A String to indicate the source caller of this search. It is+ * used to label the search statsd for performance analysis. It+ * is not the tag we are using in {@link android.util.Log}. The+ * length of the teg should between 1 and 100.+ */+ @CanIgnoreReturnValue+ @NonNull+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG)+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG)+ public Builder setSearchSourceLogTag(@NonNull String searchSourceLogTag) {+ Preconditions.checkStringNotEmpty(searchSourceLogTag);+ Preconditions.checkArgument(searchSourceLogTag.length() <= 100,+ "The maximum supported tag length is 100. This tag is too long: "+ + searchSourceLogTag.length());+ resetIfBuilt();+ mSearchSourceLogTag = searchSourceLogTag;+ return this;+ }++ /** * Sets the order of returned search results, the default is * {@link #ORDER_DESCENDING}, meaning that results with higher scores come first. *@@ -1143,9 +1502,8 @@
* results of that type will be retrieved. * *
If property path is added for the
- * {@link SearchSpec#PROJECTION_SCHEMA_TYPE_WILDCARD}, then those property paths will- * apply to all results, excepting any types that have their own, specific property paths- * set.+ * {@link SearchSpec#SCHEMA_TYPE_WILDCARD}, then those property paths will apply to all+ * results, excepting any types that have their own, specific property paths set. * *
Suppose the following document is in the index.
*
{@code
@@ -1226,7 +1584,8 @@
@SuppressLint("MissingGetterMatchingBuilder") // Projections available from getProjections @NonNull public SearchSpec.Builder addProjectionsForDocumentClass(- @NonNull Class> documentClass, @NonNull Collection propertyPaths)+ @NonNull java.lang.Class> documentClass,+ @NonNull Collection propertyPaths) throws AppSearchException { Preconditions.checkNotNull(documentClass); resetIfBuilt();@@ -1247,7 +1606,8 @@
@SuppressLint("MissingGetterMatchingBuilder") // Projections available from getProjections @NonNull public SearchSpec.Builder addProjectionPathsForDocumentClass(- @NonNull Class> documentClass, @NonNull Collection propertyPaths)+ @NonNull java.lang.Class> documentClass,+ @NonNull Collection propertyPaths) throws AppSearchException { Preconditions.checkNotNull(documentClass); resetIfBuilt();@@ -1319,12 +1679,10 @@
* weight to set for that property. * @throws IllegalArgumentException if a weight is equal to or less than 0.0. */- // @exportToFramework:startStrip() @CanIgnoreReturnValue @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)- // @exportToFramework:endStrip() @NonNull public SearchSpec.Builder setPropertyWeights(@NonNull String schemaType, @NonNull Map propertyPathWeights) {@@ -1354,12 +1712,10 @@
* * @param joinSpec a specification on how to perform the Join operation. */- // @exportToFramework:startStrip() @CanIgnoreReturnValue @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.JOIN_SPEC_AND_QUALIFIED_ID)- // @exportToFramework:endStrip() @NonNull public Builder setJoinSpec(@NonNull JoinSpec joinSpec) { resetIfBuilt();@@ -1397,12 +1753,10 @@
* weight to set for that property. * @throws IllegalArgumentException if a weight is equal to or less than 0.0. */- // @exportToFramework:startStrip() @CanIgnoreReturnValue @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)- // @exportToFramework:endStrip() @NonNull public SearchSpec.Builder setPropertyWeightPaths(@NonNull String schemaType, @NonNull Map propertyPathWeights) {@@ -1460,7 +1814,7 @@
name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS) @NonNull public SearchSpec.Builder setPropertyWeightsForDocumentClass(- @NonNull Class> documentClass,+ @NonNull java.lang.Class> documentClass, @NonNull Map propertyPathWeights) throws AppSearchException { Preconditions.checkNotNull(documentClass); DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();@@ -1508,7 +1862,7 @@
name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS) @NonNull public SearchSpec.Builder setPropertyWeightPathsForDocumentClass(- @NonNull Class> documentClass,+ @NonNull java.lang.Class> documentClass, @NonNull Map propertyPathWeights) throws AppSearchException { Preconditions.checkNotNull(documentClass); DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();@@ -1518,6 +1872,72 @@
// @exportToFramework:endStrip() /**+ * Adds an embedding search to {@link SearchSpec} Entry, which will be referred in the+ * query expression and the ranking expression for embedding search.+ *+ * @see AppSearchSession#search+ * @see SearchSpec.Builder#setRankingStrategy(String)+ */+ @CanIgnoreReturnValue+ @NonNull+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public Builder addSearchEmbeddings(@NonNull EmbeddingVector... searchEmbeddings) {+ Preconditions.checkNotNull(searchEmbeddings);+ resetIfBuilt();+ return addSearchEmbeddings(Arrays.asList(searchEmbeddings));+ }++ /**+ * Adds an embedding search to {@link SearchSpec} Entry, which will be referred in the+ * query expression and the ranking expression for embedding search.+ *+ * @see AppSearchSession#search+ * @see SearchSpec.Builder#setRankingStrategy(String)+ */+ @CanIgnoreReturnValue+ @NonNull+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public Builder addSearchEmbeddings(+ @NonNull Collection searchEmbeddings) {+ Preconditions.checkNotNull(searchEmbeddings);+ resetIfBuilt();+ mSearchEmbeddings.addAll(searchEmbeddings);+ return this;+ }++ /**+ * Sets the default embedding metric type used for embedding search+ * (see {@link AppSearchSession#search}) and ranking+ * (see {@link SearchSpec.Builder#setRankingStrategy(String)}).+ *+ *
If this method is not called, the default embedding search metric type is
+ * {@link SearchSpec#EMBEDDING_SEARCH_METRIC_TYPE_COSINE}. Metrics specified within+ * "semanticSearch" or "matchedSemanticScores" functions in search/ranking expressions+ * will override this default.+ */+ @CanIgnoreReturnValue+ @NonNull+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public Builder setDefaultEmbeddingSearchMetricType(+ @EmbeddingSearchMetricType int defaultEmbeddingSearchMetricType) {+ Preconditions.checkArgumentInRange(defaultEmbeddingSearchMetricType,+ EMBEDDING_SEARCH_METRIC_TYPE_COSINE,+ EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN, "Embedding search metric type");+ resetIfBuilt();+ mDefaultEmbeddingSearchMetricType = defaultEmbeddingSearchMetricType;+ return this;+ }++ /** * Sets the NUMERIC_SEARCH feature as enabled/disabled according to the enabled parameter. * * @param enabled Enables the feature if true, otherwise disables it.@@ -1526,11 +1946,10 @@
* {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE} and all other numeric * querying features. */- // @exportToFramework:startStrip()+ @CanIgnoreReturnValue @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.NUMERIC_SEARCH)- // @exportToFramework:endStrip() @NonNull public Builder setNumericSearchEnabled(boolean enabled) { modifyEnabledFeature(FeatureConstants.NUMERIC_SEARCH, enabled);@@ -1550,11 +1969,10 @@
*
For example, The verbatim string operator '"foo/bar" OR baz' will ensure that
* 'foo/bar' is treated as a single 'verbatim' token. */- // @exportToFramework:startStrip()+ @CanIgnoreReturnValue @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.VERBATIM_SEARCH)- // @exportToFramework:endStrip() @NonNull public Builder setVerbatimSearchEnabled(boolean enabled) { modifyEnabledFeature(FeatureConstants.VERBATIM_SEARCH, enabled);@@ -1578,7 +1996,7 @@
*
The newly added custom functions covered by this feature are:
*
*
createList(String...)
- *
termSearch(String, List)
+ *
termSearch(String, {@code List})
* * *
createList takes a variable number of strings and returns a list of strings.
@@ -1590,11 +2008,10 @@
* for example, the query "(subject:foo OR body:foo) (subject:bar OR body:bar)" * could be rewritten as "termSearch(\"foo bar\", createList(\"subject\", \"bar\"))" */- // @exportToFramework:startStrip()+ @CanIgnoreReturnValue @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.LIST_FILTER_QUERY_LANGUAGE)- // @exportToFramework:endStrip() @NonNull public Builder setListFilterQueryLanguageEnabled(boolean enabled) { modifyEnabledFeature(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE, enabled);@@ -1602,6 +2019,65 @@
} /**+ * Sets the LIST_FILTER_HAS_PROPERTY_FUNCTION feature as enabled/disabled according to+ * the enabled parameter.+ *+ * @param enabled Enables the feature if true, otherwise disables it+ *+ *
If disabled, disallows the use of the "hasProperty" function. See
+ * {@link AppSearchSession#search} for more details about the function.+ */+ @CanIgnoreReturnValue+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.LIST_FILTER_HAS_PROPERTY_FUNCTION)+ @NonNull+ @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION)+ public Builder setListFilterHasPropertyFunctionEnabled(boolean enabled) {+ modifyEnabledFeature(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION, enabled);+ return this;+ }++ /**+ * Sets the embedding search feature as enabled/disabled according to the enabled parameter.+ *+ *
If disabled, disallows the use of the "semanticSearch" function. See
+ * {@link AppSearchSession#search} for more details about the function.+ *+ * @param enabled Enables the feature if true, otherwise disables it+ */+ @CanIgnoreReturnValue+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ @NonNull+ @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)+ public Builder setEmbeddingSearchEnabled(boolean enabled) {+ modifyEnabledFeature(FeatureConstants.EMBEDDING_SEARCH, enabled);+ return this;+ }++ /**+ * Sets the LIST_FILTER_TOKENIZE_FUNCTION feature as enabled/disabled according to+ * the enabled parameter.+ *+ * @param enabled Enables the feature if true, otherwise disables it+ *+ *
If disabled, disallows the use of the "tokenize" function. See
+ * {@link AppSearchSession#search} for more details about the function.+ */+ @CanIgnoreReturnValue+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.LIST_FILTER_TOKENIZE_FUNCTION)+ @NonNull+ @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)+ public Builder setListFilterTokenizeFunctionEnabled(boolean enabled) {+ modifyEnabledFeature(FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION, enabled);+ return this;+ }++ /** * Constructs a new {@link SearchSpec} from the contents of this builder. * * @throws IllegalArgumentException if property weights are provided with a@@ -1617,7 +2093,6 @@
*/ @NonNull public SearchSpec build() {- Bundle bundle = new Bundle(); if (mJoinSpec != null) { if (mRankingStrategy != RANKING_STRATEGY_JOIN_AGGREGATE_SCORE && mJoinSpec.getAggregationScoringStrategy()@@ -1626,59 +2101,26 @@
+ "the nested JoinSpec, but ranking strategy is not " + "RANKING_STRATEGY_JOIN_AGGREGATE_SCORE"); }- bundle.putBundle(JOIN_SPEC, mJoinSpec.getBundle()); } else if (mRankingStrategy == RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) { throw new IllegalStateException("Attempting to rank based on joined documents, but " + "no JoinSpec provided"); } if (!mTypePropertyWeights.isEmpty()- && RANKING_STRATEGY_RELEVANCE_SCORE != mRankingStrategy- && RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION != mRankingStrategy) {+ && mRankingStrategy != RANKING_STRATEGY_RELEVANCE_SCORE+ && mRankingStrategy != RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION) { throw new IllegalArgumentException("Property weights are only compatible with the " + "RANKING_STRATEGY_RELEVANCE_SCORE and " + "RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION ranking strategies."); }- // If the schema filter isn't empty, and there is a schema with a projection but not- // in the filter, that is a SearchSpec user error.- if (!mSchemas.isEmpty()) {- for (String schema : mProjectionTypePropertyMasks.keySet()) {- if (!mSchemas.contains(schema)) {- throw new IllegalArgumentException("Projection requested for schema not "- + "in schemas filters: " + schema);- }- }- }-- Set schemaFilter = new ArraySet<>(mSchemas);- if (!mSchemas.isEmpty()) {- for (String schema : mTypePropertyFilters.keySet()) {- if (!schemaFilter.contains(schema)) {- throw new IllegalStateException(- "The schema: " + schema + " exists in the property filter but "- + "doesn't exist in the schema filter.");- }- }- }- bundle.putStringArrayList(SCHEMA_FIELD, mSchemas);- bundle.putBundle(PROPERTY_FIELD, mTypePropertyFilters);- bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);- bundle.putStringArrayList(PACKAGE_NAME_FIELD, mPackageNames);- bundle.putStringArrayList(ENABLED_FEATURES_FIELD, new ArrayList<>(mEnabledFeatures));- bundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks);- bundle.putInt(NUM_PER_PAGE_FIELD, mResultCountPerPage);- bundle.putInt(TERM_MATCH_TYPE_FIELD, mTermMatchType);- bundle.putInt(SNIPPET_COUNT_FIELD, mSnippetCount);- bundle.putInt(SNIPPET_COUNT_PER_PROPERTY_FIELD, mSnippetCountPerProperty);- bundle.putInt(MAX_SNIPPET_FIELD, mMaxSnippetSize);- bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy);- bundle.putInt(ORDER_FIELD, mOrder);- bundle.putInt(RESULT_GROUPING_TYPE_FLAGS, mGroupingTypeFlags);- bundle.putInt(RESULT_GROUPING_LIMIT, mGroupingLimit);- bundle.putBundle(TYPE_PROPERTY_WEIGHTS_FIELD, mTypePropertyWeights);- bundle.putString(ADVANCED_RANKING_EXPRESSION, mAdvancedRankingExpression); mBuilt = true;- return new SearchSpec(bundle);+ return new SearchSpec(mTermMatchType, mSchemas, mNamespaces,+ mTypePropertyFilters, mPackageNames, mResultCountPerPage,+ mRankingStrategy, mOrder, mSnippetCount, mSnippetCountPerProperty,+ mMaxSnippetSize, mProjectionTypePropertyMasks, mGroupingTypeFlags,+ mGroupingLimit, mTypePropertyWeights, mJoinSpec, mAdvancedRankingExpression,+ new ArrayList<>(mEnabledFeatures), mSearchSourceLogTag, mSearchEmbeddings,+ mDefaultEmbeddingSearchMetricType, mInformationalRankingExpressions); } private void resetIfBuilt() {@@ -1689,6 +2131,9 @@
mPackageNames = new ArrayList<>(mPackageNames); mProjectionTypePropertyMasks = BundleUtil.deepCopy(mProjectionTypePropertyMasks); mTypePropertyWeights = BundleUtil.deepCopy(mTypePropertyWeights);+ mSearchEmbeddings = new ArrayList<>(mSearchEmbeddings);+ mInformationalRankingExpressions = new ArrayList<>(+ mInformationalRankingExpressions); mBuilt = false; } }
@@ -18,14 +18,22 @@
import android.annotation.SuppressLint; import android.os.Bundle;+import android.os.Parcel;+import android.os.Parcelable; import androidx.annotation.IntDef; import androidx.annotation.IntRange; import androidx.annotation.NonNull;+import androidx.annotation.RequiresFeature; import androidx.annotation.RestrictTo; import androidx.appsearch.annotation.CanIgnoreReturnValue; import androidx.appsearch.annotation.Document; import androidx.appsearch.exceptions.AppSearchException;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags;+import androidx.appsearch.safeparcel.AbstractSafeParcelable;+import androidx.appsearch.safeparcel.SafeParcelable;+import androidx.appsearch.safeparcel.stub.StubCreators.SearchSuggestionSpecCreator; import androidx.appsearch.util.BundleUtil; import androidx.collection.ArrayMap; import androidx.collection.ArraySet;@@ -47,24 +55,48 @@
* * @see AppSearchSession#searchSuggestionAsync */-public final class SearchSuggestionSpec {- static final String NAMESPACE_FIELD = "namespace";- static final String SCHEMA_FIELD = "schema";- static final String PROPERTY_FIELD = "property";- static final String DOCUMENT_IDS_FIELD = "documentIds";- static final String MAXIMUM_RESULT_COUNT_FIELD = "maximumResultCount";- static final String RANKING_STRATEGY_FIELD = "rankingStrategy";- private final Bundle mBundle;[email protected](creator = "SearchSuggestionSpecCreator")+@SuppressWarnings("HiddenSuperclass")+public final class SearchSuggestionSpec extends AbstractSafeParcelable {+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @NonNull public static final Parcelable.Creator CREATOR =+ new SearchSuggestionSpecCreator();+ @Field(id = 1, getter = "getFilterNamespaces")+ private final List mFilterNamespaces;+ @Field(id = 2, getter = "getFilterSchemas")+ private final List mFilterSchemas;+ // Maps are not supported by SafeParcelable fields, using Bundle instead. Here the key is+ // schema type and value is a list of target property paths in that schema to search over.+ @Field(id = 3)+ final Bundle mFilterProperties;+ // Maps are not supported by SafeParcelable fields, using Bundle instead. Here the key is+ // namespace and value is a list of target document ids in that namespace to search over.+ @Field(id = 4)+ final Bundle mFilterDocumentIds;+ @Field(id = 5, getter = "getRankingStrategy")+ private final int mRankingStrategy;+ @Field(id = 6, getter = "getMaximumResultCount") private final int mMaximumResultCount; /** @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- public SearchSuggestionSpec(@NonNull Bundle bundle) {- Preconditions.checkNotNull(bundle);- mBundle = bundle;- mMaximumResultCount = bundle.getInt(MAXIMUM_RESULT_COUNT_FIELD);- Preconditions.checkArgument(mMaximumResultCount >= 1,+ @Constructor+ public SearchSuggestionSpec(+ @Param(id = 1) @NonNull List filterNamespaces,+ @Param(id = 2) @NonNull List filterSchemas,+ @Param(id = 3) @NonNull Bundle filterProperties,+ @Param(id = 4) @NonNull Bundle filterDocumentIds,+ @Param(id = 5) @SuggestionRankingStrategy int rankingStrategy,+ @Param(id = 6) int maximumResultCount) {+ Preconditions.checkArgument(maximumResultCount >= 1, "MaximumResultCount must be positive.");+ mFilterNamespaces = Preconditions.checkNotNull(filterNamespaces);+ mFilterSchemas = Preconditions.checkNotNull(filterSchemas);+ mFilterProperties = Preconditions.checkNotNull(filterProperties);+ mFilterDocumentIds = Preconditions.checkNotNull(filterDocumentIds);+ mRankingStrategy = rankingStrategy;+ mMaximumResultCount = maximumResultCount; } /**@@ -111,17 +143,6 @@
public static final int SUGGESTION_RANKING_STRATEGY_NONE = 2; /**- * Returns the {@link Bundle} populated by this builder.- *- * @exportToFramework:hide- */- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- @NonNull- public Bundle getBundle() {- return mBundle;- }-- /** * Returns the maximum number of wanted suggestion that will be returned in the result object. */ public int getMaximumResultCount() {@@ -135,17 +156,16 @@
*/ @NonNull public List getFilterNamespaces() {- List namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);- if (namespaces == null) {+ if (mFilterNamespaces == null) { return Collections.emptyList(); }- return Collections.unmodifiableList(namespaces);+ return Collections.unmodifiableList(mFilterNamespaces); } /** Returns the ranking strategy. */ @SuggestionRankingStrategy public int getRankingStrategy() {- return mBundle.getInt(RANKING_STRATEGY_FIELD);+ return mRankingStrategy; } /**@@ -155,11 +175,10 @@
*/ @NonNull public List getFilterSchemas() {- List schemaTypes = mBundle.getStringArrayList(SCHEMA_FIELD);- if (schemaTypes == null) {+ if (mFilterSchemas == null) { return Collections.emptyList(); }- return Collections.unmodifiableList(schemaTypes);+ return Collections.unmodifiableList(mFilterSchemas); } /**@@ -173,20 +192,15 @@
* *
Calling this function repeatedly is inefficient. Prefer to retain the Map returned
* by this function, rather than calling it multiple times.- *- * @exportToFramework:hide */- // TODO(b/228240987) migrate this API when we support property restrict for multiple terms @NonNull- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) public Map> getFilterProperties() {- Bundle typePropertyPathsBundle = Preconditions.checkNotNull(- mBundle.getBundle(PROPERTY_FIELD));- Set schemas = typePropertyPathsBundle.keySet();+ Set schemas = mFilterProperties.keySet(); Map> typePropertyPathsMap = new ArrayMap<>(schemas.size()); for (String schema : schemas) { typePropertyPathsMap.put(schema, Preconditions.checkNotNull(- typePropertyPathsBundle.getStringArrayList(schema)));+ mFilterProperties.getStringArrayList(schema))); } return typePropertyPathsMap; }@@ -205,13 +219,11 @@
*/ @NonNull public Map> getFilterDocumentIds() {- Bundle documentIdsBundle = Preconditions.checkNotNull(- mBundle.getBundle(DOCUMENT_IDS_FIELD));- Set namespaces = documentIdsBundle.keySet();+ Set namespaces = mFilterDocumentIds.keySet(); Map> documentIdsMap = new ArrayMap<>(namespaces.size()); for (String namespace : namespaces) { documentIdsMap.put(namespace, Preconditions.checkNotNull(- documentIdsBundle.getStringArrayList(namespace)));+ mFilterDocumentIds.getStringArrayList(namespace))); } return documentIdsMap; }@@ -328,7 +340,7 @@
@SuppressLint("MissingGetterMatchingBuilder") @CanIgnoreReturnValue @NonNull- public Builder addFilterDocumentClasses(@NonNull Class>... documentClasses)+ public Builder addFilterDocumentClasses(@NonNull java.lang.Class>... documentClasses) throws AppSearchException { Preconditions.checkNotNull(documentClasses); resetIfBuilt();@@ -353,12 +365,13 @@
@CanIgnoreReturnValue @NonNull public Builder addFilterDocumentClasses(- @NonNull Collection extends Class>> documentClasses) throws AppSearchException {+ @NonNull Collection extends java.lang.Class>> documentClasses)+ throws AppSearchException { Preconditions.checkNotNull(documentClasses); resetIfBuilt(); List schemas = new ArrayList<>(documentClasses.size()); DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();- for (Class> documentClass : documentClasses) {+ for (java.lang.Class> documentClass : documentClasses) { DocumentClassFactory> factory = registry.getOrCreateFactory(documentClass); schemas.add(factory.getSchemaName()); }@@ -385,11 +398,13 @@
* @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited * sequence of property names indicating which property in the * document these snippets correspond to.- * @exportToFramework:hide */- // TODO(b/228240987) migrate this API when we support property restrict for multiple terms+ @CanIgnoreReturnValue @NonNull- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) public Builder addFilterProperties(@NonNull String schema, @NonNull Collection propertyPaths) { Preconditions.checkNotNull(schema);@@ -418,12 +433,15 @@
* * @param schema the {@link AppSearchSchema} that contains the target properties * @param propertyPaths The {@link PropertyPath} to search suggestion over- *- * @exportToFramework:hide */- // TODO(b/228240987) migrate this API when we support property restrict for multiple terms+ @CanIgnoreReturnValue @NonNull- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ // Getter method is getFilterProperties+ @SuppressLint("MissingGetterMatchingBuilder")+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)+ @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) public Builder addFilterPropertyPaths(@NonNull String schema, @NonNull Collection propertyPaths) { Preconditions.checkNotNull(schema);@@ -453,12 +471,12 @@
* @param propertyPaths The String version of {@link PropertyPath}. A * {@code dot-delimited sequence of property names indicating which property in the * document these snippets correspond to.- * @exportToFramework:hide */- // TODO(b/228240987) migrate this API when we support property restrict for multiple terms @NonNull- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- public Builder addFilterProperties(@NonNull Class> documentClass,+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)+ public Builder addFilterProperties(@NonNull java.lang.Class> documentClass, @NonNull Collection propertyPaths) throws AppSearchException { Preconditions.checkNotNull(documentClass); Preconditions.checkNotNull(propertyPaths);@@ -484,12 +502,14 @@
* * @param documentClass class annotated with {@link Document}. * @param propertyPaths The {@link PropertyPath} to search suggestion over- * @exportToFramework:hide */- // TODO(b/228240987) migrate this API when we support property restrict for multiple terms @NonNull- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- public Builder addFilterPropertyPaths(@NonNull Class> documentClass,+ // Getter method is getFilterProperties+ @SuppressLint("MissingGetterMatchingBuilder")+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)+ public Builder addFilterPropertyPaths(@NonNull java.lang.Class> documentClass, @NonNull Collection propertyPaths) throws AppSearchException { Preconditions.checkNotNull(documentClass); Preconditions.checkNotNull(propertyPaths);@@ -540,7 +560,6 @@
/** Constructs a new {@link SearchSpec} from the contents of this builder. */ @NonNull public SearchSuggestionSpec build() {- Bundle bundle = new Bundle(); if (!mSchemas.isEmpty()) { Set schemaFilter = new ArraySet<>(mSchemas); for (String schema : mTypePropertyFilters.keySet()) {@@ -561,14 +580,14 @@
} } }- bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);- bundle.putStringArrayList(SCHEMA_FIELD, mSchemas);- bundle.putBundle(PROPERTY_FIELD, mTypePropertyFilters);- bundle.putBundle(DOCUMENT_IDS_FIELD, mDocumentIds);- bundle.putInt(MAXIMUM_RESULT_COUNT_FIELD, mTotalResultCount);- bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy); mBuilt = true;- return new SearchSuggestionSpec(bundle);+ return new SearchSuggestionSpec(+ mNamespaces,+ mSchemas,+ mTypePropertyFilters,+ mDocumentIds,+ mRankingStrategy,+ mTotalResultCount); } private void resetIfBuilt() {@@ -581,4 +600,11 @@
} } }++ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @Override+ public void writeToParcel(@NonNull Parcel dest, int flags) {+ SearchSuggestionSpecCreator.writeToParcel(this, dest, flags);+ } }
@@ -21,10 +21,13 @@
import androidx.annotation.IntDef; import androidx.annotation.IntRange; import androidx.annotation.NonNull;+import androidx.annotation.Nullable; import androidx.annotation.RequiresFeature; import androidx.annotation.RestrictTo; import androidx.appsearch.annotation.CanIgnoreReturnValue; import androidx.appsearch.exceptions.AppSearchException;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags; import androidx.collection.ArrayMap; import androidx.collection.ArraySet; import androidx.core.util.Preconditions;@@ -100,13 +103,13 @@
READ_EXTERNAL_STORAGE, READ_HOME_APP_SEARCH_DATA, READ_ASSISTANT_APP_SEARCH_DATA,+ ENTERPRISE_ACCESS,+ MANAGED_PROFILE_CONTACTS_ACCESS, }) @Retention(RetentionPolicy.SOURCE)- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)- // @exportToFramework:endStrip() @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public @interface AppSearchSupportedPermission {}@@ -114,72 +117,85 @@
* The {@link android.Manifest.permission#READ_SMS} AppSearch supported in * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} */- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)- // @exportToFramework:endStrip() public static final int READ_SMS = 1; /** * The {@link android.Manifest.permission#READ_CALENDAR} AppSearch supported in * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} */- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)- // @exportToFramework:endStrip() public static final int READ_CALENDAR = 2; /** * The {@link android.Manifest.permission#READ_CONTACTS} AppSearch supported in * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} */- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)- // @exportToFramework:endStrip() public static final int READ_CONTACTS = 3; /** * The {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} AppSearch supported in * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} */- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)- // @exportToFramework:endStrip() public static final int READ_EXTERNAL_STORAGE = 4; /** * The {@link android.Manifest.permission#READ_HOME_APP_SEARCH_DATA} AppSearch supported in * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} */- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)- // @exportToFramework:endStrip() public static final int READ_HOME_APP_SEARCH_DATA = 5; /** * The {@link android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA} AppSearch supported in * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} */- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)- // @exportToFramework:endStrip() public static final int READ_ASSISTANT_APP_SEARCH_DATA = 6;+ /**+ * A schema must have this permission set through {@link+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} to be visible to an+ * {@link EnterpriseGlobalSearchSession}. A call from a regular {@link GlobalSearchSession} will+ * not count as having this permission.+ *+ * @exportToFramework:hide+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ public static final int ENTERPRISE_ACCESS = 7;++ /**+ * A schema with this permission set through {@link+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} requires the caller+ * to have managed profile contacts access from {@link android.app.admin.DevicePolicyManager} to+ * be visible. This permission indicates that the protected schema may expose managed profile+ * data for contacts search.+ *+ * @exportToFramework:hide+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ public static final int MANAGED_PROFILE_CONTACTS_ACCESS = 8;+ private final Set mSchemas; private final Set mSchemasNotDisplayedBySystem; private final Map> mSchemasVisibleToPackages; private final Map>> mSchemasVisibleToPermissions;+ private final Map mPubliclyVisibleSchemas;+ private final Map> mSchemasVisibleToConfigs; private final Map mMigrators; private final boolean mForceOverride; private final int mVersion;@@ -188,6 +204,8 @@
@NonNull Set schemasNotDisplayedBySystem, @NonNull Map> schemasVisibleToPackages, @NonNull Map>> schemasVisibleToPermissions,+ @NonNull Map publiclyVisibleSchemas,+ @NonNull Map> schemasVisibleToConfigs, @NonNull Map migrators, boolean forceOverride, int version) {@@ -195,6 +213,8 @@
mSchemasNotDisplayedBySystem = Preconditions.checkNotNull(schemasNotDisplayedBySystem); mSchemasVisibleToPackages = Preconditions.checkNotNull(schemasVisibleToPackages); mSchemasVisibleToPermissions = Preconditions.checkNotNull(schemasVisibleToPermissions);+ mPubliclyVisibleSchemas = Preconditions.checkNotNull(publiclyVisibleSchemas);+ mSchemasVisibleToConfigs = Preconditions.checkNotNull(schemasVisibleToConfigs); mMigrators = Preconditions.checkNotNull(migrators); mForceOverride = forceOverride; mVersion = version;@@ -258,12 +278,41 @@
* {@link SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and * {@link SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}. */+ // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden @NonNull public Map>> getRequiredPermissionsForSchemaTypeVisibility() { return deepCopy(mSchemasVisibleToPermissions); } /**+ * Returns a mapping of publicly visible schemas to the {@link PackageIdentifier} specifying+ * the package the schemas are from.+ */+ @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)+ @NonNull+ public Map getPubliclyVisibleSchemas() {+ return Collections.unmodifiableMap(mPubliclyVisibleSchemas);+ }++ /**+ * Returns a mapping of schema types to the set of {@link SchemaVisibilityConfig} that have+ * access to that schema type.+ *+ *
It’s inefficient to call this method repeatedly.
+ * @see SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig+ */+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)+ @NonNull+ public Map> getSchemasVisibleToConfigs() {+ Map> copy = new ArrayMap<>();+ for (Map.Entry> entry :+ mSchemasVisibleToConfigs.entrySet()) {+ copy.put(entry.getKey(), new ArraySet<>(entry.getValue()));+ }+ return copy;+ }++ /** * Returns the map of {@link Migrator}, the key will be the schema type of the * {@link Migrator} associated with. */@@ -307,6 +356,9 @@
private ArrayMap> mSchemasVisibleToPackages = new ArrayMap<>(); private ArrayMap>> mSchemasVisibleToPermissions = new ArrayMap<>();+ private ArrayMap mPubliclyVisibleSchemas = new ArrayMap<>();+ private ArrayMap> mSchemaVisibleToConfigs =+ new ArrayMap<>(); private ArrayMap mMigrators = new ArrayMap<>(); private boolean mForceOverride = false; private int mVersion = DEFAULT_VERSION;@@ -451,10 +503,16 @@
*
You can call this method to add multiple permission combinations, and the querier
* will have access if they holds ANY of the combinations. *- *
The supported Permissions are {@link #READ_SMS}, {@link #READ_CALENDAR},
+ *
The supported Permissions are {@link #READ_SMS}, {@link #READ_CALENDAR},
The relationship between permissions added in this method and package visibility
+ * setting {@link #setSchemaTypeVisibilityForPackage} is "OR". The caller could access+ * the schema if they match ANY requirements. If you want to set "AND" requirements like+ * a caller must hold required permissions AND it is a specified package, please use+ * {@link #addSchemaTypeVisibleToConfig}.+ * * @see android.Manifest.permission#READ_SMS * @see android.Manifest.permission#READ_CALENDAR * @see android.Manifest.permission#READ_CONTACTS@@ -467,14 +525,13 @@
* schema. * @throws IllegalArgumentException – if input unsupported permission. */+ // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden // Merged list available from getRequiredPermissionsForSchemaTypeVisibility @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder")- // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)- // @exportToFramework:endStrip() @NonNull public Builder addRequiredPermissionsForSchemaTypeVisibility(@NonNull String schemaType, @AppSearchSupportedPermission @NonNull Set permissions) {@@ -482,7 +539,7 @@
Preconditions.checkNotNull(permissions); for (int permission : permissions) { Preconditions.checkArgumentInRange(permission, READ_SMS,- READ_ASSISTANT_APP_SEARCH_DATA, "permission");+ MANAGED_PROFILE_CONTACTS_ACCESS, "permission"); } resetIfBuilt(); Set> visibleToPermissions = mSchemasVisibleToPermissions.get(schemaType);@@ -495,12 +552,10 @@
} /** Clears all required permissions combinations for the given schema type. */- // @exportToFramework:startStrip() @CanIgnoreReturnValue @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)- // @exportToFramework:endStrip() @NonNull public Builder clearRequiredPermissionsForSchemaTypeVisibility(@NonNull String schemaType) { Preconditions.checkNotNull(schemaType);@@ -525,6 +580,12 @@
* *
By default, data sharing between applications is disabled.
*+ *
The relationship between permissions added in this method and package visibility
+ * setting {@link #setSchemaTypeVisibilityForPackage} is "OR". The caller could access+ * the schema if they match ANY requirements. If you want to set "AND" requirements like+ * a caller must hold required permissions AND it is a specified package, please use+ * {@link #addSchemaTypeVisibleToConfig}.+ * * @param schemaType The schema type to set visibility on. * @param visible Whether the {@code schemaType} will be visible or not. * @param packageIdentifier Represents the package that will be granted visibility.@@ -564,6 +625,132 @@
} /**+ * Specify that the schema should be publicly available, to packages which already have+ * visibility to {@code packageIdentifier}. This visibility is determined by the result of+ * {@link android.content.pm.PackageManager#canPackageQuery}.+ *+ *
It is possible for the packageIdentifier parameter to be different from the
+ * package performing the indexing. This might happen in the case of an on-device indexer+ * processing information about various packages. The visibility will be the same+ * regardless of which package indexes the document, as the visibility is based on the+ * packageIdentifier parameter.+ *+ *
If this is called repeatedly with the same schema, the {@link PackageIdentifier} in
+ * the last call will be used as the "from" package for that schema.+ *+ *
Calling this with packageIdentifier set to null is valid, and will remove public
+ * visibility for the schema.+ *+ * @param schema the schema to make publicly accessible.+ * @param packageIdentifier if an app can see this package via+ * PackageManager#canPackageQuery, it will be able to see the+ * documents of type {@code schema}.+ */+ // Merged list available from getPubliclyVisibleSchemas+ @CanIgnoreReturnValue+ @SuppressLint("MissingGetterMatchingBuilder")+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)+ @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)+ @NonNull+ public Builder setPubliclyVisibleSchema(@NonNull String schema,+ @Nullable PackageIdentifier packageIdentifier) {+ Preconditions.checkNotNull(schema);+ resetIfBuilt();++ // If the package identifier is null or empty we clear public visibility+ if (packageIdentifier == null || packageIdentifier.getPackageName().isEmpty()) {+ mPubliclyVisibleSchemas.remove(schema);+ return this;+ }++ mPubliclyVisibleSchemas.put(schema, packageIdentifier);+ return this;+ }++// @exportToFramework:startStrip()+ /**+ * Specify that the schema should be publicly available, to packages which already have+ * visibility to {@code packageIdentifier}.+ *+ * @param documentClass the document to make publicly accessible.+ * @param packageIdentifier if an app can see this package via+ * PackageManager#canPackageQuery, it will be able to see the+ * documents of type {@code documentClass}.+ * @see SetSchemaRequest.Builder#setPubliclyVisibleSchema+ */+ // Merged list available from getPubliclyVisibleSchemas+ @CanIgnoreReturnValue+ @SuppressLint("MissingGetterMatchingBuilder")+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)+ @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)+ @NonNull+ public Builder setPubliclyVisibleDocumentClass(@NonNull Class> documentClass,+ @Nullable PackageIdentifier packageIdentifier) throws AppSearchException {+ Preconditions.checkNotNull(documentClass);+ resetIfBuilt();+ DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();+ DocumentClassFactory> factory = registry.getOrCreateFactory(documentClass);+ return setPubliclyVisibleSchema(factory.getSchemaName(), packageIdentifier);+ }+// @exportToFramework:endStrip()++ /**+ * Sets the documents from the provided {@code schemaType} can be read by the caller if they+ * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}.+ *+ *
The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A
+ * caller must match ALL requirements to access the schema. For example, a caller must hold+ * required permissions AND it is a specified package.+ *+ *
You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig}s,
+ * and the querier will have access if they match ANY of the+ * {@link SchemaVisibilityConfig}.+ *+ * @param schemaType The schema type to set visibility on.+ * @param schemaVisibilityConfig The {@link SchemaVisibilityConfig} holds all requirements+ * that a call must to match to access the schema.+ */+ // Merged list available from getSchemasVisibleToConfigs+ @CanIgnoreReturnValue+ @SuppressLint("MissingGetterMatchingBuilder")+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)+ @NonNull+ public Builder addSchemaTypeVisibleToConfig(@NonNull String schemaType,+ @NonNull SchemaVisibilityConfig schemaVisibilityConfig) {+ Preconditions.checkNotNull(schemaType);+ Preconditions.checkNotNull(schemaVisibilityConfig);+ resetIfBuilt();+ Set visibleToConfigs = mSchemaVisibleToConfigs.get(schemaType);+ if (visibleToConfigs == null) {+ visibleToConfigs = new ArraySet<>();+ mSchemaVisibleToConfigs.put(schemaType, visibleToConfigs);+ }+ visibleToConfigs.add(schemaVisibilityConfig);+ return this;+ }++ /** Clears all visible to {@link SchemaVisibilityConfig} for the given schema type. */+ @CanIgnoreReturnValue+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)+ @NonNull+ public Builder clearSchemaTypeVisibleToConfigs(@NonNull String schemaType) {+ Preconditions.checkNotNull(schemaType);+ resetIfBuilt();+ mSchemaVisibleToConfigs.remove(schemaType);+ return this;+ }++ /** * Sets the {@link Migrator} associated with the given SchemaType. * *
The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
@@ -684,6 +871,12 @@
* *
By default, app data sharing between applications is disabled.
*+ *
The relationship between visible packages added in this method and permission
+ * visibility setting {@link #addRequiredPermissionsForSchemaTypeVisibility} is "OR". The+ * caller could access the schema if they match ANY requirements. If you want to set+ * "AND" requirements like a caller must hold required permissions AND it is a specified+ * package, please use {@link #addSchemaTypeVisibleToConfig}.+ * *
Merged list available from {@link #getSchemasVisibleToPackages()}.
* * @param documentClass The {@link androidx.appsearch.annotation.Document} class to set@@ -721,6 +914,12 @@
* {@link #READ_CONTACTS}, {@link #READ_EXTERNAL_STORAGE}, * {@link #READ_HOME_APP_SEARCH_DATA} and {@link #READ_ASSISTANT_APP_SEARCH_DATA}. *+ *
The relationship between visible packages added in this method and permission
+ * visibility setting {@link #addRequiredPermissionsForSchemaTypeVisibility} is "OR". The+ * caller could access the schema if they match ANY requirements. If you want to set+ * "AND" requirements like a caller must hold required permissions AND it is a specified+ * package, please use {@link #addSchemaTypeVisibleToConfig}.+ * *
Merged map available from {@link #getRequiredPermissionsForSchemaTypeVisibility()}.
* @see android.Manifest.permission#READ_SMS * @see android.Manifest.permission#READ_CALENDAR@@ -768,6 +967,58 @@
DocumentClassFactory> factory = registry.getOrCreateFactory(documentClass); return clearRequiredPermissionsForSchemaTypeVisibility(factory.getSchemaName()); }++ /**+ * Sets the documents from the provided {@code schemaType} can be read by the caller if they+ * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}.+ *+ *
The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A
+ * caller must match ALL requirements to access the schema. For example, a caller must hold+ * required permissions AND it is a specified package.+ *+ *
You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig}s,
+ * and the querier will have access if they match ANY of the {@link SchemaVisibilityConfig}.+ *+ * @param documentClass A class annotated with+ * {@link androidx.appsearch.annotation.Document}, the+ * visibility of which will be configured+ * @param schemaVisibilityConfig The {@link SchemaVisibilityConfig} holds all+ * requirements that a call must to match to access the+ * schema.+ */+ // Merged list available from getSchemasVisibleToConfigs+ @SuppressLint("MissingGetterMatchingBuilder")+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)+ @NonNull+ public Builder addDocumentClassVisibleToConfig(+ @NonNull Class> documentClass,+ @NonNull SchemaVisibilityConfig schemaVisibilityConfig)+ throws AppSearchException {+ Preconditions.checkNotNull(documentClass);+ resetIfBuilt();+ DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();+ DocumentClassFactory> factory = registry.getOrCreateFactory(documentClass);+ return addSchemaTypeVisibleToConfig(factory.getSchemaName(),+ schemaVisibilityConfig);+ }++ /** Clears all visible to {@link SchemaVisibilityConfig} for the given schema type. */+ @RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)+ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)+ @NonNull+ public Builder clearDocumentClassVisibleToConfigs(+ @NonNull Class> documentClass) throws AppSearchException {+ Preconditions.checkNotNull(documentClass);+ resetIfBuilt();+ DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();+ DocumentClassFactory> factory = registry.getOrCreateFactory(documentClass);+ return clearSchemaTypeVisibleToConfigs(factory.getSchemaName());+ } // @exportToFramework:endStrip() /**@@ -841,6 +1092,8 @@
Set referencedSchemas = new ArraySet<>(mSchemasNotDisplayedBySystem); referencedSchemas.addAll(mSchemasVisibleToPackages.keySet()); referencedSchemas.addAll(mSchemasVisibleToPermissions.keySet());+ referencedSchemas.addAll(mPubliclyVisibleSchemas.keySet());+ referencedSchemas.addAll(mSchemaVisibleToConfigs.keySet()); for (AppSearchSchema schema : mSchemas) { referencedSchemas.remove(schema.getSchemaType());@@ -861,6 +1114,8 @@
mSchemasNotDisplayedBySystem, mSchemasVisibleToPackages, mSchemasVisibleToPermissions,+ mPubliclyVisibleSchemas,+ mSchemaVisibleToConfigs, mMigrators, mForceOverride, mVersion);@@ -876,8 +1131,18 @@
} mSchemasVisibleToPackages = schemasVisibleToPackages;+ mPubliclyVisibleSchemas = new ArrayMap<>(mPubliclyVisibleSchemas);+ mSchemasVisibleToPermissions = deepCopy(mSchemasVisibleToPermissions);+ ArrayMap> schemaVisibleToConfigs =+ new ArrayMap<>(mSchemaVisibleToConfigs.size());+ for (Map.Entry> entry :+ mSchemaVisibleToConfigs.entrySet()) {+ schemaVisibleToConfigs.put(entry.getKey(), new ArraySet<>(entry.getValue()));+ }+ mSchemaVisibleToConfigs = schemaVisibleToConfigs;+ mSchemas = new ArraySet<>(mSchemas); mSchemasNotDisplayedBySystem = new ArraySet<>(mSchemasNotDisplayedBySystem); mMigrators = new ArrayMap<>(mMigrators);@@ -886,8 +1151,8 @@
} }- static ArrayMap>> deepCopy(@NonNull Map- Set>> original) {+ private static ArrayMap>> deepCopy(+ @NonNull Map>> original) { ArrayMap>> copy = new ArrayMap<>(original.size()); for (Map.Entry>> entry : original.entrySet()) { Set> valueCopy = new ArraySet<>();
@@ -16,12 +16,19 @@
package androidx.appsearch.app;-import android.os.Bundle;+import android.os.Parcel;+import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.appsearch.annotation.CanIgnoreReturnValue;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags;+import androidx.appsearch.safeparcel.AbstractSafeParcelable;+import androidx.appsearch.safeparcel.SafeParcelable;+import androidx.appsearch.safeparcel.stub.StubCreators.MigrationFailureCreator;+import androidx.appsearch.safeparcel.stub.StubCreators.SetSchemaResponseCreator; import androidx.collection.ArraySet; import androidx.core.util.Preconditions;@@ -32,57 +39,70 @@
import java.util.Set; /** The response class of {@link AppSearchSession#setSchemaAsync} */-public class SetSchemaResponse {[email protected](creator = "SetSchemaResponseCreator")+@SuppressWarnings("HiddenSuperclass")+public final class SetSchemaResponse extends AbstractSafeParcelable {+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @NonNull public static final Parcelable.Creator CREATOR =+ new SetSchemaResponseCreator();- private static final String DELETED_TYPES_FIELD = "deletedTypes";- private static final String INCOMPATIBLE_TYPES_FIELD = "incompatibleTypes";- private static final String MIGRATED_TYPES_FIELD = "migratedTypes";+ @Field(id = 1)+ final List mDeletedTypes;+ @Field(id = 2)+ final List mIncompatibleTypes;+ @Field(id = 3)+ final List mMigratedTypes;- private final Bundle mBundle; /**- * The migrationFailures won't be saved in the bundle. Since:+ * The migrationFailures won't be saved as a SafeParcelable field. Since: *
*
{@link MigrationFailure} is generated in {@link AppSearchSession} which will be
- * the SDK side in platform. We don't need to pass it from service side via binder.- *
Translate multiple {@link MigrationFailure}s to bundles in {@link Builder} and then
- * back in constructor will be a huge waste.+ * the SDK side in platform. We don't need to pass it from service side via binder as+ * a part of {@link SetSchemaResponse}.+ *
Writing multiple {@link MigrationFailure}s to SafeParcelable in {@link Builder} and
+ * then back in constructor will be a huge waste. * */ private final List mMigrationFailures;- /** Cache of the inflated deleted schema types. Comes from inflating mBundles at first use. */- @Nullable- private Set mDeletedTypes;+ /** Cache of the inflated deleted schema types. Comes from inflating mDeletedTypes at first use+ */+ @Nullable private Set mDeletedTypesCached;- /** Cache of the inflated migrated schema types. Comes from inflating mBundles at first use. */- @Nullable- private Set mMigratedTypes;+ /** Cache of the inflated migrated schema types. Comes from inflating mMigratedTypes at first+ * use.+ */+ @Nullable private Set mMigratedTypesCached; /**- * Cache of the inflated incompatible schema types. Comes from inflating mBundles at first use.+ * Cache of the inflated incompatible schema types. Comes from inflating mIncompatibleTypes at+ * first use. */- @Nullable- private Set mIncompatibleTypes;+ @Nullable private Set mIncompatibleTypesCached;- SetSchemaResponse(@NonNull Bundle bundle, @NonNull List migrationFailures) {- mBundle = Preconditions.checkNotNull(bundle);+ @Constructor+ SetSchemaResponse(+ @Param(id = 1) @NonNull List deletedTypes,+ @Param(id = 2) @NonNull List incompatibleTypes,+ @Param(id = 3) @NonNull List migratedTypes) {+ mDeletedTypes = deletedTypes;+ mIncompatibleTypes = incompatibleTypes;+ mMigratedTypes = migratedTypes;+ mMigrationFailures = Collections.emptyList();+ }++ SetSchemaResponse(+ @NonNull List deletedTypes,+ @NonNull List incompatibleTypes,+ @NonNull List migratedTypes,+ @NonNull List migrationFailures) {+ mDeletedTypes = deletedTypes;+ mIncompatibleTypes = incompatibleTypes;+ mMigratedTypes = migratedTypes; mMigrationFailures = Preconditions.checkNotNull(migrationFailures); }- SetSchemaResponse(@NonNull Bundle bundle) {- this(bundle, /*migrationFailures=*/ Collections.emptyList());- }-- /**- * Returns the {@link Bundle} populated by this builder.- * @exportToFramework:hide- */- @NonNull- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- public Bundle getBundle() {- return mBundle;- }- /** * Returns a {@link List} of all failed {@link MigrationFailure}. *@@ -109,11 +129,10 @@
*/ @NonNull public Set getDeletedTypes() {- if (mDeletedTypes == null) {- mDeletedTypes = new ArraySet<>(- Preconditions.checkNotNull(mBundle.getStringArrayList(DELETED_TYPES_FIELD)));+ if (mDeletedTypesCached == null) {+ mDeletedTypesCached = new ArraySet<>(Preconditions.checkNotNull(mDeletedTypes)); }- return Collections.unmodifiableSet(mDeletedTypes);+ return Collections.unmodifiableSet(mDeletedTypesCached); } /**@@ -131,11 +150,10 @@
*/ @NonNull public Set getMigratedTypes() {- if (mMigratedTypes == null) {- mMigratedTypes = new ArraySet<>(- Preconditions.checkNotNull(mBundle.getStringArrayList(MIGRATED_TYPES_FIELD)));+ if (mMigratedTypesCached == null) {+ mMigratedTypesCached = new ArraySet<>(Preconditions.checkNotNull(mMigratedTypes)); }- return Collections.unmodifiableSet(mMigratedTypes);+ return Collections.unmodifiableSet(mMigratedTypesCached); } /**@@ -151,27 +169,11 @@
*/ @NonNull public Set getIncompatibleTypes() {- if (mIncompatibleTypes == null) {- mIncompatibleTypes = new ArraySet<>(- Preconditions.checkNotNull(- mBundle.getStringArrayList(INCOMPATIBLE_TYPES_FIELD)));+ if (mIncompatibleTypesCached == null) {+ mIncompatibleTypesCached =+ new ArraySet<>(Preconditions.checkNotNull(mIncompatibleTypes)); }- return Collections.unmodifiableSet(mIncompatibleTypes);- }-- /**- * Translates the {@link SetSchemaResponse}'s bundle to {@link Builder}.- * @exportToFramework:hide- */- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- @NonNull- // TODO(b/179302942) change to Builder(mBundle) powered by mBundle.deepCopy- public Builder toBuilder() {- return new Builder()- .addDeletedTypes(getDeletedTypes())- .addIncompatibleTypes(getIncompatibleTypes())- .addMigratedTypes(getMigratedTypes())- .addMigrationFailures(mMigrationFailures);+ return Collections.unmodifiableSet(mIncompatibleTypesCached); } /** Builder for {@link SetSchemaResponse} objects. */@@ -182,6 +184,23 @@
private ArrayList mIncompatibleTypes = new ArrayList<>(); private boolean mBuilt = false;+ /**+ * Creates a new {@link SetSchemaResponse.Builder} from the given SetSchemaResponse.+ *+ * @exportToFramework:hide+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ public Builder(@NonNull SetSchemaResponse setSchemaResponse) {+ Preconditions.checkNotNull(setSchemaResponse);+ mDeletedTypes.addAll(setSchemaResponse.getDeletedTypes());+ mIncompatibleTypes.addAll(setSchemaResponse.getIncompatibleTypes());+ mMigratedTypes.addAll(setSchemaResponse.getMigratedTypes());+ mMigrationFailures.addAll(setSchemaResponse.getMigrationFailures());+ }++ /** Create a {@link Builder} object} */+ public Builder() {}+ /** Adds {@link MigrationFailure}s to the list of migration failures. */ @CanIgnoreReturnValue @NonNull@@ -266,15 +285,15 @@
/** Builds a {@link SetSchemaResponse} object. */ @NonNull public SetSchemaResponse build() {- Bundle bundle = new Bundle();- bundle.putStringArrayList(INCOMPATIBLE_TYPES_FIELD, mIncompatibleTypes);- bundle.putStringArrayList(DELETED_TYPES_FIELD, mDeletedTypes);- bundle.putStringArrayList(MIGRATED_TYPES_FIELD, mMigratedTypes); mBuilt = true; // Avoid converting the potential thousands of MigrationFailures to Pracelable and // back just for put in bundle. In platform, we should set MigrationFailures in // AppSearchSession after we pass SetSchemaResponse via binder.- return new SetSchemaResponse(bundle, mMigrationFailures);+ return new SetSchemaResponse(+ mDeletedTypes,+ mIncompatibleTypes,+ mMigratedTypes,+ mMigrationFailures); } private void resetIfBuilt() {@@ -288,18 +307,50 @@
} }+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @Override+ public void writeToParcel(@NonNull Parcel dest, int flags) {+ SetSchemaResponseCreator.writeToParcel(this, dest, flags);+ }+ /** * The class represents a post-migrated {@link GenericDocument} that failed to be saved by * {@link AppSearchSession#setSchemaAsync}. */- public static class MigrationFailure {- private static final String SCHEMA_TYPE_FIELD = "schemaType";- private static final String NAMESPACE_FIELD = "namespace";- private static final String DOCUMENT_ID_FIELD = "id";- private static final String ERROR_MESSAGE_FIELD = "errorMessage";- private static final String RESULT_CODE_FIELD = "resultCode";+ @SafeParcelable.Class(creator = "MigrationFailureCreator")+ @SuppressWarnings("HiddenSuperclass")+ public static class MigrationFailure extends AbstractSafeParcelable {+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @NonNull+ public static final Parcelable.Creator CREATOR =+ new MigrationFailureCreator();- private final Bundle mBundle;+ @Field(id = 1, getter = "getNamespace")+ private final String mNamespace;+ @Field(id = 2, getter = "getDocumentId")+ private final String mDocumentId;+ @Field(id = 3, getter = "getSchemaType")+ private final String mSchemaType;+ @Field(id = 4)+ @Nullable final String mErrorMessage;+ @Field(id = 5)+ final int mResultCode;++ @Constructor+ MigrationFailure(+ @Param(id = 1) @NonNull String namespace,+ @Param(id = 2) @NonNull String documentId,+ @Param(id = 3) @NonNull String schemaType,+ @Param(id = 4) @Nullable String errorMessage,+ @Param(id = 5) int resultCode) {+ mNamespace = namespace;+ mDocumentId = documentId;+ mSchemaType = schemaType;+ mErrorMessage = errorMessage;+ mResultCode = resultCode;+ } /** * Constructs a new {@link MigrationFailure}.@@ -315,49 +366,33 @@
@NonNull String documentId, @NonNull String schemaType, @NonNull AppSearchResult> failedResult) {- mBundle = new Bundle();- mBundle.putString(NAMESPACE_FIELD, Preconditions.checkNotNull(namespace));- mBundle.putString(DOCUMENT_ID_FIELD, Preconditions.checkNotNull(documentId));- mBundle.putString(SCHEMA_TYPE_FIELD, Preconditions.checkNotNull(schemaType));+ mNamespace = namespace;+ mDocumentId = documentId;+ mSchemaType = schemaType; Preconditions.checkNotNull(failedResult); Preconditions.checkArgument( !failedResult.isSuccess(), "failedResult was actually successful");- mBundle.putString(ERROR_MESSAGE_FIELD, failedResult.getErrorMessage());- mBundle.putInt(RESULT_CODE_FIELD, failedResult.getResultCode());- }-- MigrationFailure(@NonNull Bundle bundle) {- mBundle = Preconditions.checkNotNull(bundle);- }-- /**- * Returns the Bundle of the {@link MigrationFailure}.- *- * @exportToFramework:hide- */- @NonNull- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)- public Bundle getBundle() {- return mBundle;+ mErrorMessage = failedResult.getErrorMessage();+ mResultCode = failedResult.getResultCode(); } /** Returns the namespace of the {@link GenericDocument} that failed to be migrated. */ @NonNull public String getNamespace() {- return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/"");+ return mNamespace; } /** Returns the id of the {@link GenericDocument} that failed to be migrated. */ @NonNull public String getDocumentId() {- return mBundle.getString(DOCUMENT_ID_FIELD, /*defaultValue=*/"");+ return mDocumentId; } /** Returns the schema type of the {@link GenericDocument} that failed to be migrated. */ @NonNull public String getSchemaType() {- return mBundle.getString(SCHEMA_TYPE_FIELD, /*defaultValue=*/"");+ return mSchemaType; } /**@@ -366,8 +401,7 @@
*/ @NonNull public AppSearchResult getAppSearchResult() {- return AppSearchResult.newFailedResult(mBundle.getInt(RESULT_CODE_FIELD),- mBundle.getString(ERROR_MESSAGE_FIELD, /*defaultValue=*/""));+ return AppSearchResult.newFailedResult(mResultCode, mErrorMessage); } @NonNull@@ -377,5 +411,12 @@
+ getNamespace() + ", documentId: " + getDocumentId() + ", appSearchResult: " + getAppSearchResult().toString() + "}"; }++ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @Override+ public void writeToParcel(@NonNull Parcel dest, int flags) {+ MigrationFailureCreator.writeToParcel(this, dest, flags);+ } } }
@@ -1,446 +0,0 @@
-/*- * Copyright (C) 2021 The Android Open Source Project- *- * Licensed under the Apache License, Version 2.0 (the "License");- * you may not use this file except in compliance with the License.- * You may obtain a copy of the License at- *- * http://www.apache.org/licenses/LICENSE-2.0- *- * Unless required by applicable law or agreed to in writing, software- * distributed under the License is distributed on an "AS IS" BASIS,- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.- * See the License for the specific language governing permissions and- * limitations under the License.- */-package androidx.appsearch.app;--import android.os.Parcel;--import androidx.annotation.NonNull;-import androidx.annotation.Nullable;-import androidx.annotation.RestrictTo;-import androidx.appsearch.annotation.CanIgnoreReturnValue;-import androidx.appsearch.safeparcel.AbstractSafeParcelable;-import androidx.appsearch.safeparcel.SafeParcelable;-import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityDocumentCreator;-import androidx.collection.ArraySet;--import java.util.ArrayList;-import java.util.Arrays;-import java.util.Collections;-import java.util.List;-import java.util.Map;-import java.util.Objects;-import java.util.Set;--/**- * Holds the visibility settings that apply to a schema type.- * @exportToFramework:hide- */-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)[email protected](creator = "VisibilityDocumentCreator")-public final class VisibilityDocument extends AbstractSafeParcelable {- @NonNull- public static final VisibilityDocumentCreator CREATOR = new VisibilityDocumentCreator();-- /**- * The Schema type for documents that hold AppSearch's metadata, such as visibility settings.- */- public static final String SCHEMA_TYPE = "VisibilityType";- /** Namespace of documents that contain visibility settings */- public static final String NAMESPACE = "";-- /**- * Property that holds the list of platform-hidden schemas, as part of the visibility settings.- */- private static final String NOT_DISPLAYED_BY_SYSTEM_PROPERTY = "notPlatformSurfaceable";-- /** Property that holds the package name that can access a schema. */- private static final String PACKAGE_NAME_PROPERTY = "packageName";-- /** Property that holds the SHA 256 certificate of the app that can access a schema. */- private static final String SHA_256_CERT_PROPERTY = "sha256Cert";-- /** Property that holds the required permissions to access the schema. */- private static final String PERMISSION_PROPERTY = "permission";-- // The initial schema version, one VisibilityDocument contains all visibility information for- // whole package.- public static final int SCHEMA_VERSION_DOC_PER_PACKAGE = 0;-- // One VisibilityDocument contains visibility information for a single schema.- public static final int SCHEMA_VERSION_DOC_PER_SCHEMA = 1;-- // One VisibilityDocument contains visibility information for a single schema.- public static final int SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA = 2;-- public static final int SCHEMA_VERSION_LATEST = SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA;-- /**- * Schema for the VisibilityStore's documents.- *- *
NOTE: If you update this, also update {@link #SCHEMA_VERSION_LATEST}.
- */- public static final AppSearchSchema- SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)- .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(- NOT_DISPLAYED_BY_SYSTEM_PROPERTY)- .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)- .build())- .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(PACKAGE_NAME_PROPERTY)- .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)- .build())- .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(SHA_256_CERT_PROPERTY)- .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)- .build())- .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(PERMISSION_PROPERTY,- VisibilityPermissionDocument.SCHEMA_TYPE)- .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)- .build())- .build();-- @NonNull- @Field(id = 1, getter = "getId")- private String mId;-- @Field(id = 2, getter = "isNotDisplayedBySystem")- private final boolean mIsNotDisplayedBySystem;-- @NonNull- @Field(id = 3, getter = "getPackageNames")- private final String[] mPackageNames;-- @NonNull- @Field(id = 4, getter = "getSha256Certs")- private final byte[][] mSha256Certs;-- @Nullable- @Field(id = 5, getter = "getPermissionDocuments")- private final VisibilityPermissionDocument[] mPermissionDocuments;-- @Nullable- // We still need to convert this class to a GenericDocument until we completely treat it- // differently in AppSearchImpl.- // TODO(b/298118943) Remove this once internally we don't use GenericDocument to store- // visibility information.- private GenericDocument mGenericDocument;-- @Nullable- private Integer mHashCode;-- @Constructor- VisibilityDocument(- @Param(id = 1) @NonNull String id,- @Param(id = 2) boolean isNotDisplayedBySystem,- @Param(id = 3) @NonNull String[] packageNames,- @Param(id = 4) @NonNull byte[][] sha256Certs,- @Param(id = 5) @Nullable VisibilityPermissionDocument[] permissionDocuments) {- mId = Objects.requireNonNull(id);- mIsNotDisplayedBySystem = isNotDisplayedBySystem;- mPackageNames = Objects.requireNonNull(packageNames);- mSha256Certs = Objects.requireNonNull(sha256Certs);- mPermissionDocuments = permissionDocuments;- }-- /**- * Gets the id for this VisibilityDocument.- *- *
This is being used as the document id when we convert a {@link VisibilityDocument}
- * to a {@link GenericDocument}.- */- @NonNull- public String getId() {- return mId;- }-- /** Returns whether this schema is visible to the system. */- public boolean isNotDisplayedBySystem() {- return mIsNotDisplayedBySystem;- }-- /**- * Returns a package name array which could access this schema. Use {@link #getSha256Certs()} to- * get package's sha 256 certs. The same index of package names array and sha256Certs array- * represents same package.- */- @NonNull- public String[] getPackageNames() {- return mPackageNames;- }-- /**- * Returns a package sha256Certs array which could access this schema. Use {@link- * #getPackageNames()} to get package's name. The same index of package names array and- * sha256Certs array represents same package.- */- @NonNull- public byte[][] getSha256Certs() {- return mSha256Certs;- }-- /** Gets a list of {@link VisibilityDocument}.- *- *
A {@link VisibilityDocument} holds all required permissions for the caller need to have
- * to access the schema this {@link VisibilityDocument} presents.- */- @Nullable- VisibilityPermissionDocument[] getPermissionDocuments() {- return mPermissionDocuments;- }-- /**- * Returns an array of Android Permissions that caller mush hold to access the schema this- * {@link VisibilityDocument} represents.- */- @NonNull- public Set> getVisibleToPermissions() {- if (mPermissionDocuments == null) {- return Collections.emptySet();- }- Set> visibleToPermissions = new ArraySet<>(mPermissionDocuments.length);- for (VisibilityPermissionDocument permissionDocument : mPermissionDocuments) {- Set requiredPermissions = permissionDocument.getAllRequiredPermissions();- if (requiredPermissions != null) {- visibleToPermissions.add(requiredPermissions);- }- }- return visibleToPermissions;- }-- /** Build the List of {@link VisibilityDocument} from visibility settings. */- @NonNull- public static List toVisibilityDocuments(- @NonNull SetSchemaRequest setSchemaRequest) {- Set searchSchemas = setSchemaRequest.getSchemas();- Set schemasNotDisplayedBySystem = setSchemaRequest.getSchemasNotDisplayedBySystem();- Map> schemasVisibleToPackages =- setSchemaRequest.getSchemasVisibleToPackages();- Map>> schemasVisibleToPermissions =- setSchemaRequest.getRequiredPermissionsForSchemaTypeVisibility();- List visibilityDocuments = new ArrayList<>(searchSchemas.size());- for (AppSearchSchema searchSchema : searchSchemas) {- String schemaType = searchSchema.getSchemaType();- VisibilityDocument.Builder documentBuilder =- new VisibilityDocument.Builder(/*id=*/ searchSchema.getSchemaType());- documentBuilder.setNotDisplayedBySystem(- schemasNotDisplayedBySystem.contains(schemaType));-- if (schemasVisibleToPackages.containsKey(schemaType)) {- documentBuilder.addVisibleToPackages(schemasVisibleToPackages.get(schemaType));- }-- if (schemasVisibleToPermissions.containsKey(schemaType)) {- documentBuilder.setVisibleToPermissions(- schemasVisibleToPermissions.get(schemaType));- }- visibilityDocuments.add(documentBuilder.build());- }- return visibilityDocuments;- }-- /**- * Generates a {@link GenericDocument} from the current class.- *- *
This conversion is needed until we don't treat Visibility related documents as
- * {@link GenericDocument}s internally.- */- @NonNull- public GenericDocument toGenericDocument() {- if (mGenericDocument == null) {- GenericDocument.Builder> builder = new GenericDocument.Builder<>(- NAMESPACE, mId, SCHEMA_TYPE);- builder.setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY, mIsNotDisplayedBySystem);- builder.setPropertyString(PACKAGE_NAME_PROPERTY, mPackageNames);- builder.setPropertyBytes(SHA_256_CERT_PROPERTY, mSha256Certs);-- // Generate an array of GenericDocument for VisibilityPermissionDocument.- if (mPermissionDocuments != null) {- GenericDocument[] permissionGenericDocs =- new GenericDocument[mPermissionDocuments.length];- for (int i = 0; i < mPermissionDocuments.length; ++i) {- permissionGenericDocs[i] = mPermissionDocuments[i].toGenericDocument();- }- builder.setPropertyDocument(PERMISSION_PROPERTY, permissionGenericDocs);- }-- // The creationTimestamp doesn't matter for Visibility documents.- // But to make tests pass, we set it 0 so two GenericDocuments generated from- // the same VisibilityDocument can be same.- builder.setCreationTimestampMillis(0L);-- mGenericDocument = builder.build();- }- return mGenericDocument;- }-- @Override- public int hashCode() {- if (mHashCode == null) {- mHashCode = Objects.hash(- mId,- mIsNotDisplayedBySystem,- Arrays.hashCode(mPackageNames),- Arrays.deepHashCode(mSha256Certs),- Arrays.hashCode(mPermissionDocuments));- }- return mHashCode;- }-- @Override- public boolean equals(@Nullable Object other) {- if (this == other) {- return true;- }- if (!(other instanceof VisibilityDocument)) {- return false;- }- VisibilityDocument otherVisibilityDocument = (VisibilityDocument) other;- return mId.equals(otherVisibilityDocument.mId)- && mIsNotDisplayedBySystem == otherVisibilityDocument.mIsNotDisplayedBySystem- && Arrays.equals(- mPackageNames, otherVisibilityDocument.mPackageNames)- && Arrays.deepEquals(- mSha256Certs, otherVisibilityDocument.mSha256Certs)- && Arrays.equals(- mPermissionDocuments, otherVisibilityDocument.mPermissionDocuments);- }-- @Override- public void writeToParcel(@NonNull Parcel dest, int flags) {- VisibilityDocumentCreator.writeToParcel(this, dest, flags);- }-- /** Builder for {@link VisibilityDocument}. */- public static final class Builder {- private final Set mPackageIdentifiers = new ArraySet<>();- private String mId;- private boolean mIsNotDisplayedBySystem;- private VisibilityPermissionDocument[] mPermissionDocuments;-- /**- * Creates a {@link Builder} for a {@link VisibilityDocument}.- *- * @param id The SchemaType of the {@link AppSearchSchema} that this {@link- * VisibilityDocument} represents. The package and database prefix will be added in- * server side. We are using prefixed schema type to be the final id of this {@link- * VisibilityDocument}.- */- public Builder(@NonNull String id) {- mId = Objects.requireNonNull(id);- }-- /**- * Constructs a {@link VisibilityDocument} from a {@link GenericDocument}.- *- *
This constructor is still needed until we don't treat Visibility related documents as
- * {@link GenericDocument}s internally.- */- public Builder(@NonNull GenericDocument genericDocument) {- Objects.requireNonNull(genericDocument);-- mId = genericDocument.getId();- mIsNotDisplayedBySystem = genericDocument.getPropertyBoolean(- NOT_DISPLAYED_BY_SYSTEM_PROPERTY);-- String[] packageNames = genericDocument.getPropertyStringArray(PACKAGE_NAME_PROPERTY);- byte[][] sha256Certs = genericDocument.getPropertyBytesArray(SHA_256_CERT_PROPERTY);- for (int i = 0; i < packageNames.length; ++i) {- mPackageIdentifiers.add(new PackageIdentifier(packageNames[i], sha256Certs[i]));- }-- GenericDocument[] permissionDocs =- genericDocument.getPropertyDocumentArray(PERMISSION_PROPERTY);- if (permissionDocs != null) {- mPermissionDocuments = new VisibilityPermissionDocument[permissionDocs.length];- for (int i = 0; i < permissionDocs.length; ++i) {- mPermissionDocuments[i] = new VisibilityPermissionDocument.Builder(- permissionDocs[i]).build();- }- }- }-- public Builder(@NonNull VisibilityDocument visibilityDocument) {- Objects.requireNonNull(visibilityDocument);-- mIsNotDisplayedBySystem = visibilityDocument.mIsNotDisplayedBySystem;- mPermissionDocuments = visibilityDocument.mPermissionDocuments;- for (int i = 0; i < visibilityDocument.mPackageNames.length; ++i) {- mPackageIdentifiers.add(new PackageIdentifier(visibilityDocument.mPackageNames[i],- visibilityDocument.mSha256Certs[i]));- }- }-- /** Sets id. */- @CanIgnoreReturnValue- @NonNull- public Builder setId(@NonNull String id) {- mId = Objects.requireNonNull(id);- return this;- }-- /** Sets whether this schema has opted out of platform surfacing. */- @CanIgnoreReturnValue- @NonNull- public Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {- mIsNotDisplayedBySystem = notDisplayedBySystem;- return this;- }-- /** Add {@link PackageIdentifier} of packages which has access to this schema. */- @CanIgnoreReturnValue- @NonNull- public Builder addVisibleToPackages(@NonNull Set packageIdentifiers) {- mPackageIdentifiers.addAll(Objects.requireNonNull(packageIdentifiers));- return this;- }-- /** Add {@link PackageIdentifier} of packages which has access to this schema. */- @CanIgnoreReturnValue- @NonNull- public Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {- mPackageIdentifiers.add(Objects.requireNonNull(packageIdentifier));- return this;- }-- /**- * Sets required permission sets for a package needs to hold to the schema this {@link- * VisibilityDocument} represents.- *- *
The querier could have access if they holds ALL required permissions of ANY of the
- * individual value sets.- */- @CanIgnoreReturnValue- @NonNull- public Builder setVisibleToPermissions(@NonNull Set> visibleToPermissions) {- Objects.requireNonNull(visibleToPermissions);- mPermissionDocuments =- new VisibilityPermissionDocument[visibleToPermissions.size()];- int i = 0;- for (Set allRequiredPermissions : visibleToPermissions) {- mPermissionDocuments[i++] =- new VisibilityPermissionDocument.Builder(- NAMESPACE, /*id=*/ String.valueOf(i))- .setVisibleToAllRequiredPermissions(allRequiredPermissions)- .build();- }- return this;- }-- /** Build a {@link VisibilityDocument} */- @NonNull- public VisibilityDocument build() {- String[] packageNames = new String[mPackageIdentifiers.size()];- byte[][] sha256Certs = new byte[mPackageIdentifiers.size()][32];- int i = 0;- for (PackageIdentifier packageIdentifier : mPackageIdentifiers) {- packageNames[i] = packageIdentifier.getPackageName();- sha256Certs[i] = packageIdentifier.getSha256Certificate();- ++i;- }- return new VisibilityDocument(mId, mIsNotDisplayedBySystem,- packageNames, sha256Certs, mPermissionDocuments);- }- }-}-
@@ -0,0 +1,185 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.app;++import android.os.Parcel;+import android.os.Parcelable;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RestrictTo;+import androidx.appsearch.safeparcel.AbstractSafeParcelable;+import androidx.appsearch.safeparcel.SafeParcelable;+import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityPermissionConfigCreator;+import androidx.collection.ArraySet;++import java.util.Arrays;+import java.util.Objects;+import java.util.Set;++/**+ * The config class that holds all required permissions for a caller need to hold to access the+ * schema which the outer {@link SchemaVisibilityConfig} represents.+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)[email protected](creator = "VisibilityPermissionConfigCreator")+public final class VisibilityPermissionConfig extends AbstractSafeParcelable {+ @NonNull+ public static final Parcelable.Creator CREATOR =+ new VisibilityPermissionConfigCreator();++ /**+ * The Schema type for documents that hold AppSearch's metadata, such as visibility settings.+ */+ public static final String SCHEMA_TYPE = "VisibilityPermissionType";++ /** Property that holds the required permissions to access the schema. */+ public static final String ALL_REQUIRED_PERMISSIONS_PROPERTY = "allRequiredPermissions";++ /**+ * Schema for the VisibilityStore's documents.+ *+ *
NOTE: If you update this, also update schema version number in
+ * VisibilityToDocumentConverter+ */+ public static final AppSearchSchema+ SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)+ .addProperty(new AppSearchSchema.LongPropertyConfig+ .Builder(ALL_REQUIRED_PERMISSIONS_PROPERTY)+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)+ .build())+ .build();++ @Nullable+ @Field(id = 1)+ final int[] mAllRequiredPermissions;++ @Nullable+ // We still need to convert this class to a GenericDocument until we completely treat it+ // differently in AppSearchImpl.+ // TODO(b/298118943) Remove this once internally we don't use GenericDocument to store+ // visibility information.+ private GenericDocument mGenericDocument;++ @Nullable+ private Integer mHashCode;++ @Constructor+ VisibilityPermissionConfig(@Param(id = 1) @Nullable int[] allRequiredPermissions) {+ mAllRequiredPermissions = allRequiredPermissions;+ }++ /**+ * Sets a set of Android Permissions that caller must hold to access the schema that the+ * outer {@link SchemaVisibilityConfig} represents.+ */+ public VisibilityPermissionConfig(@NonNull Set allRequiredPermissions) {+ mAllRequiredPermissions = toInts(Objects.requireNonNull(allRequiredPermissions));+ }++ /**+ * Returns an array of Android Permissions that caller mush hold to access the schema that the+ * outer {@link SchemaVisibilityConfig} represents.+ */+ @Nullable+ public Set getAllRequiredPermissions() {+ return toIntegerSet(mAllRequiredPermissions);+ }++ @NonNull+ private static int[] toInts(@NonNull Set properties) {+ int[] outputs = new int[properties.size()];+ int i = 0;+ for (int property : properties) {+ outputs[i++] = property;+ }+ return outputs;+ }++ @Nullable+ private static Set toIntegerSet(@Nullable int[] properties) {+ if (properties == null) {+ return null;+ }+ Set outputs = new ArraySet<>(properties.length);+ for (int property : properties) {+ outputs.add(property);+ }+ return outputs;+ }++ /**+ * Generates a {@link GenericDocument} from the current class.+ *+ *
This conversion is needed until we don't treat Visibility related documents as
+ * {@link GenericDocument}s internally.+ */+ @NonNull+ public GenericDocument toGenericDocument() {+ if (mGenericDocument == null) {+ // This is used as a nested document, we do not need a namespace or id.+ GenericDocument.Builder> builder = new GenericDocument.Builder<>(+ /*namespace=*/"", /*id=*/"", SCHEMA_TYPE);++ if (mAllRequiredPermissions != null) {+ // GenericDocument only supports long, so int[] needs to be converted to+ // long[] here.+ long[] longs = new long[mAllRequiredPermissions.length];+ for (int i = 0; i < mAllRequiredPermissions.length; ++i) {+ longs[i] = mAllRequiredPermissions[i];+ }+ builder.setPropertyLong(ALL_REQUIRED_PERMISSIONS_PROPERTY, longs);+ }++ // The creationTimestamp doesn't matter for Visibility documents.+ // But to make tests pass, we set it 0 so two GenericDocuments generated from+ // the same VisibilityPermissionConfig can be same.+ builder.setCreationTimestampMillis(0L);++ mGenericDocument = builder.build();+ }+ return mGenericDocument;+ }++ @Override+ public int hashCode() {+ if (mHashCode == null) {+ mHashCode = Arrays.hashCode(mAllRequiredPermissions);+ }+ return mHashCode;+ }++ @Override+ public boolean equals(@Nullable Object other) {+ if (this == other) {+ return true;+ }+ if (!(other instanceof VisibilityPermissionConfig)) {+ return false;+ }+ VisibilityPermissionConfig otherVisibilityPermissionConfig =+ (VisibilityPermissionConfig) other;+ return Arrays.equals(mAllRequiredPermissions,+ otherVisibilityPermissionConfig.mAllRequiredPermissions);+ }++ @Override+ public void writeToParcel(@NonNull Parcel dest, int flags) {+ VisibilityPermissionConfigCreator.writeToParcel(this, dest, flags);+ }+}
@@ -1,269 +0,0 @@
-/*- * Copyright 2022 The Android Open Source Project- *- * Licensed under the Apache License, Version 2.0 (the "License");- * you may not use this file except in compliance with the License.- * You may obtain a copy of the License at- *- * http://www.apache.org/licenses/LICENSE-2.0- *- * Unless required by applicable law or agreed to in writing, software- * distributed under the License is distributed on an "AS IS" BASIS,- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.- * See the License for the specific language governing permissions and- * limitations under the License.- */--package androidx.appsearch.app;--import android.os.Parcel;--import androidx.annotation.NonNull;-import androidx.annotation.Nullable;-import androidx.annotation.RestrictTo;-import androidx.appsearch.annotation.CanIgnoreReturnValue;-import androidx.appsearch.safeparcel.AbstractSafeParcelable;-import androidx.appsearch.safeparcel.SafeParcelable;-import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityPermissionDocumentCreator;-import androidx.collection.ArraySet;--import java.util.Arrays;-import java.util.Objects;-import java.util.Set;--/**- * The nested document that holds all required permissions for a caller need to hold to access the- * schema which the outer {@link VisibilityDocument} represents.- * @exportToFramework:hide- */-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)[email protected](creator = "VisibilityPermissionDocumentCreator")-public final class VisibilityPermissionDocument extends AbstractSafeParcelable {- @NonNull- public static final VisibilityPermissionDocumentCreator CREATOR =- new VisibilityPermissionDocumentCreator();-- /**- * The Schema type for documents that hold AppSearch's metadata, such as visibility settings.- */- public static final String SCHEMA_TYPE = "VisibilityPermissionType";-- /** Property that holds the required permissions to access the schema. */- private static final String ALL_REQUIRED_PERMISSIONS_PROPERTY = "allRequiredPermissions";-- /**- * Schema for the VisibilityStore's documents.- *- *
NOTE: If you update this, also update {@link VisibilityDocument#SCHEMA_VERSION_LATEST}.
- */- public static final AppSearchSchema- SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)- .addProperty(new AppSearchSchema.LongPropertyConfig- .Builder(ALL_REQUIRED_PERMISSIONS_PROPERTY)- .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)- .build())- .build();-- @NonNull- @Field(id = 1, getter = "getId")- private final String mId;-- @NonNull- @Field(id = 2, getter = "getNamespace")- private final String mNamespace;-- @Nullable- @Field(id = 3, getter = "getAllRequiredPermissionsInts")- // SafeParcelable doesn't support Set, so we have to convert it to int[].- private final int[] mAllRequiredPermissions;-- @Nullable- // We still need to convert this class to a GenericDocument until we completely treat it- // differently in AppSearchImpl.- // TODO(b/298118943) Remove this once internally we don't use GenericDocument to store- // visibility information.- private GenericDocument mGenericDocument;-- @Nullable- private Integer mHashCode;-- @Constructor- VisibilityPermissionDocument(- @Param(id = 1) @NonNull String id,- @Param(id = 2) @NonNull String namespace,- @Param(id = 3) @Nullable int[] allRequiredPermissions) {- mId = Objects.requireNonNull(id);- mNamespace = Objects.requireNonNull(namespace);- mAllRequiredPermissions = allRequiredPermissions;- }-- /**- * Gets the id for this {@link VisibilityPermissionDocument}.- *- *
This is being used as the document id when we convert a
- * {@link VisibilityPermissionDocument} to a {@link GenericDocument}.- */- @NonNull- public String getId() {- return mId;- }-- /**- * Gets the namespace for this {@link VisibilityPermissionDocument}.- *- *
This is being used as the namespace when we convert a
- * {@link VisibilityPermissionDocument} to a {@link GenericDocument}.- */- @NonNull- public String getNamespace() {- return mNamespace;- }-- /** Gets the required Android Permissions in an int array. */- @Nullable- int[] getAllRequiredPermissionsInts() {- return mAllRequiredPermissions;- }-- /**- * Returns an array of Android Permissions that caller mush hold to access the schema that the- * outer {@link VisibilityDocument} represents.- */- @Nullable- public Set getAllRequiredPermissions() {- return toIntegerSet(mAllRequiredPermissions);- }-- @NonNull- private static int[] toInts(@NonNull Set properties) {- int[] outputs = new int[properties.size()];- int i = 0;- for (int property : properties) {- outputs[i++] = property;- }- return outputs;- }-- @Nullable- private static Set toIntegerSet(@Nullable int[] properties) {- if (properties == null) {- return null;- }- Set outputs = new ArraySet<>(properties.length);- for (int property : properties) {- outputs.add(property);- }- return outputs;- }-- /**- * Generates a {@link GenericDocument} from the current class.- *- *
This conversion is needed until we don't treat Visibility related documents as
- * {@link GenericDocument}s internally.- */- @NonNull- public GenericDocument toGenericDocument() {- if (mGenericDocument == null) {- GenericDocument.Builder> builder = new GenericDocument.Builder<>(- mNamespace, mId, SCHEMA_TYPE);-- if (mAllRequiredPermissions != null) {- // GenericDocument only supports long, so int[] needs to be converted to- // long[] here.- long[] longs = new long[mAllRequiredPermissions.length];- for (int i = 0; i < mAllRequiredPermissions.length; ++i) {- longs[i] = mAllRequiredPermissions[i];- }- builder.setPropertyLong(ALL_REQUIRED_PERMISSIONS_PROPERTY, longs);- }-- mGenericDocument = builder.build();- }- return mGenericDocument;- }-- @Override- public int hashCode() {- if (mHashCode == null) {- mHashCode = Objects.hash(mId, mNamespace, Arrays.hashCode(mAllRequiredPermissions));- }- return mHashCode;- }-- @Override- public boolean equals(@Nullable Object other) {- if (this == other) {- return true;- }- if (!(other instanceof VisibilityPermissionDocument)) {- return false;- }- VisibilityPermissionDocument otherVisibilityPermissionDocument =- (VisibilityPermissionDocument) other;- return mId.equals(otherVisibilityPermissionDocument.mId)- && mNamespace.equals(otherVisibilityPermissionDocument.mNamespace)- && Arrays.equals(- mAllRequiredPermissions,- otherVisibilityPermissionDocument.mAllRequiredPermissions);- }-- @Override- public void writeToParcel(@NonNull Parcel dest, int flags) {- VisibilityPermissionDocumentCreator.writeToParcel(this, dest, flags);- }-- /** Builder for {@link VisibilityPermissionDocument}. */- public static final class Builder {- private String mId;- private String mNamespace;- private int[] mAllRequiredPermissions;-- /**- * Constructs a {@link VisibilityPermissionDocument} from a {@link GenericDocument}.- *- *
This constructor is still needed until we don't treat Visibility related documents as
- * {@link GenericDocument}s internally.- */- public Builder(@NonNull GenericDocument genericDocument) {- Objects.requireNonNull(genericDocument);- mId = genericDocument.getId();- mNamespace = genericDocument.getNamespace();- // GenericDocument only supports long[], so we need to convert it back to int[].- long[] longs = genericDocument.getPropertyLongArray(- ALL_REQUIRED_PERMISSIONS_PROPERTY);- if (longs != null) {- mAllRequiredPermissions = new int[longs.length];- for (int i = 0; i < longs.length; ++i) {- mAllRequiredPermissions[i] = (int) longs[i];- }- }- }-- /** Creates a {@link VisibilityDocument.Builder} for a {@link VisibilityDocument}. */- public Builder(@NonNull String namespace, @NonNull String id) {- mNamespace = Objects.requireNonNull(namespace);- mId = Objects.requireNonNull(id);- }-- /**- * Sets a set of Android Permissions that caller mush hold to access the schema that the- * outer {@link VisibilityDocument} represents.- */- @CanIgnoreReturnValue- @NonNull- public Builder setVisibleToAllRequiredPermissions(- @NonNull Set allRequiredPermissions) {- mAllRequiredPermissions = toInts(Objects.requireNonNull(allRequiredPermissions));- return this;- }-- /** Builds a {@link VisibilityPermissionDocument} */- @NonNull- public VisibilityPermissionDocument build() {- return new VisibilityPermissionDocument(mId,- mNamespace,- mAllRequiredPermissions);- }- }-}
@@ -0,0 +1,54 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.checker.initialization.qual;++import androidx.annotation.RestrictTo;++import java.lang.annotation.ElementType;+import java.lang.annotation.Retention;+import java.lang.annotation.RetentionPolicy;+import java.lang.annotation.Target;++// This is an annotation stub to avoid dependencies on annotations that aren't+// in the Android platform source tree.++/**+ * + */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+@Target({+ ElementType.ANNOTATION_TYPE,+ ElementType.CONSTRUCTOR,+ ElementType.FIELD,+ ElementType.LOCAL_VARIABLE,+ ElementType.METHOD,+ ElementType.PACKAGE,+ ElementType.PARAMETER,+ ElementType.TYPE,+ ElementType.TYPE_PARAMETER,+ ElementType.TYPE_USE})+@Retention(RetentionPolicy.SOURCE)+public @interface UnderInitialization {++ // These fields maintain API compatibility with annotations that expect arguments.++ String[] value() default {};++ boolean result() default false;++ String[] expression() default "";+}
@@ -0,0 +1,54 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.checker.initialization.qual;++import androidx.annotation.RestrictTo;++import java.lang.annotation.ElementType;+import java.lang.annotation.Retention;+import java.lang.annotation.RetentionPolicy;+import java.lang.annotation.Target;++// This is an annotation stub to avoid dependencies on annotations that aren't+// in the Android platform source tree.++/**+ * + */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+@Target({+ ElementType.ANNOTATION_TYPE,+ ElementType.CONSTRUCTOR,+ ElementType.FIELD,+ ElementType.LOCAL_VARIABLE,+ ElementType.METHOD,+ ElementType.PACKAGE,+ ElementType.PARAMETER,+ ElementType.TYPE,+ ElementType.TYPE_PARAMETER,+ ElementType.TYPE_USE})+@Retention(RetentionPolicy.SOURCE)+public @interface UnknownInitialization {++ // These fields maintain API compatibility with annotations that expect arguments.++ String[] value() default {};++ boolean result() default false;++ String[] expression() default "";+}
@@ -0,0 +1,55 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.checker.nullness.qual;+++// This is an annotation stub to avoid dependencies on annotations that aren't+// in the Android platform source tree.++import androidx.annotation.RestrictTo;++import java.lang.annotation.ElementType;+import java.lang.annotation.Retention;+import java.lang.annotation.RetentionPolicy;+import java.lang.annotation.Target;++/**+ * + */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+@Target({+ ElementType.ANNOTATION_TYPE,+ ElementType.CONSTRUCTOR,+ ElementType.FIELD,+ ElementType.LOCAL_VARIABLE,+ ElementType.METHOD,+ ElementType.PACKAGE,+ ElementType.PARAMETER,+ ElementType.TYPE,+ ElementType.TYPE_PARAMETER,+ ElementType.TYPE_USE})+@Retention(RetentionPolicy.SOURCE)+public @interface Nullable {++ // These fields maintain API compatibility with annotations that expect arguments.++ String[] value() default {};++ boolean result() default false;++ String[] expression() default "";+}
@@ -0,0 +1,54 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.checker.nullness.qual;++import androidx.annotation.RestrictTo;++import java.lang.annotation.ElementType;+import java.lang.annotation.Retention;+import java.lang.annotation.RetentionPolicy;+import java.lang.annotation.Target;++// This is an annotation stub to avoid dependencies on annotations that aren't+// in the Android platform source tree.++/**+ * + */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+@Target({+ ElementType.ANNOTATION_TYPE,+ ElementType.CONSTRUCTOR,+ ElementType.FIELD,+ ElementType.LOCAL_VARIABLE,+ ElementType.METHOD,+ ElementType.PACKAGE,+ ElementType.PARAMETER,+ ElementType.TYPE,+ ElementType.TYPE_PARAMETER,+ ElementType.TYPE_USE})+@Retention(RetentionPolicy.SOURCE)+public @interface RequiresNonNull {++ // These fields maintain API compatibility with annotations that expect arguments.++ String[] value() default {};++ boolean result() default false;++ String[] expression() default "";+}
@@ -27,7 +27,7 @@
* for propagating to the client. */ public class AppSearchException extends Exception {- private final @AppSearchResult.ResultCode int mResultCode;+ @AppSearchResult.ResultCode private final int mResultCode; /** * Initializes an {@link AppSearchException} with no message.
@@ -0,0 +1,31 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.flags;++import androidx.annotation.RestrictTo;++/**+ * Indicates an API is part of a feature that is guarded by an aconfig flag in the framework, and+ * only available if the flag is enabled.+ *+ *
Our own Jetpack version is created here for code sync purpose.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
new file mode 100644
index 0000000..5ad11f3
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
@@ -0,0 +1,242 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++// @exportToFramework:skipFile()+package androidx.appsearch.flags;+++import androidx.annotation.RestrictTo;+import androidx.appsearch.app.AppSearchSchema;++import java.util.Collection;++/**+ * Flags to control different features.+ *+ *
In Jetpack, those values can't be changed during runtime.
+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public final class Flags {+ private Flags() {+ }++ // The prefix of all the flags defined for AppSearch. The prefix has+ // "com.android.appsearch.flags", aka the package name for generated AppSearch flag classes in+ // the framework, plus an additional trailing '.'.+ private static final String FLAG_PREFIX =+ "com.android.appsearch.flags.";++ // The full string values for flags defined in the framework.+ //+ // The values of the static variables are the names of the flag defined in the framework's+ // aconfig files. E.g. "enable_safe_parcelable", with FLAG_PREFIX as the prefix.+ //+ // The name of the each static variable should be "FLAG_" + capitalized value of the flag.++ /** Enable SafeParcelable related features. */+ public static final String FLAG_ENABLE_SAFE_PARCELABLE_2 =+ FLAG_PREFIX + "enable_safe_parcelable_2";++ /** Enable the "hasProperty" function in list filter query expressions. */+ public static final String FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION =+ FLAG_PREFIX + "enable_list_filter_has_property_function";++ /** Enable the "tokenize" function in list filter query expressions. */+ public static final String FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION =+ FLAG_PREFIX + "enable_list_filter_tokenize_function";+++ /** Enable Schema Type Grouping related features. */+ public static final String FLAG_ENABLE_GROUPING_TYPE_PER_SCHEMA =+ FLAG_PREFIX + "enable_grouping_type_per_schema";++ /** Enable GenericDocument to take another GenericDocument to copy construct. */+ public static final String FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR =+ FLAG_PREFIX + "enable_generic_document_copy_constructor";++ /**+ * Enable the {@link androidx.appsearch.app.SearchSpec.Builder#addFilterProperties} and+ * {@link androidx.appsearch.app.SearchSuggestionSpec.Builder#addFilterProperties}.+ */+ public static final String FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES =+ FLAG_PREFIX + "enable_search_spec_filter_properties";+ /**+ * Enable the {@link androidx.appsearch.app.SearchSpec.Builder#setSearchSourceLogTag} method.+ */+ public static final String FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG =+ FLAG_PREFIX + "enable_search_spec_set_search_source_log_tag";++ /** Enable addTakenActions API in PutDocumentsRequest. */+ public static final String FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS =+ FLAG_PREFIX + "enable_put_documents_request_add_taken_actions";++ /** Enable setPubliclyVisibleSchema in SetSchemaRequest. */+ public static final String FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA = FLAG_PREFIX+ + "enable_set_publicly_visible_schema";++ /**+ * Enable {@link androidx.appsearch.app.GenericDocument.Builder} to use previously hidden+ * methods.+ */+ public static final String FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS = FLAG_PREFIX+ + "enable_generic_document_builder_hidden_methods";++ public static final String FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS = FLAG_PREFIX+ + "enable_set_schema_visible_to_configs";++ /** Enable {@link androidx.appsearch.app.EnterpriseGlobalSearchSession}. */+ public static final String FLAG_ENABLE_ENTERPRISE_GLOBAL_SEARCH_SESSION =+ FLAG_PREFIX + "enable_enterprise_global_search_session";++ /**+ * Enables {@link android.app.appsearch.functions.AppFunctionManager} and app functions related+ * stuff.+ */+ public static final String FLAG_ENABLE_APP_FUNCTIONS = FLAG_PREFIX + "enable_app_functions";++ /**+ * Enable {@link androidx.appsearch.app.AppSearchResult#RESULT_DENIED} and+ * {@link androidx.appsearch.app.AppSearchResult#RESULT_RATE_LIMITED} which were previously+ * hidden.+ */+ public static final String FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED =+ FLAG_PREFIX + "enable_result_denied_and_result_rate_limited";++ /**+ * Enables {@link AppSearchSchema#getParentTypes()},+ * {@link AppSearchSchema.DocumentPropertyConfig#getIndexableNestedProperties()} and variants of+ * {@link AppSearchSchema.DocumentPropertyConfig.Builder#addIndexableNestedProperties(Collection)}}.+ */+ public static final String FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES =+ FLAG_PREFIX + "enable_get_parent_types_and_indexable_nested_properties";++ /** Enables embedding search related APIs. */+ public static final String FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG =+ FLAG_PREFIX + "enable_schema_embedding_property_config";++ /** Enables informational ranking expressions. */+ public static final String FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS =+ FLAG_PREFIX + "enable_informational_ranking_expressions";++ // Whether the features should be enabled.+ //+ // In Jetpack, those should always return true.++ /** Whether SafeParcelable should be enabled. */+ public static boolean enableSafeParcelable() {+ return true;+ }++ /** Whether the "hasProperty" function in list filter query expressions should be enabled. */+ public static boolean enableListFilterHasPropertyFunction() {+ return true;+ }++ /** Whether Schema Type Grouping should be enabled. */+ public static boolean enableGroupingTypePerSchema() {+ return true;+ }++ /** Whether Generic Document Copy Constructing should be enabled. */+ public static boolean enableGenericDocumentCopyConstructor() {+ return true;+ }++ /**+ * Whether the {@link androidx.appsearch.app.SearchSpec.Builder#addFilterProperties} and+ * {@link androidx.appsearch.app.SearchSuggestionSpec.Builder#addFilterProperties} should be+ * enabled.+ */+ public static boolean enableSearchSpecFilterProperties() {+ return true;+ }++ /**+ * Whether the {@link androidx.appsearch.app.SearchSpec.Builder#setSearchSourceLogTag} should+ * be enabled.+ */+ public static boolean enableSearchSpecSetSearchSourceLogTag() {+ return true;+ }++ /** Whether addTakenActions API in PutDocumentsRequest should be enabled. */+ public static boolean enablePutDocumentsRequestAddTakenActions() {+ return true;+ }++ /** Whether setPubliclyVisibleSchema in SetSchemaRequest.Builder should be enabled. */+ public static boolean enableSetPubliclyVisibleSchema() {+ return true;+ }++ /**+ * Whether {@link androidx.appsearch.app.GenericDocument.Builder#setNamespace(String)},+ * {@link androidx.appsearch.app.GenericDocument.Builder#setId(String)},+ * {@link androidx.appsearch.app.GenericDocument.Builder#setSchemaType(String)}, and+ * {@link androidx.appsearch.app.GenericDocument.Builder#clearProperty(String)}+ * should be enabled.+ */+ public static boolean enableGenericDocumentBuilderHiddenMethods() {+ return true;+ }++ /**+ * Whether+ * {@link androidx.appsearch.app.SetSchemaRequest.Builder #setSchemaTypeVisibilityForConfigs}+ * should be enabled.+ */+ public static boolean enableSetSchemaVisibleToConfigs() {+ return true;+ }++ /** Whether {@link androidx.appsearch.app.EnterpriseGlobalSearchSession} should be enabled. */+ public static boolean enableEnterpriseGlobalSearchSession() {+ return true;+ }++ /**+ * Whether {@link androidx.appsearch.app.AppSearchResult#RESULT_DENIED} and+ * {@link androidx.appsearch.app.AppSearchResult#RESULT_RATE_LIMITED} should be enabled.+ */+ public static boolean enableResultDeniedAndResultRateLimited() {+ return true;+ }++ /**+ * Whether {@link AppSearchSchema#getParentTypes()},+ * {@link AppSearchSchema.DocumentPropertyConfig#getIndexableNestedProperties()} and variants of+ * {@link AppSearchSchema.DocumentPropertyConfig.Builder#addIndexableNestedProperties(Collection)}}+ * should be enabled.+ */+ public static boolean enableGetParentTypesAndIndexableNestedProperties() {+ return true;+ }++ /** Whether embedding search related APIs should be enabled. */+ public static boolean enableSchemaEmbeddingPropertyConfig() {+ return true;+ }++ /** Whether the "tokenize" function in list filter query expressions should be enabled. */+ public static boolean enableListFilterTokenizeFunction() {+ return true;+ }++ /** Whether informational ranking expressions should be enabled. */+ public static boolean enableInformationalRankingExpressions() {+ return true;+ }+}
@@ -211,4 +211,10 @@
*/ public void writeToParcel(@NonNull Parcel dest, int flags) { }++ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @Override+ public final int describeContents() {+ return 0;+ } }
@@ -16,20 +16,22 @@
package androidx.appsearch.safeparcel;+import android.annotation.SuppressLint; import android.os.Parcel;+import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo;-import androidx.annotation.VisibleForTesting; import androidx.appsearch.annotation.CanIgnoreReturnValue; import androidx.appsearch.app.AppSearchSchema; import androidx.appsearch.app.AppSearchSession;+import androidx.appsearch.app.EmbeddingVector; import androidx.appsearch.app.GenericDocument;-import androidx.appsearch.safeparcel.stub.StubCreators.GenericDocumentParcelCreator; import androidx.collection.ArrayMap;-import java.util.Arrays;+import java.util.ArrayList;+import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set;@@ -41,9 +43,11 @@
*/ @RestrictTo(RestrictTo.Scope.LIBRARY) @SafeParcelable.Class(creator = "GenericDocumentParcelCreator")-public final class GenericDocumentParcel extends AbstractSafeParcelable {+// This won't be used to send data over binder, and we have to use Parcelable for code sync purpose.+@SuppressLint("BanParcelableUsage")+public final class GenericDocumentParcel extends AbstractSafeParcelable implements Parcelable { @NonNull- public static final GenericDocumentParcelCreator CREATOR =+ public static final Parcelable.Creator CREATOR = new GenericDocumentParcelCreator(); /** The default score of document. */@@ -83,7 +87,15 @@
*/ @Field(id = 7, getter = "getProperties") @NonNull- private final PropertyParcel[] mProperties;+ private final List mProperties;++ /**+ * Contains all parent properties for this {@link GenericDocument} in a list.+ *+ */+ @Field(id = 8, getter = "getParentTypes")+ @Nullable+ private final List mParentTypes; /** * Contains all properties in {@link GenericDocument} to support getting properties via name@@ -110,9 +122,10 @@
@Param(id = 4) long creationTimestampMillis, @Param(id = 5) long ttlMillis, @Param(id = 6) int score,- @Param(id = 7) @NonNull PropertyParcel[] properties) {+ @Param(id = 7) @NonNull List properties,+ @Param(id = 8) @Nullable List parentTypes) { this(namespace, id, schemaType, creationTimestampMillis, ttlMillis, score,- properties, createPropertyMapFromPropertyArray(properties));+ properties, createPropertyMapFromPropertyArray(properties), parentTypes); } /**@@ -128,8 +141,9 @@
long creationTimestampMillis, long ttlMillis, int score,- @NonNull PropertyParcel[] properties,- @NonNull Map propertyMap) {+ @NonNull List properties,+ @NonNull Map propertyMap,+ @Nullable List parentTypes) { mNamespace = Objects.requireNonNull(namespace); mId = Objects.requireNonNull(id); mSchemaType = Objects.requireNonNull(schemaType);@@ -138,14 +152,15 @@
mScore = score; mProperties = Objects.requireNonNull(properties); mPropertyMap = Objects.requireNonNull(propertyMap);+ mParentTypes = parentTypes; } private static Map createPropertyMapFromPropertyArray(- @NonNull PropertyParcel[] properties) {+ @NonNull List properties) { Objects.requireNonNull(properties);- Map propertyMap = new ArrayMap<>(properties.length);- for (int i = 0; i < properties.length; ++i) {- PropertyParcel property = properties[i];+ Map propertyMap = new ArrayMap<>(properties.size());+ for (int i = 0; i < properties.size(); ++i) {+ PropertyParcel property = properties.get(i); propertyMap.put(property.getPropertyName(), property); } return propertyMap;@@ -193,7 +208,7 @@
/** Returns all the properties the document has. */ @NonNull- public PropertyParcel[] getProperties() {+ public List getProperties() { return mProperties; }@@ -203,6 +218,12 @@
return mPropertyMap; }+ /** Returns the list of parent types for the {@link GenericDocument}. */+ @Nullable+ public List getParentTypes() {+ return mParentTypes;+ }+ @Override public boolean equals(@Nullable Object other) { if (this == other) {@@ -218,8 +239,9 @@
&& mTtlMillis == otherDocument.mTtlMillis && mCreationTimestampMillis == otherDocument.mCreationTimestampMillis && mScore == otherDocument.mScore- && Arrays.equals(mProperties, otherDocument.mProperties)- && Objects.equals(mPropertyMap, otherDocument.mPropertyMap);+ && Objects.equals(mProperties, otherDocument.mProperties)+ && Objects.equals(mPropertyMap, otherDocument.mPropertyMap)+ && Objects.equals(mParentTypes, otherDocument.mParentTypes); } @Override@@ -232,8 +254,9 @@
mTtlMillis, mScore, mCreationTimestampMillis,- Arrays.hashCode(mProperties),- mPropertyMap.hashCode());+ Objects.hashCode(mProperties),+ Objects.hashCode(mPropertyMap),+ Objects.hashCode(mParentTypes)); } return mHashCode; }@@ -252,7 +275,7 @@
private long mTtlMillis; private int mScore; private Map mPropertyMap;- private boolean mBuilt = false;+ @Nullable private List mParentTypes; /** * Creates a new {@link GenericDocument.Builder}.@@ -275,7 +298,6 @@
* Creates a new {@link GenericDocumentParcel.Builder} from the given * {@link GenericDocumentParcel}. */- @VisibleForTesting public Builder(@NonNull GenericDocumentParcel documentSafeParcel) { Objects.requireNonNull(documentSafeParcel);@@ -292,6 +314,10 @@
for (PropertyParcel value : propertyMap.values()) { mPropertyMap.put(value.getPropertyName(), value); }++ // We don't need to create a shallow copy here, as in the setter for ParentTypes we+ // will create a new list anyway.+ mParentTypes = documentSafeParcel.mParentTypes; } /**@@ -306,7 +332,6 @@
@NonNull public Builder setNamespace(@NonNull String namespace) { Objects.requireNonNull(namespace);- resetIfBuilt(); mNamespace = namespace; return this; }@@ -321,7 +346,6 @@
@NonNull public Builder setId(@NonNull String id) { Objects.requireNonNull(id);- resetIfBuilt(); mId = id; return this; }@@ -336,7 +360,6 @@
@NonNull public Builder setSchemaType(@NonNull String schemaType) { Objects.requireNonNull(schemaType);- resetIfBuilt(); mSchemaType = schemaType; return this; }@@ -345,7 +368,6 @@
@CanIgnoreReturnValue @NonNull public Builder setScore(int score) {- resetIfBuilt(); mScore = score; return this; }@@ -364,7 +386,6 @@
@NonNull public Builder setCreationTimestampMillis( /*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {- resetIfBuilt(); mCreationTimestampMillis = creationTimestampMillis; return this; }@@ -388,12 +409,24 @@
if (ttlMillis < 0) { throw new IllegalArgumentException("Document ttlMillis cannot be negative."); }- resetIfBuilt(); mTtlMillis = ttlMillis; return this; } /**+ * Sets the list of parent types of the {@link GenericDocument}'s type.+ *+ *
Child types must appear before parent types in the list.
+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setParentTypes(@NonNull List parentTypes) {+ Objects.requireNonNull(parentTypes);+ mParentTypes = new ArrayList<>(parentTypes);+ return this;+ }++ /** * Clears the value for the property with the given name. * *
Note that this method does not support property paths.
@@ -404,40 +437,43 @@
@NonNull public Builder clearProperty(@NonNull String name) { Objects.requireNonNull(name);- resetIfBuilt(); mPropertyMap.remove(name); return this; } /** puts an array of {@link String} in property map. */+ @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull String[] values) throws IllegalArgumentException {- mPropertyMap.put(name,+ putInPropertyMap(name, new PropertyParcel.Builder(name).setStringValues(values).build()); return this; } /** puts an array of boolean in property map. */+ @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull boolean[] values) {- mPropertyMap.put(name,+ putInPropertyMap(name, new PropertyParcel.Builder(name).setBooleanValues(values).build()); return this; } /** puts an array of double in property map. */+ @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull double[] values) {- mPropertyMap.put(name,+ putInPropertyMap(name, new PropertyParcel.Builder(name).setDoubleValues(values).build()); return this; } /** puts an array of long in property map. */+ @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull long[] values) {- mPropertyMap.put(name,+ putInPropertyMap(name, new PropertyParcel.Builder(name).setLongValues(values).build()); return this; }@@ -445,26 +481,47 @@
/** * Converts and saves a byte[][] into {@link #mProperties}. */+ @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull byte[][] values) {- mPropertyMap.put(name,+ putInPropertyMap(name, new PropertyParcel.Builder(name).setBytesValues(values).build()); return this; } /** puts an array of {@link GenericDocumentParcel} in property map. */+ @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull GenericDocumentParcel[] values) {- mPropertyMap.put(name,+ putInPropertyMap(name, new PropertyParcel.Builder(name).setDocumentValues(values).build()); return this; }+ /** puts an array of {@link EmbeddingVector} in property map. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder putInPropertyMap(@NonNull String name,+ @NonNull EmbeddingVector[] values) {+ putInPropertyMap(name,+ new PropertyParcel.Builder(name).setEmbeddingValues(values).build());+ return this;+ }++ /** Directly puts a {@link PropertyParcel} in property map. */+ @CanIgnoreReturnValue+ @NonNull+ public Builder putInPropertyMap(@NonNull String name,+ @NonNull PropertyParcel value) {+ Objects.requireNonNull(value);+ mPropertyMap.put(name, value);+ return this;+ }+ /** Builds the {@link GenericDocument} object. */ @NonNull public GenericDocumentParcel build() {- mBuilt = true; // Set current timestamp for creation timestamp by default. if (mCreationTimestampMillis == INVALID_CREATION_TIMESTAMP_MILLIS) { mCreationTimestampMillis = System.currentTimeMillis();@@ -476,19 +533,8 @@
mCreationTimestampMillis, mTtlMillis, mScore,- mPropertyMap.values().toArray(new PropertyParcel[0]));- }-- void resetIfBuilt() {- if (mBuilt) {- Map propertyMap = mPropertyMap;- mPropertyMap = new ArrayMap<>(propertyMap.size());- for (PropertyParcel value : propertyMap.values()) {- // PropertyParcel is not deep copied since it is not mutable.- mPropertyMap.put(value.getPropertyName(), value);- }- mBuilt = false;- }+ new ArrayList<>(mPropertyMap.values()),+ mParentTypes); } } }
@@ -0,0 +1,142 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.safeparcel;+++import android.os.Bundle;+import android.os.Parcel;+import android.os.Parcelable;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RestrictTo;++import java.util.ArrayList;+import java.util.List;++/**+ * An implemented creator for {@link GenericDocumentParcel}.+ *+ *
In Jetpack, in order to serialize
+ * {@link GenericDocumentParcel} for {@link androidx.appsearch.app.GenericDocument},+ * {@link PropertyParcel} needs to be a real {@link Parcelable}.+ */+// @exportToFramework:skipFile()+@RestrictTo(RestrictTo.Scope.LIBRARY)+public class GenericDocumentParcelCreator implements+ Parcelable.Creator {+ private static final String PROPERTIES_FIELD = "properties";+ private static final String SCHEMA_TYPE_FIELD = "schemaType";+ private static final String ID_FIELD = "id";+ private static final String SCORE_FIELD = "score";+ private static final String TTL_MILLIS_FIELD = "ttlMillis";+ private static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis";+ private static final String NAMESPACE_FIELD = "namespace";+ private static final String PARENT_TYPES_FIELD = "parentTypes";++ /** Creates a {@link GenericDocumentParcel} from a {@link Bundle}. */+ @NonNull+ private static GenericDocumentParcel createGenericDocumentParcelFromBundle(+ @NonNull Bundle genericDocumentParcelBundle) {+ // Get namespace, id, and schema type+ String namespace = genericDocumentParcelBundle.getString(NAMESPACE_FIELD);+ String id = genericDocumentParcelBundle.getString(ID_FIELD);+ String schemaType = genericDocumentParcelBundle.getString(SCHEMA_TYPE_FIELD);++ // Those three can NOT be null.+ if (namespace == null || id == null || schemaType == null) {+ throw new IllegalArgumentException("GenericDocumentParcel bundle doesn't have "+ + "namespace, id, or schemaType.");+ }++ GenericDocumentParcel.Builder builder = new GenericDocumentParcel.Builder(namespace,+ id, schemaType);+ List parentTypes =+ genericDocumentParcelBundle.getStringArrayList(PARENT_TYPES_FIELD);+ if (parentTypes != null) {+ builder.setParentTypes(parentTypes);+ }+ builder.setScore(genericDocumentParcelBundle.getInt(SCORE_FIELD));+ builder.setCreationTimestampMillis(+ genericDocumentParcelBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD));+ builder.setTtlMillis(genericDocumentParcelBundle.getLong(TTL_MILLIS_FIELD));++ // properties+ Bundle propertyBundle = genericDocumentParcelBundle.getBundle(PROPERTIES_FIELD);+ if (propertyBundle != null) {+ for (String propertyName : propertyBundle.keySet()) {+ // SuppressWarnings can be applied on a local variable, but not any+ // single line of code.+ @SuppressWarnings("deprecation")+ PropertyParcel propertyParcel = propertyBundle.getParcelable(propertyName);+ builder.putInPropertyMap(propertyName, propertyParcel);+ }+ }++ return builder.build();+ }++ /** Creates a {@link Bundle} from a {@link GenericDocumentParcel}. */+ @NonNull+ private static Bundle createBundleFromGenericDocumentParcel(+ @NonNull GenericDocumentParcel genericDocumentParcel) {+ Bundle genericDocumentParcelBundle = new Bundle();++ // Common fields+ genericDocumentParcelBundle.putString(NAMESPACE_FIELD,+ genericDocumentParcel.getNamespace());+ genericDocumentParcelBundle.putString(ID_FIELD, genericDocumentParcel.getId());+ genericDocumentParcelBundle.putString(SCHEMA_TYPE_FIELD,+ genericDocumentParcel.getSchemaType());+ genericDocumentParcelBundle.putStringArrayList(PARENT_TYPES_FIELD,+ (ArrayList) genericDocumentParcel.getParentTypes());+ genericDocumentParcelBundle.putInt(SCORE_FIELD, genericDocumentParcel.getScore());+ genericDocumentParcelBundle.putLong(CREATION_TIMESTAMP_MILLIS_FIELD,+ genericDocumentParcel.getCreationTimestampMillis());+ genericDocumentParcelBundle.putLong(TTL_MILLIS_FIELD,+ genericDocumentParcel.getTtlMillis());++ // Properties+ Bundle properties = new Bundle();+ List propertyParcels = genericDocumentParcel.getProperties();+ for (int i = 0; i < propertyParcels.size(); ++i) {+ PropertyParcel propertyParcel = propertyParcels.get(i);+ properties.putParcelable(propertyParcel.getPropertyName(), propertyParcel);+ }+ genericDocumentParcelBundle.putBundle(PROPERTIES_FIELD, properties);++ return genericDocumentParcelBundle;+ }++ @Nullable+ @Override+ public GenericDocumentParcel createFromParcel(Parcel in) {+ Bundle bundle = in.readBundle(getClass().getClassLoader());+ return createGenericDocumentParcelFromBundle(bundle);+ }++ @Override+ public GenericDocumentParcel[] newArray(int size) {+ return new GenericDocumentParcel[size];+ }++ /** Writes a {@link GenericDocumentParcel} to a {@link Parcel}. */+ public static void writeToParcel(@NonNull GenericDocumentParcel genericDocumentParcel,+ @NonNull android.os.Parcel parcel, int flags) {+ parcel.writeBundle(createBundleFromGenericDocumentParcel(genericDocumentParcel));+ }+}
@@ -0,0 +1,101 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.safeparcel;++import android.annotation.SuppressLint;+import android.os.Parcel;+import android.os.Parcelable;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RestrictTo;+import androidx.appsearch.app.PackageIdentifier;+import androidx.appsearch.flags.FlaggedApi;+import androidx.appsearch.flags.Flags;+import androidx.core.util.Preconditions;++import java.util.Arrays;+import java.util.Objects;++/**+ * Holds data for a {@link PackageIdentifier}.+ *+ * TODO(b/275592563): This class is currently used in GetSchemaResponse as a bundle, and+ * therefore needs to implement Parcelable directly. Reassess if this is still needed once+ * VisibilityConfig becomes available, and if not we should switch to a SafeParcelable+ * implementation instead.+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY)[email protected](creator = "PackageIdentifierParcelCreator")+@SuppressLint("BanParcelableUsage")+public final class PackageIdentifierParcel extends AbstractSafeParcelable implements Parcelable {+ @NonNull+ public static final Parcelable.Creator CREATOR =+ new PackageIdentifierParcelCreator();++ @Field(id = 1, getter = "getPackageName")+ private final String mPackageName;+ @Field(id = 2, getter = "getSha256Certificate")+ private final byte[] mSha256Certificate;++ /**+ * Creates a unique identifier for a package.+ *+ * @see PackageIdentifier+ */+ @Constructor+ public PackageIdentifierParcel(@Param(id = 1) @NonNull String packageName,+ @Param(id = 2) @NonNull byte[] sha256Certificate) {+ mPackageName = Preconditions.checkNotNull(packageName);+ mSha256Certificate = Preconditions.checkNotNull(sha256Certificate);+ }++ @NonNull+ public String getPackageName() {+ return mPackageName;+ }++ @NonNull+ public byte[] getSha256Certificate() {+ return mSha256Certificate;+ }++ @Override+ public boolean equals(@Nullable Object obj) {+ if (this == obj) {+ return true;+ }+ if (!(obj instanceof PackageIdentifierParcel)) {+ return false;+ }+ final PackageIdentifierParcel other = (PackageIdentifierParcel) obj;+ return mPackageName.equals(other.mPackageName)+ && Arrays.equals(mSha256Certificate, other.mSha256Certificate);+ }++ @Override+ public int hashCode() {+ return Objects.hash(mPackageName, Arrays.hashCode(mSha256Certificate));+ }++ @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)+ @Override+ public void writeToParcel(@NonNull Parcel dest, int flags) {+ PackageIdentifierParcelCreator.writeToParcel(this, dest, flags);+ }+}
@@ -0,0 +1,93 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.safeparcel;++import android.os.Bundle;+import android.os.Parcel;+import android.os.Parcelable;++import androidx.annotation.NonNull;+import androidx.annotation.RestrictTo;+import androidx.core.util.Preconditions;++import java.util.Objects;++/**+ * An implemented creator for {@link PackageIdentifierParcel}.+ *+ *
In Jetpack, {@link androidx.appsearch.app.PackageIdentifier} is serialized in a bundle for
+ * {@link androidx.appsearch.app.GetSchemaResponse}, and therefore needs to implement a real+ * {@link Parcelable}.+ */+// @exportToFramework:skipFile()+@RestrictTo(RestrictTo.Scope.LIBRARY)+public class PackageIdentifierParcelCreator implements Parcelable.Creator {+ private static final String PACKAGE_NAME_FIELD = "packageName";+ private static final String SHA256_CERTIFICATE_FIELD = "sha256Certificate";++ public PackageIdentifierParcelCreator() {+ }++ /**+ * Creates a {@link PackageIdentifierParcel} from a {@link Bundle}+ */+ @NonNull+ private static PackageIdentifierParcel createPackageIdentifierFromBundle(+ @NonNull Bundle packageIdentifierBundle) {+ Objects.requireNonNull(packageIdentifierBundle);+ String packageName =+ Preconditions.checkNotNull(packageIdentifierBundle.getString(PACKAGE_NAME_FIELD));+ byte[] sha256Certificate =+ Preconditions.checkNotNull(+ packageIdentifierBundle.getByteArray(SHA256_CERTIFICATE_FIELD));++ return new PackageIdentifierParcel(packageName, sha256Certificate);+ }++ /** Creates a {@link Bundle} from a {@link PackageIdentifierParcel}. */+ @NonNull+ private static Bundle createBundleFromPackageIdentifier(+ @NonNull PackageIdentifierParcel packageIdentifierParcel) {+ Objects.requireNonNull(packageIdentifierParcel);+ Bundle packageIdentifierBundle = new Bundle();+ packageIdentifierBundle.putString(PACKAGE_NAME_FIELD,+ packageIdentifierParcel.getPackageName());+ packageIdentifierBundle.putByteArray(SHA256_CERTIFICATE_FIELD,+ packageIdentifierParcel.getSha256Certificate());++ return packageIdentifierBundle;+ }++ @NonNull+ @Override+ public PackageIdentifierParcel createFromParcel(Parcel parcel) {+ Bundle bundle = Preconditions.checkNotNull(parcel.readBundle(getClass().getClassLoader()));+ return createPackageIdentifierFromBundle(bundle);+ }++ @NonNull+ @Override+ public PackageIdentifierParcel[] newArray(int size) {+ return new PackageIdentifierParcel[size];+ }++ /** Writes a {@link PackageIdentifierParcel} to a {@link Parcel}. */+ public static void writeToParcel(@NonNull PackageIdentifierParcel packageIdentifierParcel,+ @NonNull android.os.Parcel parcel, int flags) {+ parcel.writeBundle(createBundleFromPackageIdentifier(packageIdentifierParcel));+ }+}
@@ -0,0 +1,223 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.safeparcel;++import android.os.Bundle;+import android.os.Parcel;+import android.os.Parcelable;++import androidx.annotation.NonNull;+import androidx.annotation.RestrictTo;+import androidx.appsearch.app.EmbeddingVector;++import java.util.ArrayList;+import java.util.List;+import java.util.Objects;++/**+ * An implemented creator for {@link PropertyParcel}.+ *+ *
In Jetpack, in order to serialize
+ * {@link GenericDocumentParcel} for {@link androidx.appsearch.app.GenericDocument},+ * {@link PropertyParcel} needs to be a real {@link Parcelable}.+ */+// @exportToFramework:skipFile()+@RestrictTo(RestrictTo.Scope.LIBRARY)+public class PropertyParcelCreator implements Parcelable.Creator {+ private static final String PROPERTY_NAME_FIELD = "propertyName";+ private static final String STRING_ARRAY_FIELD = "stringArray";+ private static final String LONG_ARRAY_FIELD = "longArray";+ private static final String DOUBLE_ARRAY_FIELD = "doubleArray";+ private static final String BOOLEAN_ARRAY_FIELD = "booleanArray";+ // 1d+ private static final String BYTE_ARRAY_FIELD = "byteArray";+ // 2d+ private static final String BYTES_ARRAY_FIELD = "bytesArray";+ private static final String DOC_ARRAY_FIELD = "docArray";+ private static final String EMBEDDING_VALUE_FIELD = "embeddingValue";+ private static final String EMBEDDING_MODEL_SIGNATURE_FIELD = "embeddingModelSignature";+ private static final String EMBEDDING_ARRAY_FIELD = "embeddingArray";++ public PropertyParcelCreator() {+ }++ /** Creates a {@link PropertyParcel} from a {@link Bundle}. */+ @SuppressWarnings({"unchecked"})+ @NonNull+ private static PropertyParcel createPropertyParcelFromBundle(+ @NonNull Bundle propertyParcelBundle) {+ Objects.requireNonNull(propertyParcelBundle);+ String propertyName = propertyParcelBundle.getString(PROPERTY_NAME_FIELD);++ Objects.requireNonNull(propertyName);+ PropertyParcel.Builder builder = new PropertyParcel.Builder(propertyName);++ // Get the values out of the bundle.+ String[] stringValues = propertyParcelBundle.getStringArray(STRING_ARRAY_FIELD);+ long[] longValues = propertyParcelBundle.getLongArray(LONG_ARRAY_FIELD);+ double[] doubleValues = propertyParcelBundle.getDoubleArray(DOUBLE_ARRAY_FIELD);+ boolean[] booleanValues = propertyParcelBundle.getBooleanArray(BOOLEAN_ARRAY_FIELD);++ List bytesArray;+ // SuppressWarnings can be applied on a local variable, but not any single line of+ // code.+ @SuppressWarnings("deprecation")+ List tmpList = propertyParcelBundle.getParcelableArrayList(BYTES_ARRAY_FIELD);+ bytesArray = tmpList;++ Parcelable[] docValues;+ // SuppressWarnings can be applied on a local variable, but not any single line of+ // code.+ @SuppressWarnings("deprecation")+ Parcelable[] tmpParcel = propertyParcelBundle.getParcelableArray(DOC_ARRAY_FIELD);+ docValues = tmpParcel;++ // SuppressWarnings can be applied on a local variable, but not any single line of+ // code.+ @SuppressWarnings("deprecation")+ List embeddingArray = propertyParcelBundle.getParcelableArrayList(+ EMBEDDING_ARRAY_FIELD);++ // Only one of those values will be set.+ boolean valueSet = false;+ if (stringValues != null) {+ builder.setStringValues(stringValues);+ valueSet = true;+ } else if (longValues != null) {+ builder.setLongValues(longValues);+ valueSet = true;+ } else if (doubleValues != null) {+ builder.setDoubleValues(doubleValues);+ valueSet = true;+ } else if (booleanValues != null) {+ builder.setBooleanValues(booleanValues);+ valueSet = true;+ } else if (bytesArray != null) {+ byte[][] bytes = new byte[bytesArray.size()][];+ for (int i = 0; i < bytesArray.size(); i++) {+ Bundle byteArray = bytesArray.get(i);+ if (byteArray == null) {+ continue;+ }+ byte[] innerBytes = byteArray.getByteArray(BYTE_ARRAY_FIELD);+ if (innerBytes == null) {+ continue;+ }+ bytes[i] = innerBytes;+ }+ builder.setBytesValues(bytes);+ valueSet = true;+ } else if (docValues != null && docValues.length > 0) {+ GenericDocumentParcel[] documentParcels =+ new GenericDocumentParcel[docValues.length];+ System.arraycopy(docValues, 0, documentParcels, 0, docValues.length);+ builder.setDocumentValues(documentParcels);+ valueSet = true;+ } else if (embeddingArray != null) {+ EmbeddingVector[] embeddings = new EmbeddingVector[embeddingArray.size()];+ for (int i = 0; i < embeddingArray.size(); i++) {+ Bundle embeddingBundle = embeddingArray.get(i);+ if (embeddingBundle == null) {+ continue;+ }+ float[] values = embeddingBundle.getFloatArray(EMBEDDING_VALUE_FIELD);+ String modelSignature = embeddingBundle.getString(EMBEDDING_MODEL_SIGNATURE_FIELD);+ if (values == null || modelSignature == null) {+ continue;+ }+ embeddings[i] = new EmbeddingVector(values, modelSignature);+ }+ builder.setEmbeddingValues(embeddings);+ valueSet = true;+ }++ if (!valueSet) {+ throw new IllegalArgumentException("property bundle passed in doesn't have any "+ + "value set.");+ }++ return builder.build();+ }++ /** Creates a {@link Bundle} from a {@link PropertyParcel}. */+ @NonNull+ private static Bundle createBundleFromPropertyParcel(+ @NonNull PropertyParcel propertyParcel) {+ Objects.requireNonNull(propertyParcel);+ Bundle propertyParcelBundle = new Bundle();+ propertyParcelBundle.putString(PROPERTY_NAME_FIELD, propertyParcel.getPropertyName());++ // Check and set the properties+ String[] stringValues = propertyParcel.getStringValues();+ long[] longValues = propertyParcel.getLongValues();+ double[] doubleValues = propertyParcel.getDoubleValues();+ boolean[] booleanValues = propertyParcel.getBooleanValues();+ byte[][] bytesArray = propertyParcel.getBytesValues();+ GenericDocumentParcel[] docArray = propertyParcel.getDocumentValues();+ EmbeddingVector[] embeddingArray = propertyParcel.getEmbeddingValues();++ if (stringValues != null) {+ propertyParcelBundle.putStringArray(STRING_ARRAY_FIELD, stringValues);+ } else if (longValues != null) {+ propertyParcelBundle.putLongArray(LONG_ARRAY_FIELD, longValues);+ } else if (doubleValues != null) {+ propertyParcelBundle.putDoubleArray(DOUBLE_ARRAY_FIELD, doubleValues);+ } else if (booleanValues != null) {+ propertyParcelBundle.putBooleanArray(BOOLEAN_ARRAY_FIELD, booleanValues);+ } else if (bytesArray != null) {+ ArrayList bundles = new ArrayList<>(bytesArray.length);+ for (int i = 0; i < bytesArray.length; i++) {+ Bundle byteArray = new Bundle();+ byteArray.putByteArray(BYTE_ARRAY_FIELD, bytesArray[i]);+ bundles.add(byteArray);+ }+ propertyParcelBundle.putParcelableArrayList(BYTES_ARRAY_FIELD, bundles);+ } else if (docArray != null) {+ propertyParcelBundle.putParcelableArray(DOC_ARRAY_FIELD, docArray);+ } else if (embeddingArray != null) {+ ArrayList bundles = new ArrayList<>(embeddingArray.length);+ for (int i = 0; i < embeddingArray.length; i++) {+ Bundle embedding = new Bundle();+ embedding.putFloatArray(EMBEDDING_VALUE_FIELD, embeddingArray[i].getValues());+ embedding.putString(EMBEDDING_MODEL_SIGNATURE_FIELD,+ embeddingArray[i].getModelSignature());+ bundles.add(embedding);+ }+ propertyParcelBundle.putParcelableArrayList(EMBEDDING_ARRAY_FIELD, bundles);+ }++ return propertyParcelBundle;+ }++ @NonNull+ @Override+ public PropertyParcel createFromParcel(Parcel in) {+ Bundle bundle = in.readBundle(getClass().getClassLoader());+ return createPropertyParcelFromBundle(bundle);+ }++ @Override+ public PropertyParcel[] newArray(int size) {+ return new PropertyParcel[size];+ }++ /** Writes a {@link PropertyParcel} to a {@link Parcel}. */+ public static void writeToParcel(@NonNull PropertyParcel propertyParcel,+ @NonNull android.os.Parcel parcel, int flags) {+ parcel.writeBundle(createBundleFromPropertyParcel(propertyParcel));+ }+}
@@ -74,4 +74,7 @@
*/ boolean doNotParcelTypeDefaultValues() default false; }++ /** Provide same interface as {@link android.os.Parcelable} for code sync purpose. */+ int describeContents(); }
@@ -17,6 +17,7 @@
package androidx.appsearch.safeparcel.stub; import android.os.Parcel;+import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.RestrictTo;@@ -31,7 +32,21 @@
*/ // @exportToFramework:skipFile() @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)-abstract class AbstractCreator {+abstract class AbstractCreator implements Parcelable.Creator {+ @Override+ public T createFromParcel(Parcel var1) {+ // This is here only for code sync purpose.+ throw new UnsupportedOperationException("createFromParcel is not implemented and should "+ + "not be used.");+ }++ @Override+ public T[] newArray(int var1) {+ // This is here only for code sync purpose.+ throw new UnsupportedOperationException("newArray is not implemented and should "+ + "not be used.");+ }+ public static void writeToParcel( @NonNull SafeParcelable safeParcelable, @NonNull Parcel parcel,
@@ -17,6 +17,7 @@
package androidx.appsearch.stats; import android.os.Parcel;+import android.os.Parcelable; import androidx.annotation.IntDef; import androidx.annotation.NonNull;@@ -40,10 +41,10 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @SafeParcelable.Class(creator = "SchemaMigrationStatsCreator") public final class SchemaMigrationStats extends AbstractSafeParcelable {- @NonNull public static final SchemaMigrationStatsCreator CREATOR =+ @NonNull public static final Parcelable.Creator CREATOR = new SchemaMigrationStatsCreator();- // Indicate the how a SetSchema call relative to SchemaMigration case.+ /** Indicate the SetSchema call type relative to SchemaMigration case. */ @IntDef( value = { NO_MIGRATION,
@@ -0,0 +1,55 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.usagereporting;++import androidx.annotation.RestrictTo;++/**+ * Wrapper class for action constants.+ *+ * @exportToFramework:hide+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public final class ActionConstants {+ /**+ * Unknown action type.+ *+ *
It is defined for abstract action class and compatibility, so it should not be used in any
+ * concrete instances.+ */+ public static final int ACTION_TYPE_UNKNOWN = 0;++ /**+ * Search action type.+ *+ * + *
It is the action type for {@link SearchAction}.
+ * + */+ public static final int ACTION_TYPE_SEARCH = 1;++ /**+ * Click action type.+ *+ * + *
It is the action type for {@link ClickAction}.
+ * + */+ public static final int ACTION_TYPE_CLICK = 2;++ private ActionConstants() {}+}
@@ -0,0 +1,286 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+// @exportToFramework:skipFile()++package androidx.appsearch.usagereporting;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RequiresFeature;+import androidx.appsearch.annotation.CanIgnoreReturnValue;+import androidx.appsearch.annotation.Document;+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;+import androidx.appsearch.app.Features;+import androidx.core.util.Preconditions;++/**+ * {@link ClickAction} is a built-in AppSearch document type that contains different metrics.+ * Clients can report the user's click actions on a {@link androidx.appsearch.app.SearchResult}+ * document.+ *+ *
In order to use this document type, the client must explicitly set this schema type via
Click actions can be used as signals to boost ranking via
+ * {@link androidx.appsearch.app.JoinSpec} API in future search requests.+ *+ *
Since {@link ClickAction} is an AppSearch document, the client can handle deletion via
+ * {@link androidx.appsearch.app.AppSearchSession#removeAsync} or document time-to-live (TTL). The+ * default TTL is 60 days.+ */+// In ClickAction document, there is a joinable property "referencedQualifiedId" for reporting the+// qualified id of the clicked document. The client can create personal navboost with click action+// signals by join query with this property. Therefore, ClickAction document class requires join+// feature.+@RequiresFeature(+ enforcement = "androidx.appsearch.app.Features#isFeatureSupported",+ name = Features.JOIN_SPEC_AND_QUALIFIED_ID)+@Document(name = "builtin:ClickAction")+public class ClickAction extends TakenAction {+ @Nullable+ @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)+ private final String mQuery;++ @Nullable+ @Document.StringProperty(joinableValueType =+ StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)+ private final String mReferencedQualifiedId;++ @Document.LongProperty+ private final int mResultRankInBlock;++ @Document.LongProperty+ private final int mResultRankGlobal;++ @Document.LongProperty+ private final long mTimeStayOnResultMillis;++ ClickAction(@NonNull String namespace, @NonNull String id, long documentTtlMillis,+ long actionTimestampMillis, @TakenAction.ActionType int actionType,+ @Nullable String query, @Nullable String referencedQualifiedId, int resultRankInBlock,+ int resultRankGlobal, long timeStayOnResultMillis) {+ super(namespace, id, documentTtlMillis, actionTimestampMillis, actionType);++ mQuery = query;+ mReferencedQualifiedId = referencedQualifiedId;+ mResultRankInBlock = resultRankInBlock;+ mResultRankGlobal = resultRankGlobal;+ mTimeStayOnResultMillis = timeStayOnResultMillis;+ }++ /**+ * Returns the user-entered search input (without any operators or rewriting) that yielded the+ * {@link androidx.appsearch.app.SearchResult} on which the user clicked.+ */+ @Nullable+ public String getQuery() {+ return mQuery;+ }++ /**+ * Returns the qualified id of the {@link androidx.appsearch.app.SearchResult} document that the+ * user clicked on.+ *+ *
A qualified id is a string generated by package, database, namespace, and document id. See
+ * {@link androidx.appsearch.util.DocumentIdUtil#createQualifiedId(String,String,String,String)}+ * for more details.+ */+ @Nullable+ public String getReferencedQualifiedId() {+ return mReferencedQualifiedId;+ }++ /**+ * Returns the rank of the {@link androidx.appsearch.app.SearchResult} document among the+ * user-defined block.+ *+ *
The client can define its own custom definition for block, e.g. corpus name, group, etc.
+ *+ *
For example, a client defines the block as corpus, and AppSearch returns 5 documents with
+ * corpus = ["corpus1", "corpus1", "corpus2", "corpus3", "corpus2"]. Then the block ranks of+ * them = [1, 2, 1, 1, 2].+ *+ *
If the client is not presenting the results in multiple blocks, they should set this value
+ * to match {@link #getResultRankGlobal}.+ *+ *
If unset, then the block rank of the {@link androidx.appsearch.app.SearchResult} document
+ * will be set to -1 to mark invalid.+ */+ public int getResultRankInBlock() {+ return mResultRankInBlock;+ }++ /**+ * Returns the global rank of the {@link androidx.appsearch.app.SearchResult} document.+ *+ *
Global rank reflects the order of {@link androidx.appsearch.app.SearchResult} documents
+ * returned by AppSearch.+ *+ *
For example, AppSearch returns 2 pages with 10 {@link androidx.appsearch.app.SearchResult}
+ * documents for each page. Then the global ranks of them will be 1 to 10 for the first page,+ * and 11 to 20 for the second page.+ *+ *
If unset, then the global rank of the {@link androidx.appsearch.app.SearchResult} document
+ * will be set to -1 to mark invalid.+ */+ public int getResultRankGlobal() {+ return mResultRankGlobal;+ }++ /**+ * Returns the time in milliseconds that user stays on the+ * {@link androidx.appsearch.app.SearchResult} document after clicking it.+ */+ public long getTimeStayOnResultMillis() {+ return mTimeStayOnResultMillis;+ }++ // TODO(b/314026345): redesign builder to enable inheritance for ClickAction.+ /** Builder for {@link ClickAction}. */+ @Document.BuilderProducer+ public static final class Builder extends BuilderImpl {+ private String mQuery;+ private String mReferencedQualifiedId;+ private int mResultRankInBlock;+ private int mResultRankGlobal;+ private long mTimeStayOnResultMillis;++ /**+ * Constructor for {@link ClickAction.Builder}.+ *+ * @param namespace Namespace for the Document. See {@link Document.Namespace}.+ * @param id Unique identifier for the Document. See {@link Document.Id}.+ * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds+ * since Unix epoch.+ */+ public Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis) {+ this(namespace, id, actionTimestampMillis, ActionConstants.ACTION_TYPE_CLICK);+ }++ /**+ * Constructs {@link ClickAction.Builder} by copying existing values from the given+ * {@link ClickAction}.+ *+ * @param clickAction an existing {@link ClickAction} object.+ */+ public Builder(@NonNull ClickAction clickAction) {+ super(Preconditions.checkNotNull(clickAction));++ mQuery = clickAction.getQuery();+ mReferencedQualifiedId = clickAction.getReferencedQualifiedId();+ mResultRankInBlock = clickAction.getResultRankInBlock();+ mResultRankGlobal = clickAction.getResultRankGlobal();+ mTimeStayOnResultMillis = clickAction.getTimeStayOnResultMillis();+ }++ /**+ * Constructor for {@link ClickAction.Builder}.+ *+ *
It is required by {@link Document.BuilderProducer}.
+ *+ * @param namespace Namespace for the Document. See {@link Document.Namespace}.+ * @param id Unique identifier for the Document. See {@link Document.Id}.+ * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds+ * since Unix epoch.+ * @param actionType Action type enum for the Document. See+ * {@link TakenAction.ActionType}.+ */+ Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis,+ @TakenAction.ActionType int actionType) {+ super(namespace, id, actionTimestampMillis, actionType);++ // Default for unset result rank fields. Since negative number is invalid for ranking,+ // -1 is used as an unset value and AppSearch will ignore it.+ mResultRankInBlock = -1;+ mResultRankGlobal = -1;++ // Default for unset timeStayOnResultMillis. Since negative number is invalid for+ // time in millis, -1 is used as an unset value and AppSearch will ignore it.+ mTimeStayOnResultMillis = -1;+ }++ /**+ * Sets the user-entered search input (without any operators or rewriting) that yielded+ * the {@link androidx.appsearch.app.SearchResult} on which the user clicked.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setQuery(@Nullable String query) {+ mQuery = query;+ return this;+ }++ /**+ * Sets the qualified id of the {@link androidx.appsearch.app.SearchResult} document that+ * the user takes action on.+ *+ *
A qualified id is a string generated by package, database, namespace, and document id.
+ * See {@link androidx.appsearch.util.DocumentIdUtil#createQualifiedId(+ * String,String,String,String)} for more details.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setReferencedQualifiedId(@Nullable String referencedQualifiedId) {+ mReferencedQualifiedId = referencedQualifiedId;+ return this;+ }++ /**+ * Sets the rank of the {@link androidx.appsearch.app.SearchResult} document among the+ * user-defined block.+ *+ * @see ClickAction#getResultRankInBlock+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setResultRankInBlock(int resultRankInBlock) {+ mResultRankInBlock = resultRankInBlock;+ return this;+ }++ /**+ * Sets the global rank of the {@link androidx.appsearch.app.SearchResult} document.+ *+ * @see ClickAction#getResultRankGlobal+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setResultRankGlobal(int resultRankGlobal) {+ mResultRankGlobal = resultRankGlobal;+ return this;+ }++ /**+ * Sets the time in milliseconds that user stays on the+ * {@link androidx.appsearch.app.SearchResult} document after clicking it.+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setTimeStayOnResultMillis(long timeStayOnResultMillis) {+ mTimeStayOnResultMillis = timeStayOnResultMillis;+ return this;+ }++ /** Builds a {@link ClickAction}. */+ @Override+ @NonNull+ public ClickAction build() {+ return new ClickAction(mNamespace, mId, mDocumentTtlMillis, mActionTimestampMillis,+ mActionType, mQuery, mReferencedQualifiedId, mResultRankInBlock,+ mResultRankGlobal, mTimeStayOnResultMillis);+ }+ }+}
@@ -0,0 +1,155 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+// @exportToFramework:skipFile()++package androidx.appsearch.usagereporting;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.appsearch.annotation.CanIgnoreReturnValue;+import androidx.appsearch.annotation.Document;+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;+import androidx.core.util.Preconditions;++/**+ * {@link SearchAction} is a built-in AppSearch document type that contains different metrics.+ *
+ *
Clients can report the user's search actions.
+ *
Usually {@link SearchAction} is reported together with {@link ClickAction}, since the
+ * user clicks on {@link androidx.appsearch.app.SearchResult} documents after searching.+ * + *+ *
In order to use this document type, the client must explicitly set this schema type via
Since {@link SearchAction} is an AppSearch document, the client can handle deletion via
+ * {@link androidx.appsearch.app.AppSearchSession#removeAsync} or document time-to-live (TTL). The+ * default TTL is 60 days.+ */+@Document(name = "builtin:SearchAction")+public class SearchAction extends TakenAction {+ @Nullable+ @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)+ private final String mQuery;++ @Document.LongProperty+ private final int mFetchedResultCount;++ SearchAction(@NonNull String namespace, @NonNull String id, long documentTtlMillis,+ long actionTimestampMillis, @TakenAction.ActionType int actionType,+ @Nullable String query, int fetchedResultCount) {+ super(namespace, id, documentTtlMillis, actionTimestampMillis, actionType);++ mQuery = query;+ mFetchedResultCount = fetchedResultCount;+ }++ /** Returns the user-entered search input (without any operators or rewriting). */+ @Nullable+ public String getQuery() {+ return mQuery;+ }++ /**+ * Returns total number of results fetched from AppSearch by the client in this+ * {@link SearchAction}.+ *+ *
If unset, then it will be set to -1 to mark invalid.
+ */+ public int getFetchedResultCount() {+ return mFetchedResultCount;+ }++ // TODO(b/314026345): redesign builder to enable inheritance for SearchAction.+ /** Builder for {@link SearchAction}. */+ @Document.BuilderProducer+ public static final class Builder extends BuilderImpl {+ private String mQuery;+ private int mFetchedResultCount;++ /**+ * Constructor for {@link SearchAction.Builder}.+ *+ * @param namespace Namespace for the Document. See {@link Document.Namespace}.+ * @param id Unique identifier for the Document. See {@link Document.Id}.+ * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds+ * since Unix epoch.+ */+ public Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis) {+ this(namespace, id, actionTimestampMillis, ActionConstants.ACTION_TYPE_SEARCH);+ }++ /**+ * Constructor for {@link Builder} with all the existing values.+ */+ public Builder(@NonNull SearchAction searchAction) {+ super(Preconditions.checkNotNull(searchAction));++ mQuery = searchAction.getQuery();+ mFetchedResultCount = searchAction.getFetchedResultCount();+ }++ /**+ * Constructor for {@link SearchAction.Builder}.+ *+ *
It is required by {@link Document.BuilderProducer}.
+ *+ * @param namespace Namespace for the Document. See {@link Document.Namespace}.+ * @param id Unique identifier for the Document. See {@link Document.Id}.+ * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds+ * since Unix epoch.+ * @param actionType Action type enum for the Document. See+ * {@link TakenAction.ActionType}.+ */+ Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis,+ @TakenAction.ActionType int actionType) {+ super(namespace, id, actionTimestampMillis, actionType);++ // Default for unset fetchedResultCount. Since negative number is invalid for fetched+ // result count, -1 is used as an unset value and AppSearch will ignore it.+ mFetchedResultCount = -1;+ }++ /** Sets the user-entered search input (without any operators or rewriting). */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setQuery(@Nullable String query) {+ mQuery = query;+ return this;+ }++ /**+ * Sets total number of results fetched from AppSearch by the client in this+ * {@link SearchAction}.+ *+ * @see SearchAction#getFetchedResultCount+ */+ @CanIgnoreReturnValue+ @NonNull+ public Builder setFetchedResultCount(int fetchedResultCount) {+ mFetchedResultCount = fetchedResultCount;+ return this;+ }++ /** Builds a {@link SearchAction}. */+ @Override+ @NonNull+ public SearchAction build() {+ return new SearchAction(mNamespace, mId, mDocumentTtlMillis, mActionTimestampMillis,+ mActionType, mQuery, mFetchedResultCount);+ }+ }+}
@@ -0,0 +1,231 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+// @exportToFramework:skipFile()++package androidx.appsearch.usagereporting;++import androidx.annotation.IntDef;+import androidx.annotation.NonNull;+import androidx.annotation.RestrictTo;+import androidx.appsearch.annotation.CanIgnoreReturnValue;+import androidx.appsearch.annotation.Document;+import androidx.core.util.Preconditions;++import java.lang.annotation.Retention;+import java.lang.annotation.RetentionPolicy;++/**+ * {@link TakenAction} is an abstract class which holds common fields of other AppSearch built-in+ * action types (e.g. {@link SearchAction}, {@link ClickAction}).+ *+ *
Clients can report the user's actions by creating concrete actions with
+ * {@link androidx.appsearch.app.PutDocumentsRequest.Builder#addTakenActions} API.+ */+@Document(name = "builtin:TakenAction")+public abstract class TakenAction {+ /** Default TTL for all related {@link TakenAction} documents: 60 days. */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ public static final long DEFAULT_DOCUMENT_TTL_MILLIS = 60L * 24 * 60 * 60 * 1000;++ /** AppSearch taken action type. */+ @RestrictTo(RestrictTo.Scope.LIBRARY)+ @IntDef(value = {+ ActionConstants.ACTION_TYPE_UNKNOWN,+ ActionConstants.ACTION_TYPE_SEARCH,+ ActionConstants.ACTION_TYPE_CLICK,+ })+ @Retention(RetentionPolicy.SOURCE)+ public @interface ActionType {+ }++ @NonNull+ @Document.Namespace+ private final String mNamespace;++ @NonNull+ @Document.Id+ private final String mId;++ @Document.TtlMillis+ private final long mDocumentTtlMillis;++ @Document.CreationTimestampMillis+ private final long mActionTimestampMillis;++ @Document.LongProperty+ @ActionType+ private final int mActionType;++ TakenAction(@NonNull String namespace, @NonNull String id, long documentTtlMillis,+ long actionTimestampMillis, @ActionType int actionType) {+ mNamespace = Preconditions.checkNotNull(namespace);+ mId = Preconditions.checkNotNull(id);+ mDocumentTtlMillis = documentTtlMillis;+ mActionTimestampMillis = actionTimestampMillis;+ mActionType = actionType;+ }++ /** Returns the namespace of the {@link TakenAction}. */+ @NonNull+ public String getNamespace() {+ return mNamespace;+ }++ /** Returns the unique identifier of the {@link TakenAction}. */+ @NonNull+ public String getId() {+ return mId;+ }++ /**+ * Returns the time-to-live (TTL) of the {@link TakenAction} document as a duration in+ * milliseconds.+ *+ *
The document will be automatically deleted when the TTL expires (since
+ * {@link #getActionTimestampMillis()}).+ *+ *
The default TTL for {@link TakenAction} document is 60 days.
+ *+ *
See {@link androidx.appsearch.annotation.Document.TtlMillis} for more information on TTL.
+ */+ public long getDocumentTtlMillis() {+ return mDocumentTtlMillis;+ }++ /**+ * Returns the timestamp when the user took the action, in milliseconds since Unix epoch.+ *+ *
The action timestamp will be used together with {@link #getDocumentTtlMillis()} as the
+ * document retention.+ */+ public long getActionTimestampMillis() {+ return mActionTimestampMillis;+ }++ /**+ * Returns the action type of the {@link TakenAction}.+ *+ * @see TakenAction.ActionType+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @ActionType+ public int getActionType() {+ return mActionType;+ }++ // TODO(b/330777270): improve AnnotationProcessor for abstract document class, and remove this+ // builder.+ /** Builder for {@link TakenAction}. */+ @Document.BuilderProducer+ static final class Builder extends BuilderImpl {+ /**+ * Constructor for {@link TakenAction.Builder}.+ *+ * @param namespace Namespace for the Document. See {@link Document.Namespace}.+ * @param id Unique identifier for the Document. See {@link Document.Id}.+ * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds+ * since Unix epoch.+ * @param actionType Action type enum for the Document. See+ * {@link TakenAction.ActionType}.+ */+ Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis,+ @TakenAction.ActionType int actionType) {+ super(namespace, id, actionTimestampMillis, actionType);+ }++ /** Constructor for {@link TakenAction.Builder} with all the existing values. */+ Builder(@NonNull TakenAction takenAction) {+ super(takenAction);+ }+ }++ // Use templated BuilderImpl to resolve base class setter return type issue for child class+ // builder instances.+ @SuppressWarnings("unchecked")+ static class BuilderImpl> {+ protected final String mNamespace;+ protected final String mId;+ protected long mDocumentTtlMillis;+ protected long mActionTimestampMillis;+ @ActionType+ protected int mActionType;++ /**+ * Constructs {@link TakenAction.BuilderImpl} with given {@code namespace}, {@code id},+ * {@code actionTimestampMillis} and {@code actionType}.+ *+ * @param namespace The namespace of the {@link TakenAction} document.+ * @param id The id of the {@link TakenAction} document.+ * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds+ * since Unix epoch.+ * @param actionType The action type enum of the Document.+ */+ BuilderImpl(@NonNull String namespace, @NonNull String id, long actionTimestampMillis,+ @TakenAction.ActionType int actionType) {+ mNamespace = Preconditions.checkNotNull(namespace);+ mId = Preconditions.checkNotNull(id);+ mActionTimestampMillis = actionTimestampMillis;+ mActionType = actionType;++ // Default for documentTtlMillis.+ mDocumentTtlMillis = TakenAction.DEFAULT_DOCUMENT_TTL_MILLIS;+ }++ /**+ * Constructs {@link TakenAction.BuilderImpl} by copying existing values from the given+ * {@link TakenAction}.+ *+ * @param takenAction an existing {@link TakenAction} object.+ */+ BuilderImpl(@NonNull TakenAction takenAction) {+ this(takenAction.getNamespace(), takenAction.getId(),+ takenAction.getActionTimestampMillis(), takenAction.getActionType());+ mDocumentTtlMillis = takenAction.getDocumentTtlMillis();+ }++ /**+ * Sets the time-to-live (TTL) of the {@link TakenAction} document as a duration in+ * milliseconds.+ *+ *
The document will be automatically deleted when the TTL expires (since
The default TTL for {@link TakenAction} document is 60 days.
+ *+ *
See {@link androidx.appsearch.annotation.Document.TtlMillis} for more information on
+ * TTL.+ */+ @CanIgnoreReturnValue+ @NonNull+ public T setDocumentTtlMillis(long documentTtlMillis) {+ mDocumentTtlMillis = documentTtlMillis;+ return (T) this;+ }++ // TODO(b/330777270): improve AnnotationProcessor for abstract document class builder, and+ // make it an abstract method.+ /**+ * For AppSearch annotation processor requirement only. The client should never call it+ * since it is impossible to instantiate an abstract class.+ *+ * @throws UnsupportedOperationException+ */+ @NonNull+ public TakenAction build() {+ throw new UnsupportedOperationException();+ }+ }+}
@@ -0,0 +1,67 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.util;++import android.os.RemoteException;+import android.util.Log;++import androidx.annotation.NonNull;+import androidx.annotation.RestrictTo;++/**+ * Utilities for handling exceptions.+ */+// This file has different behavior in Framework as compared to JetPack (like it is okay to rethrow+// exception and log instead of rethrowing from SystemServer in case of RemoteException). This file+// is not synced to Framework, as it maintains its own environment specific copy.+// @exportToFramework:skipFile()+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+public final class ExceptionUtil {+ private static final String TAG = "AppSearchExceptionUtil";++ /**+ * {@link RuntimeException} will be rethrown if {@link #isItOkayToRethrowException()} returns+ * true.+ */+ public static void handleException(@NonNull Exception e) {+ if (isItOkayToRethrowException() && e instanceof RuntimeException) {+ rethrowRuntimeException((RuntimeException) e);+ }+ }++ /** Returns whether it is OK to rethrow exceptions from this entrypoint. */+ private static boolean isItOkayToRethrowException() {+ return true;+ }++ /** Rethrow exception from SystemServer in Framework code. */+ public static void handleRemoteException(@NonNull RemoteException e) {+ Log.w(TAG, "Unable to make a call to AppSearchManagerService!", e);+ }++ /**+ * A helper method to rethrow {@link RuntimeException}.+ *+ *
We use this to enforce exception type and assure the compiler/linter that the exception is
+ * indeed {@link RuntimeException} and can be rethrown safely.+ */+ private static void rethrowRuntimeException(RuntimeException e) {+ throw e;+ }++ private ExceptionUtil() {}+}
@@ -22,6 +22,7 @@
import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.annotation.Size;+import androidx.appsearch.app.AppSearchEnvironmentFactory; /** * Utilities for logging to logcat.@@ -33,6 +34,8 @@
// TODO(b/232285376): If it becomes possible to detect an eng build, turn this on by default // for eng builds. public static final boolean DEBUG = false;+ public static final boolean INFO = AppSearchEnvironmentFactory.getEnvironmentInstance()+ .isInfoLoggingEnabled(); /** * The {@link #piiTrace} logs are intended for sensitive data that can't be enabled in@@ -92,7 +95,7 @@
@NonNull String message, @Nullable Object fastTraceObj, @Nullable Object fullTraceObj) {- if (PII_TRACE_LEVEL == 0) {+ if (PII_TRACE_LEVEL == 0 || !INFO) { return; } StringBuilder builder = new StringBuilder("(trace) ").append(message);
@@ -203,8 +203,9 @@
// unboxing. // // 1b: ListCallArraysAsList- // List contains String. We have to convert this from an array of String[], but no- // conversion of the collection elements is needed. We can use Arrays#asList for this.+ // List contains String or EmbeddingVector. We have to convert this from+ // an array of String[] or EmbeddingVector[], but no conversion of the+ // collection elements is needed. We can use Arrays#asList for this. // // 1c: ListForLoopCallFromGenericDocument // List contains a class which is annotated with @Document.@@ -225,7 +226,8 @@
// of unboxing. // // 2b: ArrayUseDirectly- // Array is of type String[], long[], double[], boolean[], byte[][].+ // Array is of type String[], long[], double[], boolean[], byte[][] or+ // EmbeddingVector[]. // We can directly use this field with no conversion. // // 2c: ArrayForLoopCallFromGenericDocument@@ -243,7 +245,8 @@
// Scenario 3: Single valued fields // 3a: FieldUseDirectlyWithNullCheck- // Field is of type String, Long, Integer, Double, Float, Boolean, byte[].+ // Field is of type String, Long, Integer, Double, Float, Boolean, byte[] or+ // EmbeddingVector. // We can use this field directly, after testing for null. The java compiler will box // or unbox as needed. //@@ -393,6 +396,19 @@
default: throw new IllegalStateException("Unhandled type-category: " + typeCategory); }+ case EMBEDDING_PROPERTY:+ switch (typeCategory) {+ case COLLECTION: // List: 1b+ return listCallArraysAsList(annotation, getterOrField);+ case ARRAY:+ // EmbeddingVector[]: 2b+ return arrayUseDirectly(annotation, getterOrField);+ case SINGLE:+ // EmbeddingVector: 3a+ return fieldUseDirectlyWithNullCheck(annotation, getterOrField);+ default:+ throw new IllegalStateException("Unhandled type-category: " + typeCategory);+ } default: throw new IllegalStateException("Unhandled annotation: " + annotation); }@@ -433,8 +449,9 @@
} // 1b: ListCallArraysAsList- // List contains String. We have to convert this from an array of String[], but no- // conversion of the collection elements is needed. We can use Arrays#asList for this.+ // List contains String or EmbeddingVector. We have to convert this from+ // an array of String[] or EmbeddingVector[], but no conversion of the+ // collection elements is needed. We can use Arrays#asList for this. @NonNull private CodeBlock listCallArraysAsList( @NonNull DataPropertyAnnotation annotation,@@ -559,8 +576,8 @@
} // 2b: ArrayUseDirectly- // Array is of type String[], long[], double[], boolean[], byte[][].- // We can directly use this field with no conversion.+ // Array is of type String[], long[], double[], boolean[], byte[][] or+ // EmbeddingVector[]. @NonNull private CodeBlock arrayUseDirectly( @NonNull DataPropertyAnnotation annotation,@@ -646,7 +663,8 @@
} // 3a: FieldUseDirectlyWithNullCheck- // Field is of type String, Long, Integer, Double, Float, Boolean, byte[].+ // Field is of type String, Long, Integer, Double, Float, Boolean, byte[] or+ // EmbeddingVector. // We can use this field directly, after testing for null. The java compiler will box // or unbox as needed. @NonNull
@@ -90,6 +90,9 @@
public static final ClassName GENERIC_DOCUMENT_CLASS = ClassName.get(APPSEARCH_PKG, "GenericDocument");+ public static final ClassName EMBEDDING_VECTOR_CLASS =+ ClassName.get(APPSEARCH_PKG, "EmbeddingVector");+ public static final ClassName BUILDER_PRODUCER_CLASS = DOCUMENT_ANNOTATION_CLASS.nestedClass("BuilderProducer");@@ -108,6 +111,7 @@
public final TypeMirror mBooleanPrimitiveType; public final TypeMirror mBytePrimitiveArrayType; public final TypeMirror mGenericDocumentType;+ public final TypeMirror mEmbeddingType; public final TypeMirror mDoublePrimitiveType; final TypeMirror mCollectionType; final TypeMirror mListType;@@ -151,6 +155,8 @@
mBytePrimitiveArrayType = mTypeUtils.getArrayType(mBytePrimitiveType); mGenericDocumentType = mElementUtils.getTypeElement(GENERIC_DOCUMENT_CLASS.canonicalName()).asType();+ mEmbeddingType = mElementUtils.getTypeElement(+ EMBEDDING_VECTOR_CLASS.canonicalName()).asType(); } /**
@@ -165,10 +165,10 @@
// care of unboxing and widening where necessary. // // 1b: CollectionCallToArray- // Collection contains String or GenericDocument.- // We have to convert this into an array of String[] or GenericDocument[], but no- // conversion of the collection elements is needed. We can use Collection#toArray for- // this.+ // Collection contains String, GenericDocument or EmbeddingVector.+ // We have to convert this into an array of String[], GenericDocument[] or+ // EmbeddingVector[], but no conversion of the collection elements is+ // needed. We can use Collection#toArray for this. // // 1c: CollectionForLoopCallToGenericDocument // Collection contains a class which is annotated with @Document.@@ -188,8 +188,8 @@
// unboxing and widening where necessary. // // 2b: ArrayUseDirectly- // Array is of type String[], long[], double[], boolean[], byte[][] or- // GenericDocument[].+ // Array is of type String[], long[], double[], boolean[], byte[][],+ // GenericDocument[] or EmbeddingVector[]. // We can directly use this field with no conversion. // // 2c: ArrayForLoopCallToGenericDocument@@ -207,7 +207,8 @@
// Scenario 3: Single valued fields // 3a: FieldUseDirectlyWithNullCheck- // Field is of type String, Long, Integer, Double, Float, Boolean.+ // Field is of type String, Long, Integer, Double, Float, Boolean or+ // EmbeddingVector. // We can use this field directly, after testing for null. The java compiler will box // or unbox as needed. //@@ -375,6 +376,20 @@
default: throw new IllegalStateException("Unhandled type-category: " + typeCategory); }+ case EMBEDDING_PROPERTY:+ switch (typeCategory) {+ case COLLECTION:+ // List: 1b+ return collectionCallToArray(annotation, getterOrField);+ case ARRAY:+ // EmbeddingVector[]: 2b+ return arrayUseDirectly(annotation, getterOrField);+ case SINGLE:+ // EmbeddingVector: 3a+ return fieldUseDirectlyWithNullCheck(annotation, getterOrField);+ default:+ throw new IllegalStateException("Unhandled type-category: " + typeCategory);+ } default: throw new IllegalStateException("Unhandled annotation: " + annotation); }@@ -417,10 +432,10 @@
} // 1b: CollectionCallToArray- // Collection contains String or GenericDocument.- // We have to convert this into an array of String[] or GenericDocument[], but no- // conversion of the collection elements is needed. We can use Collection#toArray for- // this.+ // Collection contains String, GenericDocument or EmbeddingVector.+ // We have to convert this into an array of String[], GenericDocument[] or+ // EmbeddingVector[], but no conversion of the collection elements is+ // needed. We can use Collection#toArray for this. @NonNull private CodeBlock collectionCallToArray( @NonNull DataPropertyAnnotation annotation,@@ -536,8 +551,8 @@
} // 2b: ArrayUseDirectly- // Array is of type String[], long[], double[], boolean[], byte[][] or- // GenericDocument[].+ // Array is of type String[], long[], double[], boolean[], byte[][],+ // GenericDocument[] or EmbeddingVector[]. // We can directly use this field with no conversion. @NonNull private CodeBlock arrayUseDirectly(@@ -613,7 +628,8 @@
} // 3a: FieldUseDirectlyWithNullCheck- // Field is of type String, Long, Integer, Double, Float, Boolean.+ // Field is of type String, Long, Integer, Double, Float, Boolean or+ // EmbeddingVector. // We can use this field directly, after testing for null. The java compiler will box // or unbox as needed. @NonNull
@@ -0,0 +1,85 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.appsearch.compiler.annotationwrapper;++import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_SCHEMA_CLASS;+import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;++import androidx.annotation.NonNull;+import androidx.appsearch.compiler.IntrospectionHelper;+import androidx.appsearch.compiler.ProcessingException;++import com.google.auto.value.AutoValue;+import com.squareup.javapoet.ClassName;++import java.util.Map;++import javax.lang.model.type.TypeMirror;++/**+ * An instance of the {@code @Document.EmbeddingProperty} annotation.+ */+@AutoValue+public abstract class EmbeddingPropertyAnnotation extends DataPropertyAnnotation {+ public static final ClassName CLASS_NAME =+ DOCUMENT_ANNOTATION_CLASS.nestedClass("EmbeddingProperty");++ public static final ClassName CONFIG_CLASS =+ APPSEARCH_SCHEMA_CLASS.nestedClass("EmbeddingPropertyConfig");++ public EmbeddingPropertyAnnotation() {+ super(+ CLASS_NAME,+ CONFIG_CLASS,+ /* genericDocGetterName= */"getPropertyEmbedding",+ /* genericDocArrayGetterName= */"getPropertyEmbeddingArray",+ /* genericDocSetterName= */"setPropertyEmbedding");+ }++ /**+ * @param defaultName The name to use for the annotated property in case the annotation+ * params do not mention an explicit name.+ * @throws ProcessingException If the annotation points to an Illegal serializer class.+ */+ @NonNull+ static EmbeddingPropertyAnnotation parse(+ @NonNull Map annotationParams,+ @NonNull String defaultName) throws ProcessingException {+ String name = (String) annotationParams.get("name");+ return new AutoValue_EmbeddingPropertyAnnotation(+ name.isEmpty() ? defaultName : name,+ (boolean) annotationParams.get("required"),+ (int) annotationParams.get("indexingType"));+ }++ /**+ * Specifies how a property should be indexed.+ */+ public abstract int getIndexingType();++ @NonNull+ @Override+ public final Kind getDataPropertyKind() {+ return Kind.EMBEDDING_PROPERTY;+ }++ @NonNull+ @Override+ public TypeMirror getUnderlyingTypeWithinGenericDoc(@NonNull IntrospectionHelper helper) {+ return helper.mEmbeddingType;+ }+}
@@ -43,6 +43,7 @@
field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1 field public static final int ERROR_LOCKOUT = 7; // 0x7 field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9+ field public static final int ERROR_MORE_OPTIONS_BUTTON = 16; // 0x10 field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe@@ -72,16 +73,22 @@
ctor public BiometricPrompt.CryptoObject(java.security.Signature); ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher); ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);+ ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public BiometricPrompt.CryptoObject(long); method public javax.crypto.Cipher? getCipher(); method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential(); method public javax.crypto.Mac? getMac();+ method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public long getOperationHandle(); method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public android.security.identity.PresentationSession? getPresentationSession(); method public java.security.Signature? getSignature(); } public static class BiometricPrompt.PromptInfo { method public int getAllowedAuthenticators();+ method public androidx.biometric.PromptContentView? getContentView(); method public CharSequence? getDescription();+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.graphics.Bitmap? getLogoBitmap();+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getLogoDescription();+ method @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public int getLogoRes(); method public CharSequence getNegativeButtonText(); method public CharSequence? getSubtitle(); method public CharSequence getTitle();@@ -94,13 +101,54 @@
method public androidx.biometric.BiometricPrompt.PromptInfo build(); method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int); method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setContentView(androidx.biometric.PromptContentView); method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?); method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoBitmap(android.graphics.Bitmap);+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoDescription(String);+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoRes(@DrawableRes int); method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence); method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?); method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence); }+ public interface PromptContentItem {+ }++ public final class PromptContentItemBulletedText implements androidx.biometric.PromptContentItem {+ ctor public PromptContentItemBulletedText(String);+ }++ public final class PromptContentItemPlainText implements androidx.biometric.PromptContentItem {+ ctor public PromptContentItemPlainText(String);+ }++ public interface PromptContentView {+ }++ public final class PromptContentViewWithMoreOptionsButton implements androidx.biometric.PromptContentView {+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getDescription();+ }++ public static final class PromptContentViewWithMoreOptionsButton.Builder {+ ctor public PromptContentViewWithMoreOptionsButton.Builder();+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton build();+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton.Builder setDescription(String);+ }++ public final class PromptVerticalListContentView implements androidx.biometric.PromptContentView {+ method public String? getDescription();+ method public java.util.List getListItems();+ }++ public static final class PromptVerticalListContentView.Builder {+ ctor public PromptVerticalListContentView.Builder();+ method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem);+ method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem, int);+ method public androidx.biometric.PromptVerticalListContentView build();+ method public androidx.biometric.PromptVerticalListContentView.Builder setDescription(String);+ }+ } package androidx.biometric.auth {
@@ -43,6 +43,7 @@
field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1 field public static final int ERROR_LOCKOUT = 7; // 0x7 field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9+ field public static final int ERROR_MORE_OPTIONS_BUTTON = 16; // 0x10 field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe@@ -72,16 +73,22 @@
ctor public BiometricPrompt.CryptoObject(java.security.Signature); ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher); ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);+ ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public BiometricPrompt.CryptoObject(long); method public javax.crypto.Cipher? getCipher(); method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential(); method public javax.crypto.Mac? getMac();+ method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public long getOperationHandle(); method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public android.security.identity.PresentationSession? getPresentationSession(); method public java.security.Signature? getSignature(); } public static class BiometricPrompt.PromptInfo { method public int getAllowedAuthenticators();+ method public androidx.biometric.PromptContentView? getContentView(); method public CharSequence? getDescription();+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.graphics.Bitmap? getLogoBitmap();+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getLogoDescription();+ method @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public int getLogoRes(); method public CharSequence getNegativeButtonText(); method public CharSequence? getSubtitle(); method public CharSequence getTitle();@@ -94,13 +101,54 @@
method public androidx.biometric.BiometricPrompt.PromptInfo build(); method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int); method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);+ method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setContentView(androidx.biometric.PromptContentView); method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?); method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoBitmap(android.graphics.Bitmap);+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoDescription(String);+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoRes(@DrawableRes int); method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence); method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?); method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence); }+ public interface PromptContentItem {+ }++ public final class PromptContentItemBulletedText implements androidx.biometric.PromptContentItem {+ ctor public PromptContentItemBulletedText(String);+ }++ public final class PromptContentItemPlainText implements androidx.biometric.PromptContentItem {+ ctor public PromptContentItemPlainText(String);+ }++ public interface PromptContentView {+ }++ public final class PromptContentViewWithMoreOptionsButton implements androidx.biometric.PromptContentView {+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getDescription();+ }++ public static final class PromptContentViewWithMoreOptionsButton.Builder {+ ctor public PromptContentViewWithMoreOptionsButton.Builder();+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton build();+ method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton.Builder setDescription(String);+ }++ public final class PromptVerticalListContentView implements androidx.biometric.PromptContentView {+ method public String? getDescription();+ method public java.util.List getListItems();+ }++ public static final class PromptVerticalListContentView.Builder {+ ctor public PromptVerticalListContentView.Builder();+ method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem);+ method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem, int);+ method public androidx.biometric.PromptVerticalListContentView build();+ method public androidx.biometric.PromptVerticalListContentView.Builder setDescription(String);+ }+ } package androidx.biometric.auth {
@@ -16,11 +16,13 @@
package androidx.biometric;+import android.annotation.SuppressLint; import android.app.Activity; import android.app.KeyguardManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent;+import android.graphics.Bitmap; import android.os.Build; import android.os.Bundle; import android.os.Handler;@@ -81,13 +83,20 @@
static final int CANCELED_FROM_CLIENT = 3; /**+ * Authentication was canceled by the user by pressing the more options button on the prompt+ * content.+ */+ static final int CANCELED_FROM_MORE_OPTIONS_BUTTON = 4;++ /** * Where authentication was canceled from. */ @IntDef({ CANCELED_FROM_INTERNAL, CANCELED_FROM_USER, CANCELED_FROM_NEGATIVE_BUTTON,- CANCELED_FROM_CLIENT+ CANCELED_FROM_CLIENT,+ CANCELED_FROM_MORE_OPTIONS_BUTTON }) @Retention(RetentionPolicy.SOURCE) @interface CanceledFrom {}@@ -352,6 +361,15 @@
} });+ mViewModel.isMoreOptionsButtonPressPending().observe(this,+ moreOptionsButtonPressPending -> {+ if (moreOptionsButtonPressPending) {+ onMoreOptionsButtonPressed();+ mViewModel.setMoreOptionsButtonPressPending(false);+ }+ }+ );+ mViewModel.isFingerprintDialogCancelPending().observe(this, fingerprintDialogCancelPending -> { if (fingerprintDialogCancelPending) {@@ -514,6 +532,29 @@
builder, AuthenticatorUtils.isDeviceCredentialAllowed(authenticators)); }+ // Set the custom biometric prompt features introduced in Android 15 (API 35).+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {+ final int logoRes = mViewModel.getLogoRes();+ final Bitmap logoBitmap = mViewModel.getLogoBitmap();+ final String logoDescription = mViewModel.getLogoDescription();+ final android.hardware.biometrics.PromptContentView contentView =+ PromptContentViewUtils.wrapForBiometricPrompt(mViewModel.getContentView(),+ mViewModel.getClientExecutor(),+ mViewModel.getMoreOptionsButtonListener());+ if (logoRes != -1) {+ Api35Impl.setLogoRes(builder, logoRes);+ }+ if (logoBitmap != null) {+ Api35Impl.setLogoBitmap(builder, logoBitmap);+ }+ if (logoDescription != null && !logoDescription.isEmpty()) {+ Api35Impl.setLogoDescription(builder, logoDescription);+ }+ if (contentView != null) {+ Api35Impl.setContentView(builder, contentView);+ }+ }+ authenticateWithBiometricPrompt(Api28Impl.buildPrompt(builder), getContext()); }@@ -779,6 +820,16 @@
} /**+ * Callback that is run when the view model reports that the more options button has been+ * pressed on the prompt content.+ */+ void onMoreOptionsButtonPressed() {+ sendErrorAndDismiss(BiometricPrompt.ERROR_MORE_OPTIONS_BUTTON,+ "More options button in the content view is clicked.");+ cancelAuthentication(BiometricFragment.CANCELED_FROM_MORE_OPTIONS_BUTTON);+ }++ /** * Launches the confirm device credential Settings activity, where the user can authenticate * using their PIN, pattern, or password. */@@ -1097,6 +1148,72 @@
} /**+ * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35).+ */+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)+ @SuppressLint("MissingPermission")+ private static class Api35Impl {+ // Prevent instantiation.+ private Api35Impl() {+ }++ /**+ * Sets the prompt content view for the given framework prompt builder.+ *+ * @param builder An instance of+ * {@link android.hardware.biometrics.BiometricPrompt.Builder}.+ * @param logoRes A drawable resource of the logo that will be shown on the prompt.+ */+ @DoNotInline+ static void setLogoRes(+ @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder, int logoRes) {+ builder.setLogoRes(logoRes);+ }++ /**+ * Sets the prompt content view for the given framework prompt builder.+ *+ * @param builder An instance of+ * {@link android.hardware.biometrics.BiometricPrompt.Builder}.+ * @param logoBitmap A bitmap drawable of the logo that will be shown on the prompt.+ */+ @DoNotInline+ static void setLogoBitmap(+ @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder,+ @NonNull Bitmap logoBitmap) {+ builder.setLogoBitmap(logoBitmap);+ }++ /**+ * Sets the prompt content view for the given framework prompt builder.+ *+ * @param builder An instance of+ * {@link android.hardware.biometrics.BiometricPrompt.Builder}.+ * @param logoDescription The content view for the prompt.+ */+ @DoNotInline+ static void setLogoDescription(+ @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder,+ String logoDescription) {+ builder.setLogoDescription(logoDescription);+ }++ /**+ * Sets the prompt content view for the given framework prompt builder.+ *+ * @param builder An instance of+ * {@link android.hardware.biometrics.BiometricPrompt.Builder}.+ * @param contentView The content view for the prompt.+ */+ @DoNotInline+ static void setContentView(+ @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder,+ @NonNull android.hardware.biometrics.PromptContentView contentView) {+ builder.setContentView(contentView);+ }+ }++ /** * Nested class to avoid verification errors for methods introduced in Android 11 (API 30). */ @RequiresApi(Build.VERSION_CODES.R)
@@ -16,15 +16,20 @@
package androidx.biometric;+import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;+ import android.annotation.SuppressLint;+import android.graphics.Bitmap; import android.os.Build; import android.text.TextUtils; import android.util.Log;+import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi;+import androidx.annotation.RequiresPermission; import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; import androidx.biometric.BiometricManager.Authenticators;@@ -160,6 +165,11 @@
public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15; /**+ * The user pressed the more options button on prompt content.+ */+ public static final int ERROR_MORE_OPTIONS_BUTTON = 16;++ /** * An error code that may be returned during authentication. */ @IntDef({@@ -175,7 +185,8 @@
ERROR_NO_BIOMETRICS, ERROR_HW_NOT_PRESENT, ERROR_NEGATIVE_BUTTON,- ERROR_NO_DEVICE_CREDENTIAL+ ERROR_NO_DEVICE_CREDENTIAL,+ ERROR_MORE_OPTIONS_BUTTON }) @RestrictTo(RestrictTo.Scope.LIBRARY) @Retention(RetentionPolicy.SOURCE)@@ -230,6 +241,7 @@
@Nullable private final Mac mMac; @Nullable private final android.security.identity.IdentityCredential mIdentityCredential; @Nullable private final android.security.identity.PresentationSession mPresentationSession;+ private final long mOperationHandle; /** * Creates a crypto object that wraps the given signature object.@@ -242,6 +254,7 @@
mMac = null; mIdentityCredential = null; mPresentationSession = null;+ mOperationHandle = 0; } /**@@ -255,6 +268,7 @@
mMac = null; mIdentityCredential = null; mPresentationSession = null;+ mOperationHandle = 0; } /**@@ -268,6 +282,7 @@
mMac = mac; mIdentityCredential = null; mPresentationSession = null;+ mOperationHandle = 0; } /**@@ -284,6 +299,7 @@
mMac = null; mIdentityCredential = identityCredential; mPresentationSession = null;+ mOperationHandle = 0; } /**@@ -300,9 +316,27 @@
mMac = null; mIdentityCredential = null; mPresentationSession = presentationSession;+ mOperationHandle = 0; } /**+ * Create from an operation handle.+ * @see CryptoObject#getOperationHandle()+ *+ * @param operationHandle the operation handle associated with this object.+ */+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)+ public CryptoObject(long operationHandle) {+ mSignature = null;+ mCipher = null;+ mMac = null;+ mIdentityCredential = null;+ mPresentationSession = null;+ mOperationHandle = operationHandle;+ }+++ /** * Gets the signature object associated with this crypto object. * * @return The signature, or {@code null} if none is associated with this object.@@ -353,6 +387,34 @@
public android.security.identity.PresentationSession getPresentationSession() { return mPresentationSession; }++ /**+ * Returns the {@code operationHandle} associated with this object or 0 if none.+ * The {@code operationHandle} is the underlying identifier associated with+ * the {@code CryptoObject}.+ *+ *
The {@code operationHandle} can be used to reconstruct a {@code CryptoObject}
+ * instance. This is useful for any cross-process communication as the {@code CryptoObject}+ * class is not {@link android.os.Parcelable}. Hence, if the {@code CryptoObject} is+ * constructed in one process, and needs to be propagated to another process,+ * before calling the {@code authenticate()} API in the second process, the+ * recommendation is to retrieve the {@code operationHandle} using this API, and then+ * reconstruct the {@code CryptoObject}using the constructor that takes in an {@code+ * operationHandle}, and pass that in to the {@code authenticate} API mentioned above.+ */+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)+ public long getOperationHandle() {+ return CryptoObjectUtils.getOperationHandle(this);+ }++ /**+ * Returns the {@code operationHandle} from the constructor. This is only for wrapping+ * this {@link androidx.biometric.BiometricPrompt.CryptoObject} to+ * {@link android.hardware.biometrics.BiometricPrompt}.+ */+ long getOperationHandleCryptoObject() {+ return mOperationHandle;+ } } /**@@ -438,15 +500,75 @@
*/ public static class Builder { // Mutable options to be set on the builder.+ @DrawableRes private int mLogoRes = -1;+ @Nullable private Bitmap mLogoBitmap = null;+ @Nullable private String mLogoDescription = null; @Nullable private CharSequence mTitle = null; @Nullable private CharSequence mSubtitle = null; @Nullable private CharSequence mDescription = null;+ @Nullable private PromptContentView mPromptContentView = null; @Nullable private CharSequence mNegativeButtonText = null; private boolean mIsConfirmationRequired = true; private boolean mIsDeviceCredentialAllowed = false; @BiometricManager.AuthenticatorTypes private int mAllowedAuthenticators = 0; /**+ * Optional: Sets the drawable resource of the logo that will be shown on the prompt.+ *+ *
Note that using this method is not recommended in most scenarios because the
+ * calling application's icon will be used by default. Setting the logo is intended+ * for large bundled applications that perform a wide range of functions and need to+ * show distinct icons for each function.+ *+ * @param logoRes A drawable resource of the logo that will be shown on the prompt.+ * @return This builder.+ */+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)+ @NonNull+ public Builder setLogoRes(@DrawableRes int logoRes) {+ mLogoRes = logoRes;+ return this;+ }++ /**+ * Optional: Sets the bitmap drawable of the logo that will be shown on the prompt.+ *+ *
Note that using this method is not recommended in most scenarios because the
+ * calling application's icon will be used by default. Setting the logo is intended+ * for large bundled applications that perform a wide range of functions and need to+ * show distinct icons for each function.+ *+ * @param logoBitmap A bitmap drawable of the logo that will be shown on the prompt.+ * @return This builder.+ */+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)+ @NonNull+ public Builder setLogoBitmap(@NonNull Bitmap logoBitmap) {+ mLogoBitmap = logoBitmap;+ return this;+ }++ /**+ * Optional: Sets logo description text that will be shown on the prompt.+ *+ *
Note that using this method is not recommended in most scenarios because the
+ * calling application's name will be used by default. Setting the logo description+ * is intended for large bundled applications that perform a wide range of functions+ * and need to show distinct description for each function.+ *+ * @param logoDescription The logo description text that will be shown on the prompt.+ * @return This builder.+ * @throws IllegalArgumentException If logo description is null or exceeds certain+ * character limit.+ */+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)+ @NonNull+ public Builder setLogoDescription(@NonNull String logoDescription) {+ mLogoDescription = logoDescription;+ return this;+ }++ /** * Required: Sets the title for the prompt. * * @param title The title to be displayed on the prompt.@@ -471,9 +593,14 @@
} /**- * Optional: Sets the description for the prompt.+ * Optional: Sets a description that will be shown on the prompt. *- * @param description The description to be displayed on the prompt.+ *
Note that the description set by {@link Builder#setDescription(CharSequence)}
+ * will be overridden by {@link Builder#setContentView(PromptContentView)}. The view+ * provided to {@link Builder#setContentView(PromptContentView)} will be used if both+ * methods are called.+ *+ * @param description The description to display. * @return This builder. */ @NonNull@@ -483,6 +610,23 @@
} /**+ * Optional: Sets application customized content view that will be shown on the prompt.+ *+ *
Note that the description set by {@link Builder#setDescription(CharSequence)}
+ * will be overridden by {@link Builder#setContentView(PromptContentView)}. The view+ * provided to {@link Builder#setContentView(PromptContentView)} will be used if both+ * methods are called.+ *+ * @param view The customized view information.+ * @return This builder.+ */+ @NonNull+ public Builder setContentView(@NonNull PromptContentView view) {+ mPromptContentView = view;+ return this;+ }++ /** * Required: Sets the text for the negative button on the prompt. * *
Note that this option is incompatible with device credential authentication and
@@ -623,9 +767,13 @@
} return new PromptInfo(+ mLogoRes,+ mLogoBitmap,+ mLogoDescription, mTitle, mSubtitle, mDescription,+ mPromptContentView, mNegativeButtonText, mIsConfirmationRequired, mIsDeviceCredentialAllowed,@@ -634,9 +782,13 @@
} // Immutable fields for the prompt info object.+ @DrawableRes private int mLogoRes;+ @Nullable private Bitmap mLogoBitmap;+ @Nullable private String mLogoDescription; @NonNull private final CharSequence mTitle; @Nullable private final CharSequence mSubtitle; @Nullable private final CharSequence mDescription;+ @Nullable private PromptContentView mPromptContentView; @Nullable private final CharSequence mNegativeButtonText; private final boolean mIsConfirmationRequired; private final boolean mIsDeviceCredentialAllowed;@@ -645,16 +797,24 @@
// Prevent direct instantiation. @SuppressWarnings("WeakerAccess") /* synthetic access */ PromptInfo(+ int logoRes,+ @Nullable Bitmap logoBitmap,+ @Nullable String logoDescription, @NonNull CharSequence title, @Nullable CharSequence subtitle, @Nullable CharSequence description,+ @Nullable PromptContentView promptContentView, @Nullable CharSequence negativeButtonText, boolean confirmationRequired, boolean deviceCredentialAllowed, @BiometricManager.AuthenticatorTypes int allowedAuthenticators) {+ mLogoRes = logoRes;+ mLogoBitmap = logoBitmap;+ mLogoDescription = logoDescription; mTitle = title; mSubtitle = subtitle; mDescription = description;+ mPromptContentView = promptContentView; mNegativeButtonText = negativeButtonText; mIsConfirmationRequired = confirmationRequired; mIsDeviceCredentialAllowed = deviceCredentialAllowed;@@ -662,6 +822,44 @@
} /**+ * Gets the drawable resource of the logo for the prompt, as set by+ * {@link Builder#setLogoRes(int)}. Currently for system applications use only.+ *+ * @return The drawable resource of the logo, or -1 if the prompt has no logo resource set.+ */+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)+ @DrawableRes+ public int getLogoRes() {+ return mLogoRes;+ }++ /**+ * Gets the logo bitmap for the prompt, as set by {@link Builder#setLogoBitmap(Bitmap)}.+ * Currently for system applications use only.+ *+ * @return The logo bitmap of the prompt, or null if the prompt has no logo bitmap set.+ */+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)+ @Nullable+ public Bitmap getLogoBitmap() {+ return mLogoBitmap;+ }++ /**+ * Gets the logo description for the prompt, as set by+ * {@link Builder#setLogoDescription(String)}.+ * Currently for system applications use only.+ *+ * @return The logo description of the prompt, or null if the prompt has no logo description+ * set.+ */+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)+ @Nullable+ public String getLogoDescription() {+ return mLogoDescription;+ }++ /** * Gets the title for the prompt. * * @return The title to be displayed on the prompt.@@ -698,6 +896,17 @@
} /**+ * Gets the content view for the prompt, as set by+ * {@link Builder#setContentView(PromptContentView)}.+ *+ * @return The content view for the prompt, or null if the prompt has no content view.+ */+ @Nullable+ public PromptContentView getContentView() {+ return mPromptContentView;+ }++ /** * Gets the text for the negative button on the prompt. * * @return The label to be used for the negative button on the prompt, or an empty string if
@@ -16,7 +16,9 @@
package androidx.biometric;+import android.annotation.SuppressLint; import android.content.DialogInterface;+import android.graphics.Bitmap; import android.os.Handler; import android.os.Looper;@@ -139,6 +141,29 @@
} /**+ * The dialog listener that is returned by {@link #getMoreOptionsButtonListener()} ()}.+ */+ private static class MoreOptionsButtonListener implements DialogInterface.OnClickListener {+ @NonNull private final WeakReference mViewModelRef;++ /**+ * Creates a more options button listener with a weak reference to the given view model.+ *+ * @param viewModel The view model instance to hold a weak reference to.+ */+ MoreOptionsButtonListener(@Nullable BiometricViewModel viewModel) {+ mViewModelRef = new WeakReference<>(viewModel);+ }++ @Override+ public void onClick(DialogInterface dialogInterface, int which) {+ if (mViewModelRef.get() != null) {+ mViewModelRef.get().setMoreOptionsButtonPressPending(true);+ }+ }+ }++ /** * The executor that will run authentication callback methods. * *
If unset, callbacks are invoked on the main thread with {@link Looper#getMainLooper()}.
@@ -181,6 +206,11 @@
@Nullable private DialogInterface.OnClickListener mNegativeButtonListener; /**+ * A dialog listener for the more options button shown on the prompt content.+ */+ @Nullable private DialogInterface.OnClickListener mMoreOptionsButtonListener;++ /** * A label for the negative button shown on the prompt. * *
If set, this value overrides the one returned by
@@ -251,6 +281,11 @@
@Nullable private MutableLiveData mIsNegativeButtonPressPending; /**+ * Whether the user has pressed the more options button on the prompt content.+ */+ @Nullable private MutableLiveData mIsMoreOptionsButtonPressPending;++ /** * Whether the fingerprint dialog should always be dismissed instantly. */ private boolean mIsFingerprintDialogDismissedInstantly = true;@@ -327,6 +362,47 @@
} /**+ * Gets the logo res to be shown on the biometric prompt.+ *+ *
This method relies on the {@link BiometricPrompt.PromptInfo} set by
+ * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.+ *+ * @return The logo res for the prompt, or -1 if not set.+ */+ @SuppressLint("MissingPermission")+ int getLogoRes() {+ return mPromptInfo != null ? mPromptInfo.getLogoRes() : -1;+ }++ /**+ * Gets the logo bitmap to be shown on the biometric prompt.+ *+ *
This method relies on the {@link BiometricPrompt.PromptInfo} set by
+ * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.+ *+ * @return The logo bitmap for the prompt, or null if not set.+ */+ @SuppressLint("MissingPermission")+ @Nullable+ Bitmap getLogoBitmap() {+ return mPromptInfo != null ? mPromptInfo.getLogoBitmap() : null;+ }++ /**+ * Gets the logo description to be shown on the biometric prompt.+ *+ *
This method relies on the {@link BiometricPrompt.PromptInfo} set by
+ * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.+ *+ * @return The logo description for the prompt, or null if not set.+ */+ @SuppressLint("MissingPermission")+ @Nullable+ String getLogoDescription() {+ return mPromptInfo != null ? mPromptInfo.getLogoDescription() : null;+ }++ /** * Gets the title to be shown on the biometric prompt. * *
This method relies on the {@link BiometricPrompt.PromptInfo} set by
@@ -366,6 +442,19 @@
} /**+ * Gets the prompt content view to be shown on the biometric prompt.+ *+ *
This method relies on the {@link BiometricPrompt.PromptInfo} set by
+ * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.+ *+ * @return The prompt content view for the prompt, or {@code null} if not set.+ */+ @Nullable+ PromptContentView getContentView() {+ return mPromptInfo != null ? mPromptInfo.getContentView() : null;+ }++ /** * Gets the text that should be shown for the negative button on the biometric prompt. * *
If non-null, the value set by {@link #setNegativeButtonTextOverride(CharSequence)} is
@@ -112,6 +112,17 @@
} }+ // Operation handle is only supported on API 35 and above.+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {+ // This should be the bottom one and only be reachable when cryptoObject was+ // constructed with operation handle. cryptoObject from other constructors should+ // already be unwrapped and returned above.+ final long operationHandle = Api35Impl.getOperationHandle(cryptoObject);+ if (operationHandle != 0) {+ return new BiometricPrompt.CryptoObject(operationHandle);+ }+ }+ return null; }@@ -164,10 +175,39 @@
} }+ // Operation handle is only supported on API 35 and above.+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {+ final long operationHandle = cryptoObject.getOperationHandleCryptoObject();+ if (operationHandle != 0) {+ return Api35Impl.create(operationHandle);+ }+ }+ return null; } /**+ * Get the {@code operationHandle} associated with this object or 0 if none. This needs to be+ * achieved by getting the corresponding+ * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} and then get its+ * operation handle.+ *+ * @param cryptoObject An instance of {@link androidx.biometric.BiometricPrompt.CryptoObject}.+ * @return The {@code operationHandle} associated with this object or 0 if none.+ */+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)+ static long getOperationHandle(@Nullable BiometricPrompt.CryptoObject cryptoObject) {+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {+ final android.hardware.biometrics.BiometricPrompt.CryptoObject wrappedCryptoObject =+ CryptoObjectUtils.wrapForBiometricPrompt(cryptoObject);+ if (wrappedCryptoObject != null) {+ return Api35Impl.getOperationHandle(wrappedCryptoObject);+ }+ }+ return 0;+ }++ /** * Unwraps a crypto object returned by * {@link androidx.core.hardware.fingerprint.FingerprintManagerCompat}. *@@ -250,6 +290,11 @@
return null; }+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {+ Log.e(TAG, "Operation handle is not supported by FingerprintManager.");+ return null;+ }+ return null; }@@ -298,6 +343,41 @@
} /**+ * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35).+ */+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)+ private static class Api35Impl {+ // Prevent instantiation.+ private Api35Impl() {}++ /**+ * Creates an instance of the framework class+ * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} from the given+ * operation handle.+ *+ * @param operationHandle The operation handle to be wrapped.+ * @return An instance of {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}.+ */+ @NonNull+ static android.hardware.biometrics.BiometricPrompt.CryptoObject create(+ long operationHandle) {+ return new android.hardware.biometrics.BiometricPrompt.CryptoObject(operationHandle);+ }++ /**+ * Gets the operation handle associated with the given crypto object, if any.+ *+ * @param crypto An instance of+ * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}.+ * @return The wrapped operation handle object, or {@code null}.+ */+ static long getOperationHandle(+ @NonNull android.hardware.biometrics.BiometricPrompt.CryptoObject crypto) {+ return crypto.getOperationHandle();+ }+ }++ /** * Nested class to avoid verification errors for methods introduced in Android 13.0 (API 33). */ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
@@ -0,0 +1,23 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.biometric;++/**+ * An item shown on {@link PromptContentView}.+ */+public interface PromptContentItem {+}
@@ -0,0 +1,40 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.biometric;++import androidx.annotation.NonNull;++/**+ * A list item with bulleted text shown on {@link PromptVerticalListContentView}.+ */+public final class PromptContentItemBulletedText implements PromptContentItem {+ private final String mText;++ /**+ * A list item with bulleted text shown on {@link PromptVerticalListContentView}.+ *+ * @param text The text of this list item.+ */+ public PromptContentItemBulletedText(@NonNull String text) {+ mText = text;+ }++ @NonNull+ String getText() {+ return mText;+ }+}
@@ -0,0 +1,40 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.biometric;++import androidx.annotation.NonNull;++/**+ * A list item with plain text shown on {@link PromptVerticalListContentView}.+ */+public final class PromptContentItemPlainText implements PromptContentItem {+ private final String mText;++ /**+ * A list item with plain text shown on {@link PromptVerticalListContentView}.+ *+ * @param text The text of this list item.+ */+ public PromptContentItemPlainText(@NonNull String text) {+ mText = text;+ }++ @NonNull+ String getText() {+ return mText;+ }+}
@@ -0,0 +1,23 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.biometric;++/**+ * Contains the information of the template of content view for Biometric Prompt.+ */+public interface PromptContentView {+}
@@ -0,0 +1,142 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.biometric;++import android.annotation.SuppressLint;+import android.content.DialogInterface;+import android.os.Build;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RequiresApi;++import java.util.concurrent.Executor;++/**+ * Utility class for creating and converting between different types of prompt content view that may+ * be used internally by {@link BiometricPrompt}+ */+class PromptContentViewUtils {+ // Prevent instantiation.+ private PromptContentViewUtils() {+ }++ /**+ * Wraps a prompt content view to be passed to {@link BiometricPrompt}.+ *+ * @param contentView An instance of {@link PromptContentView}.+ * @param executor An executor for the more options button callback.+ * @param moreOptionsButtonListener A listener for the more options button press event.+ * @return An equivalent prompt content view that is compatible with+ * {@link android.hardware.biometrics.PromptContentView}.+ */+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)+ @Nullable+ static android.hardware.biometrics.PromptContentView wrapForBiometricPrompt(+ @Nullable PromptContentView contentView, @NonNull Executor executor,+ @NonNull DialogInterface.OnClickListener moreOptionsButtonListener) {++ if (contentView == null) {+ return null;+ }++ // Prompt content view is only supported on API 35 and above.+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {+ if (contentView instanceof PromptVerticalListContentView) {+ return Api35Impl.createPromptVerticalListContentView(+ (PromptVerticalListContentView) contentView);+ } else if (contentView instanceof PromptContentViewWithMoreOptionsButton) {+ return Api35Impl.createPromptContentViewWithMoreOptionsButton(+ (PromptContentViewWithMoreOptionsButton) contentView, executor,+ moreOptionsButtonListener);+ }+ }++ return null;+ }++ /**+ * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35).+ */+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)+ @SuppressLint("MissingPermission")+ private static class Api35Impl {+ // Prevent instantiation.+ private Api35Impl() {+ }++ /**+ * Creates an instance of the framework class+ * {@link android.hardware.biometrics.PromptVerticalListContentView} from the given+ * content view.+ *+ * @param contentView The prompt content view to be wrapped.+ * @return An instance of {@link android.hardware.biometrics.PromptVerticalListContentView}.+ */+ @NonNull+ static android.hardware.biometrics.PromptContentView createPromptVerticalListContentView(+ @NonNull PromptVerticalListContentView contentView) {+ android.hardware.biometrics.PromptVerticalListContentView.Builder+ contentViewBuilder =+ new android.hardware.biometrics.PromptVerticalListContentView.Builder();+ if (contentView.getDescription() != null) {+ contentViewBuilder.setDescription(contentView.getDescription());+ }+ contentView.getListItems().forEach(+ it -> {+ if (it instanceof PromptContentItemPlainText) {+ contentViewBuilder.addListItem(+ new android.hardware.biometrics.PromptContentItemPlainText(+ ((PromptContentItemPlainText) it).getText()));+ } else if (it instanceof PromptContentItemBulletedText) {+ contentViewBuilder.addListItem(+ new android.hardware.biometrics.PromptContentItemBulletedText(+ ((PromptContentItemBulletedText) it).getText()));+ }+ });+ return contentViewBuilder.build();+ }++ /**+ * Creates an instance of the framework class+ * {@link android.hardware.biometrics.PromptContentViewWithMoreOptionsButton} from the+ * given content view.+ *+ * @param contentView The prompt content view to be wrapped.+ * @param executor An executor for the more options button callback.+ * @param moreOptionsButtonListener A listener for the more options button press event.+ * @return An instance of+ * {@link android.hardware.biometrics.PromptContentViewWithMoreOptionsButton}.+ */+ @NonNull+ static android.hardware.biometrics.PromptContentView+ createPromptContentViewWithMoreOptionsButton(+ @NonNull PromptContentViewWithMoreOptionsButton contentView,+ @NonNull Executor executor,+ @NonNull DialogInterface.OnClickListener moreOptionsButtonListener) {+ android.hardware.biometrics.PromptContentViewWithMoreOptionsButton.Builder+ contentViewBuilder =+ new android.hardware.biometrics.PromptContentViewWithMoreOptionsButton+ .Builder();+ if (contentView.getDescription() != null) {+ contentViewBuilder.setDescription(contentView.getDescription());+ }+ contentViewBuilder.setMoreOptionsButtonListener(executor, moreOptionsButtonListener);+ return contentViewBuilder.build();+ }+ }+}
@@ -0,0 +1,111 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.biometric;++import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.annotation.RequiresPermission;++/**+ * Contains the information of the template of content view with a more options button for+ * Biometric Prompt.+ * + * This button should be used to provide more options for sign in or other purposes, such as when a+ * user needs to select between multiple app-specific accounts or profiles that are available for+ * sign in.+ * + * Apps should avoid using this when possible because it will create additional steps that the user+ * must navigate through - clicking the more options button will dismiss the prompt, provide the app+ * an opportunity to ask the user for the correct option, and finally allow the app to decide how to+ * proceed once selected.+ *+ * + * Here's how you'd set a PromptContentViewWithMoreOptionsButton on a Biometric+ * Prompt:+ * + * BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()+ * .setTitle(...)+ * .setSubTitle(...)+ * .setContentView(+ * new PromptContentViewWithMoreOptionsButton.Builder()+ * .setDescription("test description")+ * .setMoreOptionsButtonListener(executor, listener)+ * .build()+ * )+ * .build();+ *
+ */+public final class PromptContentViewWithMoreOptionsButton implements PromptContentView {+ static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225;++ private final String mDescription;++ private PromptContentViewWithMoreOptionsButton(@NonNull String description) {+ mDescription = description;+ }++ /**+ * Gets the description for the content view, as set by+ * {@link PromptContentViewWithMoreOptionsButton.Builder#setDescription(String)}.+ *+ * @return The description for the content view, or null if the content view has no description.+ */+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)+ @Nullable+ public String getDescription() {+ return mDescription;+ }++ /**+ * A builder used to set individual options for the+ * {@link PromptContentViewWithMoreOptionsButton} class.+ */+ public static final class Builder {+ private String mDescription;++ /**+ * Optional: Sets a description that will be shown on the content view.+ *+ * @param description The description to display.+ * @return This builder.+ * @throws IllegalArgumentException If description exceeds certain character limit.+ */+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)+ @NonNull+ public Builder setDescription(@NonNull String description) {+ if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) {+ throw new IllegalArgumentException("The character number of description exceeds "+ + MAX_DESCRIPTION_CHARACTER_NUMBER);+ }+ mDescription = description;+ return this;+ }++ /**+ * Creates a {@link PromptContentViewWithMoreOptionsButton}.+ *+ * @return An instance of {@link PromptContentViewWithMoreOptionsButton}.+ */+ @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)+ @NonNull+ public PromptContentViewWithMoreOptionsButton build() {+ return new PromptContentViewWithMoreOptionsButton(mDescription);+ }+ }+}
@@ -0,0 +1,171 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.biometric;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;++import java.util.ArrayList;+import java.util.List;++/**+ * Contains the information of the template of vertical list content view for Biometric Prompt.+ * + * Here's how you'd set a PromptVerticalListContentView on a Biometric Prompt:+ * + * BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()+ * .setTitle(...)+ * .setSubTitle(...)+ * .setContentView(+ * new PromptVerticalListContentView.Builder()+ * .setDescription("test description")+ * .addListItem(new PromptContentItemPlainText("test item 1"))+ * .addListItem(new PromptContentItemPlainText("test item 2"))+ * .addListItem(new PromptContentItemBulletedText("test item 3"))+ * .build()+ * )+ * .build();+ *
+ */+public final class PromptVerticalListContentView implements PromptContentView {+ static final int MAX_ITEM_NUMBER = 20;+ static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640;+ static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225;++ private final List mContentList;+ private final String mDescription;++ private PromptVerticalListContentView(@NonNull List contentList,+ @NonNull String description) {+ mContentList = contentList;+ mDescription = description;+ }++ /**+ * Gets the description for the content view, as set by+ * {@link PromptVerticalListContentView.Builder#setDescription(String)}.+ *+ * @return The description for the content view, or null if the content view has no description.+ */+ @Nullable+ public String getDescription() {+ return mDescription;+ }++ /**+ * Gets the list of items on the content view, as set by+ * {@link PromptVerticalListContentView.Builder#addListItem(PromptContentItem)}.+ *+ * @return The item list on the content view.+ */+ @NonNull+ public List getListItems() {+ return new ArrayList<>(mContentList);+ }++ /**+ * A builder used to set individual options for the {@link PromptVerticalListContentView} class.+ */+ public static final class Builder {+ private final List mContentList = new ArrayList<>();+ private String mDescription;++ /**+ * Optional: Sets a description that will be shown on the content view.+ *+ * @param description The description to display.+ * @return This builder.+ * @throws IllegalArgumentException If description exceeds certain character limit.+ */+ @NonNull+ public Builder setDescription(@NonNull String description) {+ if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) {+ throw new IllegalArgumentException("The character number of description exceeds "+ + MAX_DESCRIPTION_CHARACTER_NUMBER);+ }+ mDescription = description;+ return this;+ }++ /**+ * Optional: Adds a list item in the current row.+ *+ * @param listItem The list item view to display+ * @return This builder.+ * @throws IllegalArgumentException If this list item exceeds certain character limits or+ * the number of list items exceeds certain limit.+ */+ @NonNull+ public Builder addListItem(@NonNull PromptContentItem listItem) {+ mContentList.add(listItem);+ checkItemLimits(listItem);+ return this;+ }++ /**+ * Optional: Adds a list item in the current row.+ *+ * @param listItem The list item view to display+ * @param index The position at which to add the item+ * @return This builder.+ * @throws IllegalArgumentException If this list item exceeds certain character limits or+ * the number of list items exceeds certain limit.+ */+ @NonNull+ public Builder addListItem(@NonNull PromptContentItem listItem, int index) {+ mContentList.add(index, listItem);+ checkItemLimits(listItem);+ return this;+ }++ private void checkItemLimits(@NonNull PromptContentItem listItem) {+ if (doesListItemExceedsCharLimit(listItem)) {+ throw new IllegalArgumentException(+ "The character number of list item exceeds "+ + MAX_EACH_ITEM_CHARACTER_NUMBER);+ }+ if (mContentList.size() > MAX_ITEM_NUMBER) {+ throw new IllegalArgumentException(+ "The number of list items exceeds " + MAX_ITEM_NUMBER);+ }+ }++ private boolean doesListItemExceedsCharLimit(PromptContentItem listItem) {+ if (listItem instanceof PromptContentItemPlainText) {+ return ((PromptContentItemPlainText) listItem).getText().length()+ > MAX_EACH_ITEM_CHARACTER_NUMBER;+ } else if (listItem instanceof PromptContentItemBulletedText) {+ return ((PromptContentItemBulletedText) listItem).getText().length()+ > MAX_EACH_ITEM_CHARACTER_NUMBER;+ } else {+ return false;+ }+ }+++ /**+ * Creates a {@link PromptVerticalListContentView}.+ *+ * @return An instance of {@link PromptVerticalListContentView}.+ */+ @NonNull+ public PromptVerticalListContentView build() {+ return new PromptVerticalListContentView(mContentList, mDescription);+ }+ }+}+
@@ -1,1001 +0,0 @@
-// Signature format: 4.0-package androidx.credentials {-- public final class ClearCredentialStateRequest {- ctor public ClearCredentialStateRequest();- }-- public abstract class CreateCredentialRequest {- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);- method public final android.os.Bundle getCandidateQueryData();- method public final android.os.Bundle getCredentialData();- method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();- method public final String? getOrigin();- method public final String getType();- method public final boolean isAutoSelectAllowed();- method public final boolean isSystemProviderRequired();- method public final boolean preferImmediatelyAvailableCredentials();- property public final android.os.Bundle candidateQueryData;- property public final android.os.Bundle credentialData;- property public final androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo;- property public final boolean isAutoSelectAllowed;- property public final boolean isSystemProviderRequired;- property public final String? origin;- property public final boolean preferImmediatelyAvailableCredentials;- property public final String type;- field public static final androidx.credentials.CreateCredentialRequest.Companion Companion;- }-- public static final class CreateCredentialRequest.Companion {- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);- }-- public static final class CreateCredentialRequest.DisplayInfo {- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);- method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);- method public CharSequence? getUserDisplayName();- method public CharSequence getUserId();- property public final CharSequence? userDisplayName;- property public final CharSequence userId;- field public static final androidx.credentials.CreateCredentialRequest.DisplayInfo.Companion Companion;- }-- public static final class CreateCredentialRequest.DisplayInfo.Companion {- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);- }-- public abstract class CreateCredentialResponse {- method public final android.os.Bundle getData();- method public final String getType();- property public final android.os.Bundle data;- property public final String type;- }-- public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);- }-- public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {- ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);- }-- public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {- ctor public CreatePasswordRequest(String id, String password);- ctor public CreatePasswordRequest(String id, String password, optional String? origin);- ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);- ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials, optional boolean isAutoSelectAllowed);- ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials, boolean isAutoSelectAllowed);- method public String getId();- method public String getPassword();- property public final String id;- property public final String password;- }-- public final class CreatePasswordResponse extends androidx.credentials.CreateCredentialResponse {- ctor public CreatePasswordResponse();- }-- public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {- ctor public CreatePublicKeyCredentialRequest(String requestJson);- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin, optional boolean isAutoSelectAllowed);- ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider, boolean isAutoSelectAllowed);- method public byte[]? getClientDataHash();- method public String getRequestJson();- property public final byte[]? clientDataHash;- property public final String requestJson;- }-- public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {- ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);- method public String getRegistrationResponseJson();- property public final String registrationResponseJson;- }-- public abstract class Credential {- method public final android.os.Bundle getData();- method public final String getType();- property public final android.os.Bundle data;- property public final String type;- }-- public interface CredentialManager {- method public default suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation super kotlin.Unit>);- method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method public static androidx.credentials.CredentialManager create(android.content.Context context);- method public default suspend Object? createCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, kotlin.coroutines.Continuation super androidx.credentials.CreateCredentialResponse>);- method public void createCredentialAsync(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method @RequiresApi(34) public android.app.PendingIntent createSettingsPendingIntent();- method public default suspend Object? getCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation super androidx.credentials.GetCredentialResponse>);- method @RequiresApi(34) public default suspend Object? getCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, kotlin.coroutines.Continuation super androidx.credentials.GetCredentialResponse>);- method public void getCredentialAsync(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method @RequiresApi(34) public void getCredentialAsync(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method @RequiresApi(34) public default suspend Object? prepareGetCredential(androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation super androidx.credentials.PrepareGetCredentialResponse>);- method @RequiresApi(34) public void prepareGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- field public static final androidx.credentials.CredentialManager.Companion Companion;- }-- public static final class CredentialManager.Companion {- method public androidx.credentials.CredentialManager create(android.content.Context context);- }-- public interface CredentialManagerCallback {- method public void onError(E e);- method public void onResult(R result);- }-- public abstract class CredentialOption {- method public final java.util.Set getAllowedProviders();- method public final android.os.Bundle getCandidateQueryData();- method public final android.os.Bundle getRequestData();- method public final String getType();- method public final int getTypePriorityHint();- method public final boolean isAutoSelectAllowed();- method public final boolean isSystemProviderRequired();- property public final java.util.Set allowedProviders;- property public final android.os.Bundle candidateQueryData;- property public final boolean isAutoSelectAllowed;- property public final boolean isSystemProviderRequired;- property public final android.os.Bundle requestData;- property public final String type;- property public final int typePriorityHint;- field public static final androidx.credentials.CredentialOption.Companion Companion;- field public static final int PRIORITY_DEFAULT = 2000; // 0x7d0- field public static final int PRIORITY_OIDC_OR_SIMILAR = 500; // 0x1f4- field public static final int PRIORITY_PASSKEY_OR_SIMILAR = 100; // 0x64- field public static final int PRIORITY_PASSWORD_OR_SIMILAR = 1000; // 0x3e8- }-- public static final class CredentialOption.Companion {- }-- public interface CredentialProvider {- method public boolean isAvailableOnDevice();- method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method public void onCreateCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method public void onGetCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method @RequiresApi(34) public default void onGetCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method @RequiresApi(34) public default void onPrepareCredential(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- }-- public class CustomCredential extends androidx.credentials.Credential {- ctor public CustomCredential(String type, android.os.Bundle data);- }-- public final class GetCredentialRequest {- ctor public GetCredentialRequest(java.util.List extends androidx.credentials.CredentialOption> credentialOptions);- ctor public GetCredentialRequest(java.util.List extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);- ctor public GetCredentialRequest(java.util.List extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);- ctor public GetCredentialRequest(java.util.List extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);- ctor public GetCredentialRequest(java.util.List extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);- method public java.util.List getCredentialOptions();- method public String? getOrigin();- method public boolean getPreferIdentityDocUi();- method public android.content.ComponentName? getPreferUiBrandingComponentName();- method public boolean preferImmediatelyAvailableCredentials();- property public final java.util.List credentialOptions;- property public final String? origin;- property public final boolean preferIdentityDocUi;- property public final boolean preferImmediatelyAvailableCredentials;- property public final android.content.ComponentName? preferUiBrandingComponentName;- }-- public static final class GetCredentialRequest.Builder {- ctor public GetCredentialRequest.Builder();- method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);- method public androidx.credentials.GetCredentialRequest build();- method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List extends androidx.credentials.CredentialOption> credentialOptions);- method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);- method public androidx.credentials.GetCredentialRequest.Builder setPreferIdentityDocUi(boolean preferIdentityDocUi);- method public androidx.credentials.GetCredentialRequest.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);- method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);- }-- public final class GetCredentialResponse {- ctor public GetCredentialResponse(androidx.credentials.Credential credential);- method public androidx.credentials.Credential getCredential();- property public final androidx.credentials.Credential credential;- }-- public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders);- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders, optional int typePriorityHint);- }-- public final class GetPasswordOption extends androidx.credentials.CredentialOption {- ctor public GetPasswordOption();- ctor public GetPasswordOption(optional java.util.Set allowedUserIds);- ctor public GetPasswordOption(optional java.util.Set allowedUserIds, optional boolean isAutoSelectAllowed);- ctor public GetPasswordOption(optional java.util.Set allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders);- method public java.util.Set getAllowedUserIds();- property public final java.util.Set allowedUserIds;- }-- public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {- ctor public GetPublicKeyCredentialOption(String requestJson);- ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);- ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set allowedProviders);- method public byte[]? getClientDataHash();- method public String getRequestJson();- property public final byte[]? clientDataHash;- property public final String requestJson;- }-- public final class PasswordCredential extends androidx.credentials.Credential {- ctor public PasswordCredential(String id, String password);- method public String getId();- method public String getPassword();- property public final String id;- property public final String password;- field public static final androidx.credentials.PasswordCredential.Companion Companion;- field public static final String TYPE_PASSWORD_CREDENTIAL = "android.credentials.TYPE_PASSWORD_CREDENTIAL";- }-- public static final class PasswordCredential.Companion {- }-- @RequiresApi(34) public final class PrepareGetCredentialResponse {- method public kotlin.jvm.functions.Function1? getCredentialTypeDelegate();- method public kotlin.jvm.functions.Function0? getHasAuthResultsDelegate();- method public kotlin.jvm.functions.Function0? getHasRemoteResultsDelegate();- method public androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? getPendingGetCredentialHandle();- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults();- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(String credentialType);- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults();- method public boolean isNullHandlesForTest();- property public final kotlin.jvm.functions.Function1? credentialTypeDelegate;- property public final kotlin.jvm.functions.Function0? hasAuthResultsDelegate;- property public final kotlin.jvm.functions.Function0? hasRemoteResultsDelegate;- property public final boolean isNullHandlesForTest;- property public final androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? pendingGetCredentialHandle;- }-- @RequiresApi(34) public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle {- ctor public PrepareGetCredentialResponse.PendingGetCredentialHandle(android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? frameworkHandle);- }-- @VisibleForTesting public static final class PrepareGetCredentialResponse.TestBuilder {- ctor public PrepareGetCredentialResponse.TestBuilder();- method public androidx.credentials.PrepareGetCredentialResponse build();- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setCredentialTypeDelegate(kotlin.jvm.functions.Function1 super java.lang.String,java.lang.Boolean> handler);- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasAuthResultsDelegate(kotlin.jvm.functions.Function0 handler);- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasRemoteResultsDelegate(kotlin.jvm.functions.Function0 handler);- }-- public final class PublicKeyCredential extends androidx.credentials.Credential {- ctor public PublicKeyCredential(String authenticationResponseJson);- method public String getAuthenticationResponseJson();- property public final String authenticationResponseJson;- field public static final androidx.credentials.PublicKeyCredential.Companion Companion;- field public static final String TYPE_PUBLIC_KEY_CREDENTIAL = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL";- }-- public static final class PublicKeyCredential.Companion {- }--}--package androidx.credentials.exceptions {-- public final class ClearCredentialCustomException extends androidx.credentials.exceptions.ClearCredentialException {- ctor public ClearCredentialCustomException(String type);- ctor public ClearCredentialCustomException(String type, optional CharSequence? errorMessage);- method public String getType();- property public String type;- }-- public abstract class ClearCredentialException extends java.lang.Exception {- }-- public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {- ctor public ClearCredentialInterruptedException();- ctor public ClearCredentialInterruptedException(optional CharSequence? errorMessage);- }-- public final class ClearCredentialProviderConfigurationException extends androidx.credentials.exceptions.ClearCredentialException {- ctor public ClearCredentialProviderConfigurationException();- ctor public ClearCredentialProviderConfigurationException(optional CharSequence? errorMessage);- }-- public final class ClearCredentialUnknownException extends androidx.credentials.exceptions.ClearCredentialException {- ctor public ClearCredentialUnknownException();- ctor public ClearCredentialUnknownException(optional CharSequence? errorMessage);- }-- public final class ClearCredentialUnsupportedException extends androidx.credentials.exceptions.ClearCredentialException {- ctor public ClearCredentialUnsupportedException();- ctor public ClearCredentialUnsupportedException(optional CharSequence? errorMessage);- }-- public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialCancellationException();- ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);- }-- public final class CreateCredentialCustomException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialCustomException(String type);- ctor public CreateCredentialCustomException(String type, optional CharSequence? errorMessage);- method public String getType();- property public String type;- }-- public abstract class CreateCredentialException extends java.lang.Exception {- }-- public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialInterruptedException();- ctor public CreateCredentialInterruptedException(optional CharSequence? errorMessage);- }-- public final class CreateCredentialNoCreateOptionException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialNoCreateOptionException();- ctor public CreateCredentialNoCreateOptionException(optional CharSequence? errorMessage);- }-- public final class CreateCredentialProviderConfigurationException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialProviderConfigurationException();- ctor public CreateCredentialProviderConfigurationException(optional CharSequence? errorMessage);- }-- public final class CreateCredentialUnknownException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialUnknownException();- ctor public CreateCredentialUnknownException(optional CharSequence? errorMessage);- }-- public final class CreateCredentialUnsupportedException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialUnsupportedException();- ctor public CreateCredentialUnsupportedException(optional CharSequence? errorMessage);- }-- public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {- ctor public GetCredentialCancellationException();- ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);- }-- public final class GetCredentialCustomException extends androidx.credentials.exceptions.GetCredentialException {- ctor public GetCredentialCustomException(String type);- ctor public GetCredentialCustomException(String type, optional CharSequence? errorMessage);- method public String getType();- property public String type;- }-- public abstract class GetCredentialException extends java.lang.Exception {- }-- public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {- ctor public GetCredentialInterruptedException();- ctor public GetCredentialInterruptedException(optional CharSequence? errorMessage);- }-- public final class GetCredentialProviderConfigurationException extends androidx.credentials.exceptions.GetCredentialException {- ctor public GetCredentialProviderConfigurationException();- ctor public GetCredentialProviderConfigurationException(optional CharSequence? errorMessage);- }-- public final class GetCredentialUnknownException extends androidx.credentials.exceptions.GetCredentialException {- ctor public GetCredentialUnknownException();- ctor public GetCredentialUnknownException(optional CharSequence? errorMessage);- }-- public final class GetCredentialUnsupportedException extends androidx.credentials.exceptions.GetCredentialException {- ctor public GetCredentialUnsupportedException();- ctor public GetCredentialUnsupportedException(optional CharSequence? errorMessage);- }-- public final class NoCredentialException extends androidx.credentials.exceptions.GetCredentialException {- ctor public NoCredentialException();- ctor public NoCredentialException(optional CharSequence? errorMessage);- }--}--package androidx.credentials.exceptions.domerrors {-- public final class AbortError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public AbortError();- }-- public final class ConstraintError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public ConstraintError();- }-- public final class DataCloneError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public DataCloneError();- }-- public final class DataError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public DataError();- }-- public abstract class DomError {- ctor public DomError(String type);- }-- public final class EncodingError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public EncodingError();- }-- public final class HierarchyRequestError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public HierarchyRequestError();- }-- public final class InUseAttributeError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public InUseAttributeError();- }-- public final class InvalidCharacterError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public InvalidCharacterError();- }-- public final class InvalidModificationError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public InvalidModificationError();- }-- public final class InvalidNodeTypeError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public InvalidNodeTypeError();- }-- public final class InvalidStateError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public InvalidStateError();- }-- public final class NamespaceError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NamespaceError();- }-- public final class NetworkError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NetworkError();- }-- public final class NoModificationAllowedError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NoModificationAllowedError();- }-- public final class NotAllowedError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NotAllowedError();- }-- public final class NotFoundError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NotFoundError();- }-- public final class NotReadableError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NotReadableError();- }-- public final class NotSupportedError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NotSupportedError();- }-- public final class OperationError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public OperationError();- }-- public final class OptOutError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public OptOutError();- }-- public final class QuotaExceededError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public QuotaExceededError();- }-- public final class ReadOnlyError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public ReadOnlyError();- }-- public final class SecurityError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public SecurityError();- }-- public final class SyntaxError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public SyntaxError();- }-- public final class TimeoutError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public TimeoutError();- }-- public final class TransactionInactiveError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public TransactionInactiveError();- }-- public final class UnknownError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public UnknownError();- }-- public final class VersionError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public VersionError();- }-- public final class WrongDocumentError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public WrongDocumentError();- }--}--package androidx.credentials.exceptions.publickeycredential {-- public final class CreatePublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException {- ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);- ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);- method public androidx.credentials.exceptions.domerrors.DomError getDomError();- property public final androidx.credentials.exceptions.domerrors.DomError domError;- }-- public class CreatePublicKeyCredentialException extends androidx.credentials.exceptions.CreateCredentialException {- }-- public final class GetPublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialException {- ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);- ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);- method public androidx.credentials.exceptions.domerrors.DomError getDomError();- property public final androidx.credentials.exceptions.domerrors.DomError domError;- }-- public class GetPublicKeyCredentialException extends androidx.credentials.exceptions.GetCredentialException {- }--}--package androidx.credentials.provider {-- public final class Action {- ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);- method public static androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);- method public android.app.PendingIntent getPendingIntent();- method public CharSequence? getSubtitle();- method public CharSequence getTitle();- property public final android.app.PendingIntent pendingIntent;- property public final CharSequence? subtitle;- property public final CharSequence title;- field public static final androidx.credentials.provider.Action.Companion Companion;- }-- public static final class Action.Builder {- ctor public Action.Builder(CharSequence title, android.app.PendingIntent pendingIntent);- method public androidx.credentials.provider.Action build();- method public androidx.credentials.provider.Action.Builder setSubtitle(CharSequence? subtitle);- }-- public static final class Action.Companion {- method public androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);- }-- public final class AuthenticationAction {- ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);- method @RequiresApi(34) public static androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);- method public android.app.PendingIntent getPendingIntent();- method public CharSequence getTitle();- property public final android.app.PendingIntent pendingIntent;- property public final CharSequence title;- field public static final androidx.credentials.provider.AuthenticationAction.Companion Companion;- }-- public static final class AuthenticationAction.Builder {- ctor public AuthenticationAction.Builder(CharSequence title, android.app.PendingIntent pendingIntent);- method public androidx.credentials.provider.AuthenticationAction build();- }-- public static final class AuthenticationAction.Companion {- method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);- }-- public abstract class BeginCreateCredentialRequest {- ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);- method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);- method public static final androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);- method public final androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();- method public final android.os.Bundle getCandidateQueryData();- method public final String getType();- property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;- property public final android.os.Bundle candidateQueryData;- property public final String type;- field public static final androidx.credentials.provider.BeginCreateCredentialRequest.Companion Companion;- }-- public static final class BeginCreateCredentialRequest.Companion {- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);- method public androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);- }-- public final class BeginCreateCredentialResponse {- ctor public BeginCreateCredentialResponse(optional java.util.List createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);- method public static androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);- method public java.util.List getCreateEntries();- method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();- property public final java.util.List createEntries;- property public final androidx.credentials.provider.RemoteEntry? remoteEntry;- field public static final androidx.credentials.provider.BeginCreateCredentialResponse.Companion Companion;- }-- public static final class BeginCreateCredentialResponse.Builder {- ctor public BeginCreateCredentialResponse.Builder();- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder addCreateEntry(androidx.credentials.provider.CreateEntry createEntry);- method public androidx.credentials.provider.BeginCreateCredentialResponse build();- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setCreateEntries(java.util.List createEntries);- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);- }-- public static final class BeginCreateCredentialResponse.Companion {- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);- method public androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);- }-- public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {- ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);- }-- public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {- ctor public BeginCreatePasswordCredentialRequest(androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);- }-- public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {- ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);- ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);- method @VisibleForTesting public static androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest createForTest(android.os.Bundle data, androidx.credentials.provider.CallingAppInfo? callingAppInfo);- method public byte[]? getClientDataHash();- method public String getRequestJson();- property public final byte[]? clientDataHash;- property public final String requestJson;- }-- public abstract class BeginGetCredentialOption {- method public final android.os.Bundle getCandidateQueryData();- method public final String getId();- method public final String getType();- property public final android.os.Bundle candidateQueryData;- property public final String id;- property public final String type;- }-- public final class BeginGetCredentialRequest {- ctor public BeginGetCredentialRequest(java.util.List extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);- ctor public BeginGetCredentialRequest(java.util.List extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions, optional androidx.credentials.provider.CallingAppInfo? callingAppInfo);- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);- method public static androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);- method public java.util.List getBeginGetCredentialOptions();- method public androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();- property public final java.util.List beginGetCredentialOptions;- property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;- field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;- }-- public static final class BeginGetCredentialRequest.Companion {- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);- method public androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);- }-- public final class BeginGetCredentialResponse {- ctor public BeginGetCredentialResponse(optional java.util.List extends androidx.credentials.provider.CredentialEntry> credentialEntries, optional java.util.List actions, optional java.util.List authenticationActions, optional androidx.credentials.provider.RemoteEntry? remoteEntry);- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);- method public static androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);- method public java.util.List getActions();- method public java.util.List getAuthenticationActions();- method public java.util.List getCredentialEntries();- method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();- property public final java.util.List actions;- property public final java.util.List authenticationActions;- property public final java.util.List credentialEntries;- property public final androidx.credentials.provider.RemoteEntry? remoteEntry;- field public static final androidx.credentials.provider.BeginGetCredentialResponse.Companion Companion;- }-- public static final class BeginGetCredentialResponse.Builder {- ctor public BeginGetCredentialResponse.Builder();- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAction(androidx.credentials.provider.Action action);- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAuthenticationAction(androidx.credentials.provider.AuthenticationAction authenticationAction);- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addCredentialEntry(androidx.credentials.provider.CredentialEntry entry);- method public androidx.credentials.provider.BeginGetCredentialResponse build();- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setActions(java.util.List actions);- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setAuthenticationActions(java.util.List authenticationEntries);- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setCredentialEntries(java.util.List extends androidx.credentials.provider.CredentialEntry> entries);- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);- }-- public static final class BeginGetCredentialResponse.Companion {- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);- method public androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);- }-- public class BeginGetCustomCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {- ctor public BeginGetCustomCredentialOption(String id, String type, android.os.Bundle candidateQueryData);- }-- public final class BeginGetPasswordOption extends androidx.credentials.provider.BeginGetCredentialOption {- ctor public BeginGetPasswordOption(java.util.Set allowedUserIds, android.os.Bundle candidateQueryData, String id);- method @VisibleForTesting public static androidx.credentials.provider.BeginGetPasswordOption createForTest(android.os.Bundle data, String id);- method public java.util.Set getAllowedUserIds();- property public final java.util.Set allowedUserIds;- }-- public final class BeginGetPublicKeyCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {- ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);- ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);- method public byte[]? getClientDataHash();- method public String getRequestJson();- property public final byte[]? clientDataHash;- property public final String requestJson;- }-- public final class CallingAppInfo {- ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);- ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);- method public String? getOrigin(String privilegedAllowlist);- method public String getPackageName();- method public android.content.pm.SigningInfo getSigningInfo();- method public boolean isOriginPopulated();- property public final String packageName;- property public final android.content.pm.SigningInfo signingInfo;- }-- @RequiresApi(26) public final class CreateEntry {- ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);- method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);- method public CharSequence getAccountName();- method public CharSequence? getDescription();- method public android.graphics.drawable.Icon? getIcon();- method public java.time.Instant? getLastUsedTime();- method public Integer? getPasswordCredentialCount();- method public android.app.PendingIntent getPendingIntent();- method public Integer? getPublicKeyCredentialCount();- method public Integer? getTotalCredentialCount();- method public boolean isAutoSelectAllowed();- property public final CharSequence accountName;- property public final CharSequence? description;- property public final android.graphics.drawable.Icon? icon;- property public final boolean isAutoSelectAllowed;- property public final java.time.Instant? lastUsedTime;- property public final android.app.PendingIntent pendingIntent;- field public static final androidx.credentials.provider.CreateEntry.Companion Companion;- }-- public static final class CreateEntry.Builder {- ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);- method public androidx.credentials.provider.CreateEntry build();- method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);- method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);- method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);- method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);- method public androidx.credentials.provider.CreateEntry.Builder setPasswordCredentialCount(int count);- method public androidx.credentials.provider.CreateEntry.Builder setPublicKeyCredentialCount(int count);- method public androidx.credentials.provider.CreateEntry.Builder setTotalCredentialCount(int count);- }-- public static final class CreateEntry.Companion {- method public androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);- }-- public abstract class CredentialEntry {- method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- method public final CharSequence? getAffiliatedDomain();- method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();- method public final CharSequence getEntryGroupId();- method public final boolean isDefaultIconPreferredAsSingleProvider();- property public final CharSequence? affiliatedDomain;- property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;- property public final CharSequence entryGroupId;- property public final boolean isDefaultIconPreferredAsSingleProvider;- field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;- }-- public static final class CredentialEntry.Companion {- method public androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- }-- @RequiresApi(34) public abstract class CredentialProviderService extends android.service.credentials.CredentialProviderService {- ctor public CredentialProviderService();- method public final void onBeginCreateCredential(android.service.credentials.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);- method public abstract void onBeginCreateCredentialRequest(androidx.credentials.provider.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);- method public final void onBeginGetCredential(android.service.credentials.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);- method public abstract void onBeginGetCredentialRequest(androidx.credentials.provider.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);- method public final void onClearCredentialState(android.service.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);- method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);- }-- @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {- ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);- ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);- method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- method public android.graphics.drawable.Icon getIcon();- method public java.time.Instant? getLastUsedTime();- method public android.app.PendingIntent getPendingIntent();- method public CharSequence? getSubtitle();- method public CharSequence getTitle();- method public String getType();- method public CharSequence? getTypeDisplayName();- method public boolean hasDefaultIcon();- method public boolean isAutoSelectAllowed();- method public boolean isAutoSelectAllowedFromOption();- property public final boolean hasDefaultIcon;- property public final android.graphics.drawable.Icon icon;- property public final boolean isAutoSelectAllowed;- property public final boolean isAutoSelectAllowedFromOption;- property public final java.time.Instant? lastUsedTime;- property public final android.app.PendingIntent pendingIntent;- property public final CharSequence? subtitle;- property public final CharSequence title;- property public String type;- property public final CharSequence? typeDisplayName;- field public static final androidx.credentials.provider.CustomCredentialEntry.Companion Companion;- }-- public static final class CustomCredentialEntry.Builder {- ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);- method public androidx.credentials.provider.CustomCredentialEntry build();- method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);- method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);- method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);- method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);- method public androidx.credentials.provider.CustomCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);- method public androidx.credentials.provider.CustomCredentialEntry.Builder setSubtitle(CharSequence? subtitle);- method public androidx.credentials.provider.CustomCredentialEntry.Builder setTypeDisplayName(CharSequence? typeDisplayName);- }-- public static final class CustomCredentialEntry.Companion {- method public androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- }-- public final class IntentHandlerConverters {- method @RequiresApi(34) public static androidx.credentials.provider.BeginGetCredentialResponse? getBeginGetResponse(android.content.Intent);- method @RequiresApi(34) public static android.credentials.CreateCredentialResponse? getCreateCredentialCredentialResponse(android.content.Intent);- method @RequiresApi(34) public static android.credentials.CreateCredentialException? getCreateCredentialException(android.content.Intent);- method @RequiresApi(34) public static android.credentials.GetCredentialException? getGetCredentialException(android.content.Intent);- method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);- }-- @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {- ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);- ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);- method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- method public CharSequence? getDisplayName();- method public android.graphics.drawable.Icon getIcon();- method public java.time.Instant? getLastUsedTime();- method public android.app.PendingIntent getPendingIntent();- method public CharSequence getTypeDisplayName();- method public CharSequence getUsername();- method public boolean hasDefaultIcon();- method public boolean isAutoSelectAllowed();- method public boolean isAutoSelectAllowedFromOption();- property public final CharSequence? displayName;- property public final boolean hasDefaultIcon;- property public final android.graphics.drawable.Icon icon;- property public final boolean isAutoSelectAllowed;- property public final boolean isAutoSelectAllowedFromOption;- property public final java.time.Instant? lastUsedTime;- property public final android.app.PendingIntent pendingIntent;- property public final CharSequence typeDisplayName;- property public final CharSequence username;- field public static final androidx.credentials.provider.PasswordCredentialEntry.Companion Companion;- }-- public static final class PasswordCredentialEntry.Builder {- ctor public PasswordCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption);- method public androidx.credentials.provider.PasswordCredentialEntry build();- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);- }-- public static final class PasswordCredentialEntry.Companion {- method public androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- }-- @RequiresApi(34) public final class PendingIntentHandler {- ctor public PendingIntentHandler();- method public static androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);- method public static androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);- method public static androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);- method public static void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);- method public static void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);- method public static void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);- method public static void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);- method public static void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);- field public static final androidx.credentials.provider.PendingIntentHandler.Companion Companion;- }-- public static final class PendingIntentHandler.Companion {- method public androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);- method public androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);- method public androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);- method public void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);- method public void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);- method public void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);- method public void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);- method public void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);- }-- public final class ProviderClearCredentialStateRequest {- ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;- }-- public final class ProviderCreateCredentialRequest {- ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();- method public androidx.credentials.CreateCredentialRequest getCallingRequest();- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;- property public final androidx.credentials.CreateCredentialRequest callingRequest;- }-- public final class ProviderGetCredentialRequest {- ctor public ProviderGetCredentialRequest(java.util.List extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();- method public java.util.List getCredentialOptions();- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;- property public final java.util.List credentialOptions;- }-- @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {- ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);- ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);- method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- method public CharSequence? getDisplayName();- method public android.graphics.drawable.Icon getIcon();- method public java.time.Instant? getLastUsedTime();- method public android.app.PendingIntent getPendingIntent();- method public CharSequence getTypeDisplayName();- method public CharSequence getUsername();- method public boolean hasDefaultIcon();- method public boolean isAutoSelectAllowed();- method public boolean isAutoSelectAllowedFromOption();- property public final CharSequence? displayName;- property public final boolean hasDefaultIcon;- property public final android.graphics.drawable.Icon icon;- property public final boolean isAutoSelectAllowed;- property public final boolean isAutoSelectAllowedFromOption;- property public final java.time.Instant? lastUsedTime;- property public final android.app.PendingIntent pendingIntent;- property public final CharSequence typeDisplayName;- property public final CharSequence username;- field public static final androidx.credentials.provider.PublicKeyCredentialEntry.Companion Companion;- }-- public static final class PublicKeyCredentialEntry.Builder {- ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);- method public androidx.credentials.provider.PublicKeyCredentialEntry build();- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);- }-- public static final class PublicKeyCredentialEntry.Companion {- method public androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- }-- public final class RemoteEntry {- ctor public RemoteEntry(android.app.PendingIntent pendingIntent);- method public static androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);- method public android.app.PendingIntent getPendingIntent();- property public final android.app.PendingIntent pendingIntent;- field public static final androidx.credentials.provider.RemoteEntry.Companion Companion;- }-- public static final class RemoteEntry.Builder {- ctor public RemoteEntry.Builder(android.app.PendingIntent pendingIntent);- method public androidx.credentials.provider.RemoteEntry build();- }-- public static final class RemoteEntry.Companion {- method public androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);- }--}-
@@ -1,1001 +0,0 @@
-// Signature format: 4.0-package androidx.credentials {-- public final class ClearCredentialStateRequest {- ctor public ClearCredentialStateRequest();- }-- public abstract class CreateCredentialRequest {- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);- method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);- method public final android.os.Bundle getCandidateQueryData();- method public final android.os.Bundle getCredentialData();- method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();- method public final String? getOrigin();- method public final String getType();- method public final boolean isAutoSelectAllowed();- method public final boolean isSystemProviderRequired();- method public final boolean preferImmediatelyAvailableCredentials();- property public final android.os.Bundle candidateQueryData;- property public final android.os.Bundle credentialData;- property public final androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo;- property public final boolean isAutoSelectAllowed;- property public final boolean isSystemProviderRequired;- property public final String? origin;- property public final boolean preferImmediatelyAvailableCredentials;- property public final String type;- field public static final androidx.credentials.CreateCredentialRequest.Companion Companion;- }-- public static final class CreateCredentialRequest.Companion {- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);- }-- public static final class CreateCredentialRequest.DisplayInfo {- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);- ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);- method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);- method public CharSequence? getUserDisplayName();- method public CharSequence getUserId();- property public final CharSequence? userDisplayName;- property public final CharSequence userId;- field public static final androidx.credentials.CreateCredentialRequest.DisplayInfo.Companion Companion;- }-- public static final class CreateCredentialRequest.DisplayInfo.Companion {- method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);- }-- public abstract class CreateCredentialResponse {- method public final android.os.Bundle getData();- method public final String getType();- property public final android.os.Bundle data;- property public final String type;- }-- public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);- ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);- }-- public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {- ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);- }-- public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {- ctor public CreatePasswordRequest(String id, String password);- ctor public CreatePasswordRequest(String id, String password, optional String? origin);- ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);- ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials, optional boolean isAutoSelectAllowed);- ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials, boolean isAutoSelectAllowed);- method public String getId();- method public String getPassword();- property public final String id;- property public final String password;- }-- public final class CreatePasswordResponse extends androidx.credentials.CreateCredentialResponse {- ctor public CreatePasswordResponse();- }-- public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {- ctor public CreatePublicKeyCredentialRequest(String requestJson);- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin, optional boolean isAutoSelectAllowed);- ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider, boolean isAutoSelectAllowed);- method public byte[]? getClientDataHash();- method public String getRequestJson();- property public final byte[]? clientDataHash;- property public final String requestJson;- }-- public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {- ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);- method public String getRegistrationResponseJson();- property public final String registrationResponseJson;- }-- public abstract class Credential {- method public final android.os.Bundle getData();- method public final String getType();- property public final android.os.Bundle data;- property public final String type;- }-- public interface CredentialManager {- method public default suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation super kotlin.Unit>);- method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method public static androidx.credentials.CredentialManager create(android.content.Context context);- method public default suspend Object? createCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, kotlin.coroutines.Continuation super androidx.credentials.CreateCredentialResponse>);- method public void createCredentialAsync(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method @RequiresApi(34) public android.app.PendingIntent createSettingsPendingIntent();- method public default suspend Object? getCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation super androidx.credentials.GetCredentialResponse>);- method @RequiresApi(34) public default suspend Object? getCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, kotlin.coroutines.Continuation super androidx.credentials.GetCredentialResponse>);- method public void getCredentialAsync(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method @RequiresApi(34) public void getCredentialAsync(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method @RequiresApi(34) public default suspend Object? prepareGetCredential(androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation super androidx.credentials.PrepareGetCredentialResponse>);- method @RequiresApi(34) public void prepareGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- field public static final androidx.credentials.CredentialManager.Companion Companion;- }-- public static final class CredentialManager.Companion {- method public androidx.credentials.CredentialManager create(android.content.Context context);- }-- public interface CredentialManagerCallback {- method public void onError(E e);- method public void onResult(R result);- }-- public abstract class CredentialOption {- method public final java.util.Set getAllowedProviders();- method public final android.os.Bundle getCandidateQueryData();- method public final android.os.Bundle getRequestData();- method public final String getType();- method public final int getTypePriorityHint();- method public final boolean isAutoSelectAllowed();- method public final boolean isSystemProviderRequired();- property public final java.util.Set allowedProviders;- property public final android.os.Bundle candidateQueryData;- property public final boolean isAutoSelectAllowed;- property public final boolean isSystemProviderRequired;- property public final android.os.Bundle requestData;- property public final String type;- property public final int typePriorityHint;- field public static final androidx.credentials.CredentialOption.Companion Companion;- field public static final int PRIORITY_DEFAULT = 2000; // 0x7d0- field public static final int PRIORITY_OIDC_OR_SIMILAR = 500; // 0x1f4- field public static final int PRIORITY_PASSKEY_OR_SIMILAR = 100; // 0x64- field public static final int PRIORITY_PASSWORD_OR_SIMILAR = 1000; // 0x3e8- }-- public static final class CredentialOption.Companion {- }-- public interface CredentialProvider {- method public boolean isAvailableOnDevice();- method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method public void onCreateCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method public void onGetCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method @RequiresApi(34) public default void onGetCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- method @RequiresApi(34) public default void onPrepareCredential(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);- }-- public class CustomCredential extends androidx.credentials.Credential {- ctor public CustomCredential(String type, android.os.Bundle data);- }-- public final class GetCredentialRequest {- ctor public GetCredentialRequest(java.util.List extends androidx.credentials.CredentialOption> credentialOptions);- ctor public GetCredentialRequest(java.util.List extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);- ctor public GetCredentialRequest(java.util.List extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);- ctor public GetCredentialRequest(java.util.List extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);- ctor public GetCredentialRequest(java.util.List extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);- method public java.util.List getCredentialOptions();- method public String? getOrigin();- method public boolean getPreferIdentityDocUi();- method public android.content.ComponentName? getPreferUiBrandingComponentName();- method public boolean preferImmediatelyAvailableCredentials();- property public final java.util.List credentialOptions;- property public final String? origin;- property public final boolean preferIdentityDocUi;- property public final boolean preferImmediatelyAvailableCredentials;- property public final android.content.ComponentName? preferUiBrandingComponentName;- }-- public static final class GetCredentialRequest.Builder {- ctor public GetCredentialRequest.Builder();- method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);- method public androidx.credentials.GetCredentialRequest build();- method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List extends androidx.credentials.CredentialOption> credentialOptions);- method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);- method public androidx.credentials.GetCredentialRequest.Builder setPreferIdentityDocUi(boolean preferIdentityDocUi);- method public androidx.credentials.GetCredentialRequest.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);- method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);- }-- public final class GetCredentialResponse {- ctor public GetCredentialResponse(androidx.credentials.Credential credential);- method public androidx.credentials.Credential getCredential();- property public final androidx.credentials.Credential credential;- }-- public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders);- ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders, optional int typePriorityHint);- }-- public final class GetPasswordOption extends androidx.credentials.CredentialOption {- ctor public GetPasswordOption();- ctor public GetPasswordOption(optional java.util.Set allowedUserIds);- ctor public GetPasswordOption(optional java.util.Set allowedUserIds, optional boolean isAutoSelectAllowed);- ctor public GetPasswordOption(optional java.util.Set allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders);- method public java.util.Set getAllowedUserIds();- property public final java.util.Set allowedUserIds;- }-- public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {- ctor public GetPublicKeyCredentialOption(String requestJson);- ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);- ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set allowedProviders);- method public byte[]? getClientDataHash();- method public String getRequestJson();- property public final byte[]? clientDataHash;- property public final String requestJson;- }-- public final class PasswordCredential extends androidx.credentials.Credential {- ctor public PasswordCredential(String id, String password);- method public String getId();- method public String getPassword();- property public final String id;- property public final String password;- field public static final androidx.credentials.PasswordCredential.Companion Companion;- field public static final String TYPE_PASSWORD_CREDENTIAL = "android.credentials.TYPE_PASSWORD_CREDENTIAL";- }-- public static final class PasswordCredential.Companion {- }-- @RequiresApi(34) public final class PrepareGetCredentialResponse {- method public kotlin.jvm.functions.Function1? getCredentialTypeDelegate();- method public kotlin.jvm.functions.Function0? getHasAuthResultsDelegate();- method public kotlin.jvm.functions.Function0? getHasRemoteResultsDelegate();- method public androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? getPendingGetCredentialHandle();- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults();- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(String credentialType);- method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults();- method public boolean isNullHandlesForTest();- property public final kotlin.jvm.functions.Function1? credentialTypeDelegate;- property public final kotlin.jvm.functions.Function0? hasAuthResultsDelegate;- property public final kotlin.jvm.functions.Function0? hasRemoteResultsDelegate;- property public final boolean isNullHandlesForTest;- property public final androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? pendingGetCredentialHandle;- }-- @RequiresApi(34) public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle {- ctor public PrepareGetCredentialResponse.PendingGetCredentialHandle(android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? frameworkHandle);- }-- @VisibleForTesting public static final class PrepareGetCredentialResponse.TestBuilder {- ctor public PrepareGetCredentialResponse.TestBuilder();- method public androidx.credentials.PrepareGetCredentialResponse build();- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setCredentialTypeDelegate(kotlin.jvm.functions.Function1 super java.lang.String,java.lang.Boolean> handler);- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasAuthResultsDelegate(kotlin.jvm.functions.Function0 handler);- method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasRemoteResultsDelegate(kotlin.jvm.functions.Function0 handler);- }-- public final class PublicKeyCredential extends androidx.credentials.Credential {- ctor public PublicKeyCredential(String authenticationResponseJson);- method public String getAuthenticationResponseJson();- property public final String authenticationResponseJson;- field public static final androidx.credentials.PublicKeyCredential.Companion Companion;- field public static final String TYPE_PUBLIC_KEY_CREDENTIAL = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL";- }-- public static final class PublicKeyCredential.Companion {- }--}--package androidx.credentials.exceptions {-- public final class ClearCredentialCustomException extends androidx.credentials.exceptions.ClearCredentialException {- ctor public ClearCredentialCustomException(String type);- ctor public ClearCredentialCustomException(String type, optional CharSequence? errorMessage);- method public String getType();- property public String type;- }-- public abstract class ClearCredentialException extends java.lang.Exception {- }-- public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {- ctor public ClearCredentialInterruptedException();- ctor public ClearCredentialInterruptedException(optional CharSequence? errorMessage);- }-- public final class ClearCredentialProviderConfigurationException extends androidx.credentials.exceptions.ClearCredentialException {- ctor public ClearCredentialProviderConfigurationException();- ctor public ClearCredentialProviderConfigurationException(optional CharSequence? errorMessage);- }-- public final class ClearCredentialUnknownException extends androidx.credentials.exceptions.ClearCredentialException {- ctor public ClearCredentialUnknownException();- ctor public ClearCredentialUnknownException(optional CharSequence? errorMessage);- }-- public final class ClearCredentialUnsupportedException extends androidx.credentials.exceptions.ClearCredentialException {- ctor public ClearCredentialUnsupportedException();- ctor public ClearCredentialUnsupportedException(optional CharSequence? errorMessage);- }-- public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialCancellationException();- ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);- }-- public final class CreateCredentialCustomException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialCustomException(String type);- ctor public CreateCredentialCustomException(String type, optional CharSequence? errorMessage);- method public String getType();- property public String type;- }-- public abstract class CreateCredentialException extends java.lang.Exception {- }-- public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialInterruptedException();- ctor public CreateCredentialInterruptedException(optional CharSequence? errorMessage);- }-- public final class CreateCredentialNoCreateOptionException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialNoCreateOptionException();- ctor public CreateCredentialNoCreateOptionException(optional CharSequence? errorMessage);- }-- public final class CreateCredentialProviderConfigurationException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialProviderConfigurationException();- ctor public CreateCredentialProviderConfigurationException(optional CharSequence? errorMessage);- }-- public final class CreateCredentialUnknownException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialUnknownException();- ctor public CreateCredentialUnknownException(optional CharSequence? errorMessage);- }-- public final class CreateCredentialUnsupportedException extends androidx.credentials.exceptions.CreateCredentialException {- ctor public CreateCredentialUnsupportedException();- ctor public CreateCredentialUnsupportedException(optional CharSequence? errorMessage);- }-- public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {- ctor public GetCredentialCancellationException();- ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);- }-- public final class GetCredentialCustomException extends androidx.credentials.exceptions.GetCredentialException {- ctor public GetCredentialCustomException(String type);- ctor public GetCredentialCustomException(String type, optional CharSequence? errorMessage);- method public String getType();- property public String type;- }-- public abstract class GetCredentialException extends java.lang.Exception {- }-- public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {- ctor public GetCredentialInterruptedException();- ctor public GetCredentialInterruptedException(optional CharSequence? errorMessage);- }-- public final class GetCredentialProviderConfigurationException extends androidx.credentials.exceptions.GetCredentialException {- ctor public GetCredentialProviderConfigurationException();- ctor public GetCredentialProviderConfigurationException(optional CharSequence? errorMessage);- }-- public final class GetCredentialUnknownException extends androidx.credentials.exceptions.GetCredentialException {- ctor public GetCredentialUnknownException();- ctor public GetCredentialUnknownException(optional CharSequence? errorMessage);- }-- public final class GetCredentialUnsupportedException extends androidx.credentials.exceptions.GetCredentialException {- ctor public GetCredentialUnsupportedException();- ctor public GetCredentialUnsupportedException(optional CharSequence? errorMessage);- }-- public final class NoCredentialException extends androidx.credentials.exceptions.GetCredentialException {- ctor public NoCredentialException();- ctor public NoCredentialException(optional CharSequence? errorMessage);- }--}--package androidx.credentials.exceptions.domerrors {-- public final class AbortError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public AbortError();- }-- public final class ConstraintError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public ConstraintError();- }-- public final class DataCloneError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public DataCloneError();- }-- public final class DataError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public DataError();- }-- public abstract class DomError {- ctor public DomError(String type);- }-- public final class EncodingError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public EncodingError();- }-- public final class HierarchyRequestError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public HierarchyRequestError();- }-- public final class InUseAttributeError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public InUseAttributeError();- }-- public final class InvalidCharacterError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public InvalidCharacterError();- }-- public final class InvalidModificationError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public InvalidModificationError();- }-- public final class InvalidNodeTypeError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public InvalidNodeTypeError();- }-- public final class InvalidStateError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public InvalidStateError();- }-- public final class NamespaceError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NamespaceError();- }-- public final class NetworkError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NetworkError();- }-- public final class NoModificationAllowedError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NoModificationAllowedError();- }-- public final class NotAllowedError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NotAllowedError();- }-- public final class NotFoundError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NotFoundError();- }-- public final class NotReadableError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NotReadableError();- }-- public final class NotSupportedError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public NotSupportedError();- }-- public final class OperationError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public OperationError();- }-- public final class OptOutError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public OptOutError();- }-- public final class QuotaExceededError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public QuotaExceededError();- }-- public final class ReadOnlyError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public ReadOnlyError();- }-- public final class SecurityError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public SecurityError();- }-- public final class SyntaxError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public SyntaxError();- }-- public final class TimeoutError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public TimeoutError();- }-- public final class TransactionInactiveError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public TransactionInactiveError();- }-- public final class UnknownError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public UnknownError();- }-- public final class VersionError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public VersionError();- }-- public final class WrongDocumentError extends androidx.credentials.exceptions.domerrors.DomError {- ctor public WrongDocumentError();- }--}--package androidx.credentials.exceptions.publickeycredential {-- public final class CreatePublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException {- ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);- ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);- method public androidx.credentials.exceptions.domerrors.DomError getDomError();- property public final androidx.credentials.exceptions.domerrors.DomError domError;- }-- public class CreatePublicKeyCredentialException extends androidx.credentials.exceptions.CreateCredentialException {- }-- public final class GetPublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialException {- ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);- ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);- method public androidx.credentials.exceptions.domerrors.DomError getDomError();- property public final androidx.credentials.exceptions.domerrors.DomError domError;- }-- public class GetPublicKeyCredentialException extends androidx.credentials.exceptions.GetCredentialException {- }--}--package androidx.credentials.provider {-- public final class Action {- ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);- method public static androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);- method public android.app.PendingIntent getPendingIntent();- method public CharSequence? getSubtitle();- method public CharSequence getTitle();- property public final android.app.PendingIntent pendingIntent;- property public final CharSequence? subtitle;- property public final CharSequence title;- field public static final androidx.credentials.provider.Action.Companion Companion;- }-- public static final class Action.Builder {- ctor public Action.Builder(CharSequence title, android.app.PendingIntent pendingIntent);- method public androidx.credentials.provider.Action build();- method public androidx.credentials.provider.Action.Builder setSubtitle(CharSequence? subtitle);- }-- public static final class Action.Companion {- method public androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);- }-- public final class AuthenticationAction {- ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);- method @RequiresApi(34) public static androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);- method public android.app.PendingIntent getPendingIntent();- method public CharSequence getTitle();- property public final android.app.PendingIntent pendingIntent;- property public final CharSequence title;- field public static final androidx.credentials.provider.AuthenticationAction.Companion Companion;- }-- public static final class AuthenticationAction.Builder {- ctor public AuthenticationAction.Builder(CharSequence title, android.app.PendingIntent pendingIntent);- method public androidx.credentials.provider.AuthenticationAction build();- }-- public static final class AuthenticationAction.Companion {- method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);- }-- public abstract class BeginCreateCredentialRequest {- ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);- method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);- method public static final androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);- method public final androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();- method public final android.os.Bundle getCandidateQueryData();- method public final String getType();- property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;- property public final android.os.Bundle candidateQueryData;- property public final String type;- field public static final androidx.credentials.provider.BeginCreateCredentialRequest.Companion Companion;- }-- public static final class BeginCreateCredentialRequest.Companion {- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);- method public androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);- }-- public final class BeginCreateCredentialResponse {- ctor public BeginCreateCredentialResponse(optional java.util.List createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);- method public static androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);- method public java.util.List getCreateEntries();- method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();- property public final java.util.List createEntries;- property public final androidx.credentials.provider.RemoteEntry? remoteEntry;- field public static final androidx.credentials.provider.BeginCreateCredentialResponse.Companion Companion;- }-- public static final class BeginCreateCredentialResponse.Builder {- ctor public BeginCreateCredentialResponse.Builder();- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder addCreateEntry(androidx.credentials.provider.CreateEntry createEntry);- method public androidx.credentials.provider.BeginCreateCredentialResponse build();- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setCreateEntries(java.util.List createEntries);- method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);- }-- public static final class BeginCreateCredentialResponse.Companion {- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);- method public androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);- }-- public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {- ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);- }-- public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {- ctor public BeginCreatePasswordCredentialRequest(androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);- }-- public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {- ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);- ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);- method @VisibleForTesting public static androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest createForTest(android.os.Bundle data, androidx.credentials.provider.CallingAppInfo? callingAppInfo);- method public byte[]? getClientDataHash();- method public String getRequestJson();- property public final byte[]? clientDataHash;- property public final String requestJson;- }-- public abstract class BeginGetCredentialOption {- method public final android.os.Bundle getCandidateQueryData();- method public final String getId();- method public final String getType();- property public final android.os.Bundle candidateQueryData;- property public final String id;- property public final String type;- }-- public final class BeginGetCredentialRequest {- ctor public BeginGetCredentialRequest(java.util.List extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);- ctor public BeginGetCredentialRequest(java.util.List extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions, optional androidx.credentials.provider.CallingAppInfo? callingAppInfo);- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);- method public static androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);- method public java.util.List getBeginGetCredentialOptions();- method public androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();- property public final java.util.List beginGetCredentialOptions;- property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;- field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;- }-- public static final class BeginGetCredentialRequest.Companion {- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);- method public androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);- }-- public final class BeginGetCredentialResponse {- ctor public BeginGetCredentialResponse(optional java.util.List extends androidx.credentials.provider.CredentialEntry> credentialEntries, optional java.util.List actions, optional java.util.List authenticationActions, optional androidx.credentials.provider.RemoteEntry? remoteEntry);- method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);- method public static androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);- method public java.util.List getActions();- method public java.util.List getAuthenticationActions();- method public java.util.List getCredentialEntries();- method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();- property public final java.util.List actions;- property public final java.util.List authenticationActions;- property public final java.util.List credentialEntries;- property public final androidx.credentials.provider.RemoteEntry? remoteEntry;- field public static final androidx.credentials.provider.BeginGetCredentialResponse.Companion Companion;- }-- public static final class BeginGetCredentialResponse.Builder {- ctor public BeginGetCredentialResponse.Builder();- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAction(androidx.credentials.provider.Action action);- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAuthenticationAction(androidx.credentials.provider.AuthenticationAction authenticationAction);- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addCredentialEntry(androidx.credentials.provider.CredentialEntry entry);- method public androidx.credentials.provider.BeginGetCredentialResponse build();- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setActions(java.util.List actions);- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setAuthenticationActions(java.util.List authenticationEntries);- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setCredentialEntries(java.util.List extends androidx.credentials.provider.CredentialEntry> entries);- method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);- }-- public static final class BeginGetCredentialResponse.Companion {- method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);- method public androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);- }-- public class BeginGetCustomCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {- ctor public BeginGetCustomCredentialOption(String id, String type, android.os.Bundle candidateQueryData);- }-- public final class BeginGetPasswordOption extends androidx.credentials.provider.BeginGetCredentialOption {- ctor public BeginGetPasswordOption(java.util.Set allowedUserIds, android.os.Bundle candidateQueryData, String id);- method @VisibleForTesting public static androidx.credentials.provider.BeginGetPasswordOption createForTest(android.os.Bundle data, String id);- method public java.util.Set getAllowedUserIds();- property public final java.util.Set allowedUserIds;- }-- public final class BeginGetPublicKeyCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {- ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);- ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);- method public byte[]? getClientDataHash();- method public String getRequestJson();- property public final byte[]? clientDataHash;- property public final String requestJson;- }-- public final class CallingAppInfo {- ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);- ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);- method public String? getOrigin(String privilegedAllowlist);- method public String getPackageName();- method public android.content.pm.SigningInfo getSigningInfo();- method public boolean isOriginPopulated();- property public final String packageName;- property public final android.content.pm.SigningInfo signingInfo;- }-- @RequiresApi(26) public final class CreateEntry {- ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);- method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);- method public CharSequence getAccountName();- method public CharSequence? getDescription();- method public android.graphics.drawable.Icon? getIcon();- method public java.time.Instant? getLastUsedTime();- method public Integer? getPasswordCredentialCount();- method public android.app.PendingIntent getPendingIntent();- method public Integer? getPublicKeyCredentialCount();- method public Integer? getTotalCredentialCount();- method public boolean isAutoSelectAllowed();- property public final CharSequence accountName;- property public final CharSequence? description;- property public final android.graphics.drawable.Icon? icon;- property public final boolean isAutoSelectAllowed;- property public final java.time.Instant? lastUsedTime;- property public final android.app.PendingIntent pendingIntent;- field public static final androidx.credentials.provider.CreateEntry.Companion Companion;- }-- public static final class CreateEntry.Builder {- ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);- method public androidx.credentials.provider.CreateEntry build();- method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);- method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);- method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);- method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);- method public androidx.credentials.provider.CreateEntry.Builder setPasswordCredentialCount(int count);- method public androidx.credentials.provider.CreateEntry.Builder setPublicKeyCredentialCount(int count);- method public androidx.credentials.provider.CreateEntry.Builder setTotalCredentialCount(int count);- }-- public static final class CreateEntry.Companion {- method public androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);- }-- public abstract class CredentialEntry {- method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- method public final CharSequence? getAffiliatedDomain();- method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();- method public final CharSequence getEntryGroupId();- method public final boolean isDefaultIconPreferredAsSingleProvider();- property public final CharSequence? affiliatedDomain;- property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;- property public final CharSequence entryGroupId;- property public final boolean isDefaultIconPreferredAsSingleProvider;- field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;- }-- public static final class CredentialEntry.Companion {- method public androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- }-- @RequiresApi(34) public abstract class CredentialProviderService extends android.service.credentials.CredentialProviderService {- ctor public CredentialProviderService();- method public final void onBeginCreateCredential(android.service.credentials.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);- method public abstract void onBeginCreateCredentialRequest(androidx.credentials.provider.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);- method public final void onBeginGetCredential(android.service.credentials.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);- method public abstract void onBeginGetCredentialRequest(androidx.credentials.provider.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);- method public final void onClearCredentialState(android.service.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);- method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);- }-- @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {- ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);- ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);- method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- method public android.graphics.drawable.Icon getIcon();- method public java.time.Instant? getLastUsedTime();- method public android.app.PendingIntent getPendingIntent();- method public CharSequence? getSubtitle();- method public CharSequence getTitle();- method public String getType();- method public CharSequence? getTypeDisplayName();- method public boolean hasDefaultIcon();- method public boolean isAutoSelectAllowed();- method public boolean isAutoSelectAllowedFromOption();- property public final boolean hasDefaultIcon;- property public final android.graphics.drawable.Icon icon;- property public final boolean isAutoSelectAllowed;- property public final boolean isAutoSelectAllowedFromOption;- property public final java.time.Instant? lastUsedTime;- property public final android.app.PendingIntent pendingIntent;- property public final CharSequence? subtitle;- property public final CharSequence title;- property public String type;- property public final CharSequence? typeDisplayName;- field public static final androidx.credentials.provider.CustomCredentialEntry.Companion Companion;- }-- public static final class CustomCredentialEntry.Builder {- ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);- method public androidx.credentials.provider.CustomCredentialEntry build();- method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);- method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);- method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);- method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);- method public androidx.credentials.provider.CustomCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);- method public androidx.credentials.provider.CustomCredentialEntry.Builder setSubtitle(CharSequence? subtitle);- method public androidx.credentials.provider.CustomCredentialEntry.Builder setTypeDisplayName(CharSequence? typeDisplayName);- }-- public static final class CustomCredentialEntry.Companion {- method public androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- }-- public final class IntentHandlerConverters {- method @RequiresApi(34) public static androidx.credentials.provider.BeginGetCredentialResponse? getBeginGetResponse(android.content.Intent);- method @RequiresApi(34) public static android.credentials.CreateCredentialResponse? getCreateCredentialCredentialResponse(android.content.Intent);- method @RequiresApi(34) public static android.credentials.CreateCredentialException? getCreateCredentialException(android.content.Intent);- method @RequiresApi(34) public static android.credentials.GetCredentialException? getGetCredentialException(android.content.Intent);- method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);- }-- @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {- ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);- ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);- method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- method public CharSequence? getDisplayName();- method public android.graphics.drawable.Icon getIcon();- method public java.time.Instant? getLastUsedTime();- method public android.app.PendingIntent getPendingIntent();- method public CharSequence getTypeDisplayName();- method public CharSequence getUsername();- method public boolean hasDefaultIcon();- method public boolean isAutoSelectAllowed();- method public boolean isAutoSelectAllowedFromOption();- property public final CharSequence? displayName;- property public final boolean hasDefaultIcon;- property public final android.graphics.drawable.Icon icon;- property public final boolean isAutoSelectAllowed;- property public final boolean isAutoSelectAllowedFromOption;- property public final java.time.Instant? lastUsedTime;- property public final android.app.PendingIntent pendingIntent;- property public final CharSequence typeDisplayName;- property public final CharSequence username;- field public static final androidx.credentials.provider.PasswordCredentialEntry.Companion Companion;- }-- public static final class PasswordCredentialEntry.Builder {- ctor public PasswordCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption);- method public androidx.credentials.provider.PasswordCredentialEntry build();- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);- method public androidx.credentials.provider.PasswordCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);- }-- public static final class PasswordCredentialEntry.Companion {- method public androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- }-- @RequiresApi(34) public final class PendingIntentHandler {- ctor public PendingIntentHandler();- method public static androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);- method public static androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);- method public static androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);- method public static void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);- method public static void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);- method public static void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);- method public static void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);- method public static void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);- field public static final androidx.credentials.provider.PendingIntentHandler.Companion Companion;- }-- public static final class PendingIntentHandler.Companion {- method public androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);- method public androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);- method public androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);- method public void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);- method public void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);- method public void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);- method public void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);- method public void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);- }-- public final class ProviderClearCredentialStateRequest {- ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;- }-- public final class ProviderCreateCredentialRequest {- ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();- method public androidx.credentials.CreateCredentialRequest getCallingRequest();- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;- property public final androidx.credentials.CreateCredentialRequest callingRequest;- }-- public final class ProviderGetCredentialRequest {- ctor public ProviderGetCredentialRequest(java.util.List extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);- method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();- method public java.util.List getCredentialOptions();- property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;- property public final java.util.List credentialOptions;- }-- @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {- ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);- ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);- method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- method public CharSequence? getDisplayName();- method public android.graphics.drawable.Icon getIcon();- method public java.time.Instant? getLastUsedTime();- method public android.app.PendingIntent getPendingIntent();- method public CharSequence getTypeDisplayName();- method public CharSequence getUsername();- method public boolean hasDefaultIcon();- method public boolean isAutoSelectAllowed();- method public boolean isAutoSelectAllowedFromOption();- property public final CharSequence? displayName;- property public final boolean hasDefaultIcon;- property public final android.graphics.drawable.Icon icon;- property public final boolean isAutoSelectAllowed;- property public final boolean isAutoSelectAllowedFromOption;- property public final java.time.Instant? lastUsedTime;- property public final android.app.PendingIntent pendingIntent;- property public final CharSequence typeDisplayName;- property public final CharSequence username;- field public static final androidx.credentials.provider.PublicKeyCredentialEntry.Companion Companion;- }-- public static final class PublicKeyCredentialEntry.Builder {- ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);- method public androidx.credentials.provider.PublicKeyCredentialEntry build();- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);- method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);- }-- public static final class PublicKeyCredentialEntry.Companion {- method public androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);- }-- public final class RemoteEntry {- ctor public RemoteEntry(android.app.PendingIntent pendingIntent);- method public static androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);- method public android.app.PendingIntent getPendingIntent();- property public final android.app.PendingIntent pendingIntent;- field public static final androidx.credentials.provider.RemoteEntry.Companion Companion;- }-- public static final class RemoteEntry.Builder {- ctor public RemoteEntry.Builder(android.app.PendingIntent pendingIntent);- method public androidx.credentials.provider.RemoteEntry build();- }-- public static final class RemoteEntry.Companion {- method public androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);- }--}-
@@ -0,0 +1,111 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.credentials;++import static com.google.common.truth.Truth.assertThat;++import static org.junit.Assert.assertNotNull;++import android.content.Context;+import android.credentials.Credential;+import android.os.OutcomeReceiver;+import android.widget.EditText;++import androidx.annotation.RequiresApi;+import androidx.credentials.internal.FrameworkImplHelper;+import androidx.test.core.app.ApplicationProvider;+import androidx.test.filters.SdkSuppress;++import kotlin.Unit;++import org.junit.Test;++import java.util.Collections;+import java.util.concurrent.CountDownLatch;+import java.util.concurrent.TimeUnit;+import java.util.concurrent.atomic.AtomicReference;++@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")+public class CredentialManagerViewHandlerJavaTest {+ private final Context mContext = ApplicationProvider.getApplicationContext();++ private static final GetCredentialRequest GET_CRED_PASSWORD_REQ =+ new GetCredentialRequest.Builder()+ .setCredentialOptions(Collections.singletonList(+ new GetPasswordOption())).build();+ private static final android.credentials.GetCredentialRequest GET_CRED_PASSWORD_FRAMEWORK_REQ =+ FrameworkImplHelper.convertGetRequestToFrameworkClass(GET_CRED_PASSWORD_REQ);++ @Test+ @RequiresApi(35)+ public void setPendingCredentialRequest_frameworkAttrSetSuccessfully() {+ EditText editText = new EditText(mContext);++ PendingGetCredentialRequest pendingGetCredentialRequest = new PendingGetCredentialRequest(+ GET_CRED_PASSWORD_REQ,+ (response) -> Unit.INSTANCE);++ CredentialManagerViewHandler.setPendingGetCredentialRequest(editText,+ pendingGetCredentialRequest);++ assertNotNull(editText.getPendingCredentialRequest());+ TestUtilsKt.equals(editText.getPendingCredentialRequest(),+ GET_CRED_PASSWORD_FRAMEWORK_REQ);+ assertThat(editText.getPendingCredentialCallback()).isInstanceOf(+ OutcomeReceiver.class+ );+ }++ @Test+ @RequiresApi(35)+ public void setPendingCredentialRequest_callbackInvokedSuccessfully()+ throws InterruptedException {+ CountDownLatch latch1 = new CountDownLatch(1);+ AtomicReference getCredentialResponse = new AtomicReference<>();+ EditText editText = new EditText(mContext);++ PendingGetCredentialRequest pendingGetCredentialRequest = new PendingGetCredentialRequest(+ GET_CRED_PASSWORD_REQ,+ (response) -> {+ getCredentialResponse.set(response);+ latch1.countDown();+ return Unit.INSTANCE;+ });++ CredentialManagerViewHandler.setPendingGetCredentialRequest(editText,+ pendingGetCredentialRequest);++ assertNotNull(editText.getPendingCredentialRequest());+ TestUtilsKt.equals(editText.getPendingCredentialRequest(), GET_CRED_PASSWORD_FRAMEWORK_REQ);+ assertThat(editText.getPendingCredentialCallback()).isInstanceOf(+ OutcomeReceiver.class+ );++ PasswordCredential passwordCredential = new PasswordCredential("id", "password");+ android.credentials.GetCredentialResponse frameworkPasswordResponse =+ new android.credentials.GetCredentialResponse(new Credential(+ passwordCredential.getType(), passwordCredential.getData()));+ assertNotNull(editText.getPendingCredentialCallback());+ editText.getPendingCredentialCallback().onResult(frameworkPasswordResponse);+ latch1.await(50L, TimeUnit.MILLISECONDS);++ assertThat(getCredentialResponse.get()).isNotNull();+ GetCredentialResponse expectedGetCredentialResponse = FrameworkImplHelper+ .convertGetResponseToJetpackClass(frameworkPasswordResponse);+ TestUtilsKt.equals(expectedGetCredentialResponse, getCredentialResponse.get());+ }+}
@@ -0,0 +1,101 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+package androidx.credentials++import android.content.Context+import android.credentials.Credential+import android.os.OutcomeReceiver+import android.widget.EditText+import androidx.annotation.RequiresApi+import androidx.credentials.internal.FrameworkImplHelper.Companion.convertGetRequestToFrameworkClass+import androidx.credentials.internal.FrameworkImplHelper.Companion.convertGetResponseToJetpackClass+import androidx.test.core.app.ApplicationProvider+import androidx.test.filters.SdkSuppress+import com.google.common.truth.Truth.assertThat+import java.util.concurrent.CountDownLatch+import java.util.concurrent.TimeUnit+import java.util.concurrent.atomic.AtomicReference+import org.junit.Test++@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")+class CredentialManagerViewHandlerTest {+ private val mContext: Context = ApplicationProvider.getApplicationContext()++ companion object {+ private val GET_CRED_PASSWORD_REQ = GetCredentialRequest.Builder()+ .setCredentialOptions(listOf(GetPasswordOption())).build()+ private val GET_CRED_PASSWORD_FRAMEWORK_REQ =+ convertGetRequestToFrameworkClass(GET_CRED_PASSWORD_REQ)+ }++ @Test+ @RequiresApi(35)+ fun setPendingCredentialRequest_frameworkAttrSetSuccessfully() {+ val editText = EditText(mContext)+ val pendingGetCredentialRequest = PendingGetCredentialRequest(+ GET_CRED_PASSWORD_REQ+ ) { _: GetCredentialResponse? -> }++ editText+ .pendingGetCredentialRequest = pendingGetCredentialRequest++ equals(+ editText.pendingCredentialRequest!!,+ GET_CRED_PASSWORD_FRAMEWORK_REQ+ )+ assertThat(editText.pendingCredentialCallback).isInstanceOf(+ OutcomeReceiver::class.java+ )+ }++ @Test+ @RequiresApi(35)+ @Throws(InterruptedException::class)+ fun setPendingCredentialRequest_callbackInvokedSuccessfully() {+ val latch1 = CountDownLatch(1)+ val getCredentialResponse = AtomicReference()+ val editText = EditText(mContext)++ editText+ .pendingGetCredentialRequest = PendingGetCredentialRequest(+ GET_CRED_PASSWORD_REQ) {+ response ->+ getCredentialResponse.set(response)+ latch1.countDown()+ }++ equals(editText.pendingCredentialRequest!!, GET_CRED_PASSWORD_FRAMEWORK_REQ)+ assertThat(editText.pendingCredentialCallback).isInstanceOf(+ OutcomeReceiver::class.java+ )++ val passwordCredential = PasswordCredential("id", "password")+ val frameworkPasswordResponse =+ android.credentials.GetCredentialResponse(+ Credential(+ passwordCredential.type, passwordCredential.data+ )+ )++ editText.pendingCredentialCallback!!.onResult(frameworkPasswordResponse)+ latch1.await(50L, TimeUnit.MILLISECONDS)++ assertThat(getCredentialResponse.get()).isNotNull()+ val expectedGetCredentialResponse =+ convertGetResponseToJetpackClass(frameworkPasswordResponse)+ equals(expectedGetCredentialResponse, getCredentialResponse.get())+ }+}
@@ -0,0 +1,45 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.credentials;++import static com.google.common.truth.Truth.assertThat;++import androidx.annotation.RequiresApi;+import androidx.test.filters.SdkSuppress;++import kotlin.Unit;++import org.junit.Test;++import java.util.Collections;++@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")+public class PendingGetCredentialRequestJavaTest {+ @Test+ @RequiresApi(35)+ public void constructor_setAndGetRequestThroughViewTag() {+ GetCredentialRequest request = new GetCredentialRequest.Builder()+ .setCredentialOptions(Collections.singletonList(new GetPasswordOption()))+ .build();+ PendingGetCredentialRequest pendingGetCredentialRequest =+ new PendingGetCredentialRequest(request,+ (response) -> Unit.INSTANCE);++ assertThat(pendingGetCredentialRequest.getRequest())+ .isSameInstanceAs(request);+ }+}
@@ -0,0 +1,40 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+package androidx.credentials++import androidx.annotation.RequiresApi+import androidx.test.filters.SdkSuppress+import com.google.common.truth.Truth.assertThat+import org.junit.Test++@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")+class PendingGetCredentialRequestTest {++ @Test+ @RequiresApi(35)+ fun constructor_setAndGetRequestThroughViewTag() {+ val request = GetCredentialRequest.Builder()+ .setCredentialOptions(listOf(GetPasswordOption()))+ .build()++ val pendingGetCredentialRequest = PendingGetCredentialRequest(+ request+ ) { _: GetCredentialResponse? -> }++ assertThat(pendingGetCredentialRequest.request)+ .isSameInstanceAs(request)+ }+}
@@ -0,0 +1,209 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.credentials.provider;++import static androidx.credentials.provider.BiometricPromptData.BUNDLE_HINT_ALLOWED_AUTHENTICATORS;+import static androidx.credentials.provider.BiometricPromptData.BUNDLE_HINT_CRYPTO_OP_ID;++import static com.google.common.truth.Truth.assertThat;++import static org.junit.Assert.assertThrows;++import android.hardware.biometrics.BiometricManager;+import android.hardware.biometrics.BiometricPrompt;+import android.os.Bundle;++import androidx.test.ext.junit.runners.AndroidJUnit4;+import androidx.test.filters.SdkSuppress;+import androidx.test.filters.SmallTest;++import org.junit.Test;+import org.junit.runner.RunWith;++import javax.crypto.NullCipher;++@RunWith(AndroidJUnit4.class)+@SmallTest+@SdkSuppress(minSdkVersion = 28)+public class BiometricPromptDataJavaTest {++ private static final BiometricPrompt.CryptoObject TEST_CRYPTO_OBJECT = new+ BiometricPrompt.CryptoObject(new NullCipher());++ private static final int TEST_ALLOWED_AUTHENTICATOR = BiometricManager.Authenticators+ .BIOMETRIC_STRONG;++ @Test+ public void construct_cryptoObjectStrongAllowedAuthenticator_success() {+ BiometricPromptData biometricPromptData = new BiometricPromptData(+ /*cryptoObject=*/TEST_CRYPTO_OBJECT,+ /*allowedAuthenticators=*/TEST_ALLOWED_AUTHENTICATOR+ );++ assertThat(biometricPromptData.getAllowedAuthenticators())+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);+ assertThat(biometricPromptData.getCryptoObject()).isEqualTo(TEST_CRYPTO_OBJECT);+ }++ @Test+ public void construct_cryptoObjectNullAuthenticatorNotProvided_successWithWeakAuthenticator() {+ int expectedAuthenticator = BiometricManager.Authenticators.BIOMETRIC_WEAK;++ BiometricPromptData biometricPromptData = new BiometricPromptData.Builder().build();++ assertThat(biometricPromptData.getCryptoObject()).isNull();+ assertThat(biometricPromptData.getAllowedAuthenticators()).isEqualTo(expectedAuthenticator);+ }++ @Test+ public void construct_cryptoObjectExistsAuthenticatorNotProvided__defaultThrowsIAE() {+ assertThrows("Expected cryptoObject without strong authenticator to throw "+ + "IllegalArgumentException",+ IllegalArgumentException.class,+ () -> new BiometricPromptData.Builder()+ .setCryptoObject(TEST_CRYPTO_OBJECT).build()+ );+ }++ @Test+ public void construct_cryptoObjectNullAuthenticatorNonNull_successPassedInAuthenticator() {+ BiometricPromptData biometricPromptData = new BiometricPromptData(+ /*cryptoObject=*/null,+ /*allowedAuthenticator=*/TEST_ALLOWED_AUTHENTICATOR+ );++ assertThat(biometricPromptData.getCryptoObject()).isNull();+ assertThat(biometricPromptData.getAllowedAuthenticators()).isEqualTo(+ TEST_ALLOWED_AUTHENTICATOR);+ }++ @Test+ public void construct_authenticatorNotAccepted_throwsIAE() {+ assertThrows("Expected invalid allowed authenticator to throw "+ + "IllegalArgumentException",+ IllegalArgumentException.class,+ () -> new BiometricPromptData(+ /*cryptoObject=*/null,+ /*allowedAuthenticator=*/Integer.MIN_VALUE+ )+ );+ }++ @Test+ public void build_requiredParamsOnly_success() {+ int expectedAllowedAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK;++ BiometricPromptData actualBiometricPromptData = new BiometricPromptData.Builder().build();++ assertThat(actualBiometricPromptData.getAllowedAuthenticators()).isEqualTo(+ expectedAllowedAuthenticators);+ assertThat(actualBiometricPromptData.getCryptoObject()).isNull();+ }++ @Test+ public void build_setCryptoObjectWithStrongAuthenticator_success() {+ BiometricPromptData actualBiometricPromptData = new BiometricPromptData.Builder()+ .setCryptoObject(TEST_CRYPTO_OBJECT)+ .setAllowedAuthenticators(TEST_ALLOWED_AUTHENTICATOR).build();++ assertThat(actualBiometricPromptData.getCryptoObject()).isEqualTo(TEST_CRYPTO_OBJECT);+ assertThat(actualBiometricPromptData.getAllowedAuthenticators())+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);+ }++ @Test+ public void build_setAllowedAuthenticator_success() {+ BiometricPromptData actualBiometricPromptData = new BiometricPromptData.Builder()+ .setAllowedAuthenticators(TEST_ALLOWED_AUTHENTICATOR).build();++ assertThat(actualBiometricPromptData.getAllowedAuthenticators())+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);+ }++ // TODO(b/325469910) : Use the proper opId / CryptoObject structure when available+ @Test+ public void fromBundle_validAllowedAuthenticator_success() {+ int expectedOpId = Integer.MIN_VALUE;+ Bundle inputBundle = new Bundle();+ inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR);+ inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);++ BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);++ assertThat(actualBiometricPromptData).isNotNull();+ assertThat(actualBiometricPromptData.getAllowedAuthenticators())+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);++ }++ // TODO(b/325469910) : Use the proper opId / CryptoObject structure when available+ @Test+ public void fromBundle_unrecognizedAllowedAuthenticator_success() {+ int expectedOpId = Integer.MIN_VALUE;+ Bundle inputBundle = new Bundle();+ int unrecognizedAuthenticator = Integer.MAX_VALUE;+ inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, unrecognizedAuthenticator);+ inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);++ BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);++ assertThat(actualBiometricPromptData).isNotNull();+ assertThat(actualBiometricPromptData.getAllowedAuthenticators())+ .isEqualTo(unrecognizedAuthenticator);++ }++ // TODO(b/325469910) : Use the proper opId / CryptoObject structure when available+ @Test+ public void fromBundle_invalidBundleKey_nullBiometricPromptData() {+ int expectedOpId = Integer.MIN_VALUE;+ Bundle inputBundle = new Bundle();+ int unrecognizedAuthenticator = Integer.MAX_VALUE;+ inputBundle.putInt("invalidKey", unrecognizedAuthenticator);+ inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);++ BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);++ assertThat(actualBiometricPromptData).isNull();+ }++ @Test+ public void toBundle_success() {+ BiometricPromptData testBiometricPromptData = new BiometricPromptData(TEST_CRYPTO_OBJECT,+ TEST_ALLOWED_AUTHENTICATOR);+ int expectedOpId = Integer.MIN_VALUE;++ Bundle actualBundle = BiometricPromptData.toBundle(+ testBiometricPromptData);++ assertThat(actualBundle).isNotNull();+ assertThat(actualBundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS)).isEqualTo(+ TEST_ALLOWED_AUTHENTICATOR+ );+ assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(expectedOpId);+ }++ @Test+ public void build_setInvalidAllowedAuthenticator_throwsIAE() {+ assertThrows("Expected invalid allowed authenticator to throw "+ + "IllegalArgumentException",+ IllegalArgumentException.class,+ () -> new BiometricPromptData.Builder().setAllowedAuthenticators(-10000).build()+ );+ }++}
@@ -0,0 +1,198 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.credentials.provider++import android.hardware.biometrics.BiometricManager+import android.hardware.biometrics.BiometricPrompt+import android.os.Bundle+import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS+import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_CRYPTO_OP_ID+import androidx.test.ext.junit.runners.AndroidJUnit4+import androidx.test.filters.SdkSuppress+import androidx.test.filters.SmallTest+import com.google.common.truth.Truth.assertThat+import javax.crypto.NullCipher+import org.junit.Assert.assertThrows+import org.junit.Test+import org.junit.runner.RunWith++@RunWith(AndroidJUnit4::class)+@SdkSuppress(minSdkVersion = 28)+@SmallTest+class BiometricPromptDataTest {+ @Test+ fun construct_cryptoObjectStrongAllowedAuthenticator_success() {+ val biometricPromptData = BiometricPromptData(TEST_CRYPTO_OBJECT,+ TEST_ALLOWED_AUTHENTICATOR)++ assertThat(biometricPromptData.allowedAuthenticators).isEqualTo(TEST_ALLOWED_AUTHENTICATOR)+ assertThat(biometricPromptData.cryptoObject).isEqualTo(TEST_CRYPTO_OBJECT)+ }++ @Test+ fun construct_cryptoObjectNullAuthenticatorNotProvided_successWithWeakAuthenticator() {+ val expectedAuthenticator = BiometricManager.Authenticators.BIOMETRIC_WEAK++ val biometricPromptData = BiometricPromptData(+ null,+ )++ assertThat(biometricPromptData.cryptoObject).isNull()+ assertThat(biometricPromptData.allowedAuthenticators).isEqualTo(expectedAuthenticator)+ }++ @Test+ fun construct_cryptoObjectExistsAuthenticatorNotProvided_defaultWeakAuthenticatorThrowsIAE() {+ assertThrows(+ "Expected invalid allowed authenticator with cryptoObject to throw " ++ "IllegalArgumentException",+ java.lang.IllegalArgumentException::class.java+ ) {+ BiometricPromptData(+ TEST_CRYPTO_OBJECT+ )+ }+ }++ @Test+ fun construct_cryptoObjectNullAuthenticatorNonNull_successPassedInAuthenticator() {+ val expectedAuthenticator = BiometricManager.Authenticators.BIOMETRIC_STRONG++ val biometricPromptData = BiometricPromptData(cryptoObject = null, expectedAuthenticator)++ assertThat(biometricPromptData.cryptoObject).isNull()+ assertThat(biometricPromptData.allowedAuthenticators).isEqualTo(expectedAuthenticator)+ }++ @Test+ fun construct_authenticatorNotAccepted_throwsIAE() {+ assertThrows(+ "Expected invalid allowed authenticator IllegalArgumentException",+ java.lang.IllegalArgumentException::class.java+ ) {+ BiometricPromptData(null, allowedAuthenticators = Int.MIN_VALUE)+ }+ }++ @Test+ fun build_requiredParamsOnly_success() {+ val expectedAllowedAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK++ val actualBiometricPromptData = BiometricPromptData.Builder().build()++ assertThat(actualBiometricPromptData.allowedAuthenticators)+ .isEqualTo(expectedAllowedAuthenticators)+ assertThat(actualBiometricPromptData.cryptoObject).isNull()+ }++ @Test+ fun build_setCryptoObjectWithStrongAuthenticatorOnly_success() {+ val actualBiometricPromptData = BiometricPromptData.Builder()+ .setCryptoObject(TEST_CRYPTO_OBJECT)+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG).build()++ assertThat(actualBiometricPromptData.cryptoObject).isEqualTo(TEST_CRYPTO_OBJECT)+ assertThat(actualBiometricPromptData.allowedAuthenticators)+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)+ }++ @Test+ fun build_setAllowedAuthenticator_success() {+ val actualBiometricPromptData = BiometricPromptData.Builder()+ .setAllowedAuthenticators(TEST_ALLOWED_AUTHENTICATOR).build()++ assertThat(actualBiometricPromptData.allowedAuthenticators)+ .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)+ }++ @Test+ fun build_setInvalidAllowedAuthenticator_success() {+ assertThrows(+ "Expected builder invalid allowed authenticator to throw " ++ "IllegalArgumentException",+ java.lang.IllegalArgumentException::class.java+ ) {+ BiometricPromptData.Builder()+ .setAllowedAuthenticators(-10000).build()+ }+ }++ // TODO(b/325469910) : Use the proper opId / CryptoObject structure when available+ @Test+ fun fromBundle_validAllowedAuthenticator_success() {+ val expectedOpId = Integer.MIN_VALUE+ val inputBundle = Bundle()+ inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR)+ inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId)++ val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)++ assertThat(actualBiometricPromptData).isNotNull()+ assertThat(actualBiometricPromptData!!.allowedAuthenticators).isEqualTo(+ TEST_ALLOWED_AUTHENTICATOR)+ }++ // TODO(b/325469910) : Use the proper opId / CryptoObject structure when available+ @Test+ fun fromBundle_unrecognizedAllowedAuthenticator_success() {+ val expectedOpId = Integer.MIN_VALUE+ val inputBundle = Bundle()+ val unrecognizedAuthenticator = Integer.MAX_VALUE+ inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, unrecognizedAuthenticator)+ inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId)++ val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)++ assertThat(actualBiometricPromptData).isNotNull()+ assertThat(actualBiometricPromptData!!.allowedAuthenticators).isEqualTo(+ unrecognizedAuthenticator)+ }++ @Test+ fun fromBundle_invalidBundleKey_nullBiometricPromptData() {+ val expectedOpId = Integer.MIN_VALUE+ val inputBundle = Bundle()+ val unrecognizedAuthenticator = Integer.MAX_VALUE+ inputBundle.putInt("invalid key", unrecognizedAuthenticator)+ inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId)++ val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)++ assertThat(actualBiometricPromptData).isNull()+ }++ @Test+ fun toBundle_success() {+ val testBiometricPromptData = BiometricPromptData(TEST_CRYPTO_OBJECT,+ TEST_ALLOWED_AUTHENTICATOR)+ val expectedOpId = Integer.MIN_VALUE++ val actualBundle = BiometricPromptData.toBundle(testBiometricPromptData)++ assertThat(actualBundle).isNotNull()+ assertThat(actualBundle!!.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS)).isEqualTo(+ TEST_ALLOWED_AUTHENTICATOR)+ assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(expectedOpId)+ }++ private companion object {+ private val TEST_CRYPTO_OBJECT = BiometricPrompt.CryptoObject(NullCipher())++ private const val TEST_ALLOWED_AUTHENTICATOR = BiometricManager+ .Authenticators.BIOMETRIC_STRONG+ }+}
@@ -0,0 +1,107 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++@file:JvmName("CredentialManagerViewHandler")++package androidx.credentials++import android.os.Build+import android.os.OutcomeReceiver+import android.util.Log+import android.view.View+import androidx.annotation.RequiresApi+import androidx.credentials.internal.FrameworkImplHelper++/**+ * An extension API to the [View] class that allows setting of a+ * [PendingGetCredentialRequest], that in-turn contains a [GetCredentialRequest],+ * and a callback to deliver the final [GetCredentialResponse]. The+ * associated request is invoked when the [View] is focused/clicked by the user.+ *+ * A typical scenario for setting this request is a login screen with a username & password field.+ * We recommend calling the [CredentialManager.getCredential] API when this login screen loads, so+ * that the user can be presented with a selector with all credential options to choose from.+ * In addition, we recommend using this API to set the same [GetCredentialRequest] that is passed+ * to [CredentialManager.getCredential], on the username & password views. With that, if the+ * user dismisses the initial selector, and then taps on either the username or the password+ * field, they would see the same suggestions that they saw on the selector, but now on+ * fallback UI experiences such as keyboard suggestions or drop-down lists,+ * depending on the device capabilities.+ *+ * If you have multiple views on the screen that should invoke different requests as opposed to+ * the same, you can simply use this API to set different requests on corresponding views, and+ * hence a different set of suggestions will appear.+ *+ * Note that no errors are propagated to the [PendingGetCredentialRequest.callback].+ * In a scenario where multiple suggestions are presented to the user as part of the+ * keyboard suggestions for instance, it is possible that the user selects one, but the+ * flow ends up in an error state, due to which the final [GetCredentialResponse] cannot be+ * propagated. In that case, user will be taken back to the suggestions, and can very well+ * select a different suggestion which would this time result in a success.+ * The intermediate error states are not propagated to the developer, and only a final+ * response, if any, is propagated.+ *+ * @property pendingGetCredentialRequest the [GetCredentialRequest] and+ * the associated callback to be set on the view, and to be+ * exercised when user focused on the view in question+ */++private const val TAG = "ViewHandler"++@Suppress("NewApi")+var View.pendingGetCredentialRequest: PendingGetCredentialRequest?+ get() = getTag(R.id.androidx_credential_pendingCredentialRequest)+ as? PendingGetCredentialRequest+ set(value) {+ setTag(R.id.androidx_credential_pendingCredentialRequest, value)+ if (value != null) {+ if (Build.VERSION.SDK_INT >= 35 || (Build.VERSION.SDK_INT == 34 &&+ Build.VERSION.PREVIEW_SDK_INT > 0)) {+ Api35Impl.setPendingGetCredentialRequest(this, value.request, value.callback)+ }+ } else {+ if (Build.VERSION.SDK_INT >= 35 || (Build.VERSION.SDK_INT == 34 &&+ Build.VERSION.PREVIEW_SDK_INT > 0)) {+ Api35Impl.clearPendingGetCredentialRequest(this)+ }+ }+ }++@RequiresApi(35)+private object Api35Impl {+ fun setPendingGetCredentialRequest(+ view: View,+ request: GetCredentialRequest,+ callback: (GetCredentialResponse) -> Unit,+ ) {+ val frameworkRequest = FrameworkImplHelper.convertGetRequestToFrameworkClass(request)+ val frameworkCallback = object : OutcomeReceiver<+ android.credentials.GetCredentialResponse, android.credentials.GetCredentialException> {+ override fun onResult(response: android.credentials.GetCredentialResponse) {+ callback.invoke(FrameworkImplHelper+ .convertGetResponseToJetpackClass(response))+ }+ override fun onError(error: android.credentials.GetCredentialException) {+ Log.w(TAG, "Error: " + error.type + " , " + error.message)+ }+ }+ view.setPendingCredentialRequest(frameworkRequest, frameworkCallback)+ }++ fun clearPendingGetCredentialRequest(view: View) {+ view.clearPendingCredentialRequest()+ }+}
@@ -147,7 +147,7 @@
val classNames = mutableListOf() if (packageInfo.services != null) {- for (serviceInfo in packageInfo.services) {+ for (serviceInfo in packageInfo.services!!) { if (serviceInfo.metaData != null) { val className = serviceInfo.metaData.getString(CREDENTIAL_PROVIDER_KEY) if (className != null) {
@@ -0,0 +1,38 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.credentials++import android.view.View++/**+ * Request to be set on an Android [View], which will be invoked when the [View] is+ * focused/clicked by the user.+ *+ * Note that the [callback] only handles a final [GetCredentialResponse] and no errors are+ * propagated to the callback.+ *+ * See [View.setPendingCredentialRequest] for details on how this request will be used.+ *+ * @property request the [GetCredentialRequest] to be invoked when a given view on which this+ * request is set is focused+ * @property callback the callback on which the final [GetCredentialResponse] is returned, after+ * the user has made its selections+ */+class PendingGetCredentialRequest(+ val request: GetCredentialRequest,+ val callback: (GetCredentialResponse) -> Unit+)
@@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * limitations under the License. */+@file:Suppress("deprecation") // For usage of Slice+ package androidx.credentials.provider import android.annotation.SuppressLint
@@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * limitations under the License. */+@file:Suppress("deprecation") // For usage of Slice+ package androidx.credentials.provider import android.annotation.SuppressLint
@@ -0,0 +1,147 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.credentials.provider++import android.hardware.biometrics.BiometricPrompt+import android.util.Log+import androidx.annotation.RestrictTo+import java.util.Objects+import org.jetbrains.annotations.VisibleForTesting++/**+ * Error returned from the Biometric Prompt flow that is executed+ * by [androidx.credentials.CredentialManager] after the user+ * makes a selection on the Credential Manager account selector.+ *+ * @property errorCode the error code denoting what kind of error+ * was encountered while the biometric prompt flow failed, must+ * be one of the error codes defined in+ * [androidx.biometric.BiometricPrompt] such as+ * [androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE]+ * or+ * [androidx.biometric.BiometricPrompt.ERROR_TIMEOUT]+ * @property errorMsg the message associated with the [errorCode] in the+ * form that can be displayed on a UI.+ *+ * @see AuthenticationErrorTypes+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+class AuthenticationError @JvmOverloads constructor(+ val errorCode: @AuthenticationErrorTypes Int,+ val errorMsg: CharSequence? = null,+) {++ companion object {++ internal val TAG = "AuthenticationError"++ @VisibleForTesting+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ const val EXTRA_BIOMETRIC_AUTH_ERROR = "BIOMETRIC_AUTH_ERROR"+ @VisibleForTesting+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ const val EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE = "EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE"++ // The majority of this is unexpected to be sent, or the values are equal,+ // but should it arrive for any reason, is handled properly. This way+ // providers can be confident the Jetpack codes alone are enough.+ @VisibleForTesting+ internal val biometricFrameworkToJetpackErrorMap = linkedMapOf(+ BiometricPrompt.BIOMETRIC_ERROR_CANCELED to androidx.biometric.BiometricPrompt+ .ERROR_CANCELED,+ BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT to androidx.biometric.BiometricPrompt+ .ERROR_HW_NOT_PRESENT,+ BiometricPrompt.BIOMETRIC_ERROR_HW_UNAVAILABLE to androidx.biometric.BiometricPrompt+ .ERROR_HW_UNAVAILABLE,+ BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT to androidx.biometric.BiometricPrompt+ .ERROR_LOCKOUT,+ BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT_PERMANENT to androidx.biometric.BiometricPrompt+ .ERROR_LOCKOUT_PERMANENT,+ BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS to androidx.biometric.BiometricPrompt+ .ERROR_NO_BIOMETRICS,+ BiometricPrompt.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL to androidx.biometric+ .BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL,+ BiometricPrompt.BIOMETRIC_ERROR_NO_SPACE to androidx.biometric.BiometricPrompt+ .ERROR_NO_SPACE,+ BiometricPrompt.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED to androidx.biometric+ .BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED,+ BiometricPrompt.BIOMETRIC_ERROR_TIMEOUT to androidx.biometric.BiometricPrompt+ .ERROR_TIMEOUT,+ BiometricPrompt.BIOMETRIC_ERROR_UNABLE_TO_PROCESS to androidx.biometric.BiometricPrompt+ .ERROR_UNABLE_TO_PROCESS,+ BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED to androidx.biometric.BiometricPrompt+ .ERROR_USER_CANCELED,+ BiometricPrompt.BIOMETRIC_ERROR_VENDOR to androidx.biometric.BiometricPrompt+ .ERROR_VENDOR+ // TODO(b/340334264) : Add NEGATIVE_BUTTON from FW once avail, or wrap this in+ // a credential manager specific error.+ )++ internal fun convertFrameworkBiometricErrorToJetpack(frameworkCode: Int): Int {+ // Ignoring getOrDefault to allow this object down to API 21+ return if (biometricFrameworkToJetpackErrorMap.containsKey(frameworkCode)) {+ biometricFrameworkToJetpackErrorMap[frameworkCode]!!+ } else {+ Log.i(TAG, "Unexpected error code, $frameworkCode, ")+ frameworkCode+ }+ }++ /**+ * Generates an instance of this class, to be called by an UI consumer that calls+ * [BiometricPrompt] API and needs the result to be wrapped by this class. The caller of+ * this API must specify whether the framework [android.hardware.biometrics.BiometricPrompt]+ * API or the jetpack [androidx.biometric.BiometricPrompt] API is used through+ * [isFrameworkBiometricPrompt].+ *+ * @param uiErrorCode the error code used to create this error instance, typically using+ * the [androidx.biometric.BiometricPrompt]'s constants if conversion isn't desired, or+ * [android.hardware.biometrics.BiometricPrompt]'s constants if conversion *is* desired.+ * @param uiErrorMessage the message associated with the [uiErrorCode] in the+ * form that can be displayed on a UI.+ * @param isFrameworkBiometricPrompt the bit indicating whether or not this error code+ * requires conversion or not, set to true by default+ * @return an authentication error that has properly handled conversion of the err code+ */+ @JvmOverloads @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ internal fun createFrom(+ uiErrorCode: Int,+ uiErrorMessage: CharSequence,+ isFrameworkBiometricPrompt: Boolean = true,+ ): AuthenticationError =+ AuthenticationError(+ errorCode = if (isFrameworkBiometricPrompt)+ convertFrameworkBiometricErrorToJetpack(uiErrorCode) else uiErrorCode,+ errorMsg = uiErrorMessage,+ )+ }++ override fun equals(other: Any?): Boolean {+ if (this === other) {+ return true+ }+ if (other is AuthenticationError) {+ return this.errorCode == other.errorCode &&+ this.errorMsg == other.errorMsg+ }+ return false+ }++ override fun hashCode(): Int {+ return Objects.hash(this.errorCode, this.errorMsg)+ }+}
@@ -0,0 +1,60 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.credentials.provider++import androidx.annotation.IntDef+import androidx.annotation.RestrictTo+import androidx.biometric.BiometricPrompt+import androidx.biometric.BiometricPrompt.ERROR_CANCELED+import androidx.biometric.BiometricPrompt.ERROR_HW_NOT_PRESENT+import androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE+import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT+import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT_PERMANENT+import androidx.biometric.BiometricPrompt.ERROR_NO_BIOMETRICS+import androidx.biometric.BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL+import androidx.biometric.BiometricPrompt.ERROR_NO_SPACE+import androidx.biometric.BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED+import androidx.biometric.BiometricPrompt.ERROR_TIMEOUT+import androidx.biometric.BiometricPrompt.ERROR_UNABLE_TO_PROCESS+import androidx.biometric.BiometricPrompt.ERROR_USER_CANCELED+import androidx.biometric.BiometricPrompt.ERROR_VENDOR++/**+ * This acts as a parameter hint for what [BiometricPrompt]'s error constants should be.+ * You can learn more about the constants from [BiometricPrompt] to utilize best practices.+ *+ * @see BiometricPrompt+ */+@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)+@Retention(AnnotationRetention.SOURCE)+@IntDef(value = [+ ERROR_CANCELED,+ ERROR_HW_NOT_PRESENT,+ ERROR_HW_UNAVAILABLE,+ ERROR_LOCKOUT,+ ERROR_LOCKOUT_PERMANENT,+ ERROR_NO_BIOMETRICS,+ ERROR_NO_DEVICE_CREDENTIAL,+ ERROR_NO_SPACE,+ ERROR_SECURITY_UPDATE_REQUIRED,+ ERROR_TIMEOUT,+ ERROR_UNABLE_TO_PROCESS,+ ERROR_USER_CANCELED,+ ERROR_VENDOR+])+@RestrictTo(RestrictTo.Scope.LIBRARY)+annotation class AuthenticationErrorTypes
@@ -0,0 +1,109 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.credentials.provider++import android.hardware.biometrics.BiometricPrompt+import android.util.Log+import androidx.annotation.RestrictTo+import androidx.credentials.provider.AuthenticationError.Companion.TAG+import java.util.Objects+import org.jetbrains.annotations.VisibleForTesting++/**+ * Successful result returned from the Biometric Prompt authentication+ * flow handled by [androidx.credentials.CredentialManager].+ *+ * @property authenticationType the type of authentication (e.g. device credential or biometric)+ * that was requested from and successfully provided by the user, corresponds to+ * constants defined in [androidx.biometric.BiometricPrompt] such as+ * [androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC]+ * or+ * [androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL]+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+class AuthenticationResult(+ val authenticationType: @AuthenticatorResultTypes Int,+) {++ companion object {+ @VisibleForTesting+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ const val EXTRA_BIOMETRIC_AUTH_RESULT_TYPE =+ "BIOMETRIC_AUTH_RESULT"++ @VisibleForTesting+ internal val biometricFrameworkToJetpackResultMap = linkedMapOf(+ BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC to+ androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC,+ BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL to+ androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL,+ // TODO(b/340334264) : Add TYPE_UNKNOWN once avail from fw, though unexpected unless+ // very low API level, and may be ignored until jp only impl added in QPR, or other+ // ctr can be used directly once avail/ready+ )++ internal fun convertFrameworkBiometricResultToJetpack(frameworkCode: Int): Int {+ // Ignoring getOrDefault to allow this object down to API 21+ return if (biometricFrameworkToJetpackResultMap.containsKey(frameworkCode)) {+ biometricFrameworkToJetpackResultMap[frameworkCode]!!+ } else {+ Log.i(TAG, "Non framework result code, $frameworkCode, ")+ frameworkCode+ }+ }++ /**+ * Generates an instance of this class, to be called by an UI consumer that calls+ * [BiometricPrompt] API and needs the result to be wrapped by this class. The caller of+ * this API must specify whether the framework [android.hardware.biometrics.BiometricPrompt]+ * API or the jetpack [androidx.biometric.BiometricPrompt] API is used through+ * [isFrameworkBiometricPrompt].+ *+ * @param uiAuthenticationType the type of authentication (e.g. device credential or+ * biometric) that was requested from and successfully provided by the user, corresponds to+ * constants defined in [androidx.biometric.BiometricPrompt] if conversion is not desired,+ * or in [android.hardware.biometrics.BiometricPrompt] if conversion is desired+ * @param isFrameworkBiometricPrompt the bit indicating whether or not this error code+ * requires conversion or not, set to true by default+ * @return an authentication result that has properly handled conversion of the result types+ */+ @JvmOverloads @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ internal fun createFrom(+ uiAuthenticationType: Int,+ isFrameworkBiometricPrompt: Boolean = true,+ ): AuthenticationResult =+ AuthenticationResult(+ authenticationType = if (isFrameworkBiometricPrompt)+ convertFrameworkBiometricResultToJetpack(uiAuthenticationType)+ else uiAuthenticationType+ )+ }++ override fun equals(other: Any?): Boolean {+ if (this === other) {+ return true+ }+ if (other is AuthenticationResult) {+ return this.authenticationType == other.authenticationType+ }+ return false+ }++ override fun hashCode(): Int {+ return Objects.hash(this.authenticationType)+ }+}
@@ -0,0 +1,40 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.credentials.provider++import androidx.annotation.IntDef+import androidx.annotation.RestrictTo+import androidx.biometric.BiometricPrompt+import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC+import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL+import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_UNKNOWN++/**+ * This acts as a parameter hint for what [BiometricPrompt]'s result constants should be.+ * You can learn more about the constants from [BiometricPrompt] to utilize best practices.+ *+ * @see BiometricPrompt+ */+@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)+@Retention(AnnotationRetention.SOURCE)+@IntDef(value = [+ AUTHENTICATION_RESULT_TYPE_BIOMETRIC,+ AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL,+ AUTHENTICATION_RESULT_TYPE_UNKNOWN+])+@RestrictTo(RestrictTo.Scope.LIBRARY)+annotation class AuthenticatorResultTypes
@@ -0,0 +1,37 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.credentials.provider++import android.hardware.biometrics.BiometricManager+import androidx.annotation.IntDef+import androidx.annotation.RestrictTo+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK+import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL++/**+ * This allows verification when users pass in [BiometricManager.Authenticators] constants; namely+ * we can have a parameter hint that indicates what they should be. You can learn more about+ * the constants from [BiometricManager.Authenticators] to utilize best practices.+ *+ * @see BiometricManager.Authenticators+ */+@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)+@Retention(AnnotationRetention.SOURCE)+@IntDef(value = [BIOMETRIC_STRONG, BIOMETRIC_WEAK, DEVICE_CREDENTIAL])+@RestrictTo(RestrictTo.Scope.LIBRARY)+annotation class AuthenticatorTypes
@@ -0,0 +1,250 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.credentials.provider++import android.hardware.biometrics.BiometricManager+import android.hardware.biometrics.BiometricManager.Authenticators+import android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG+import android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK+import android.hardware.biometrics.BiometricManager.Authenticators.DEVICE_CREDENTIAL+import android.hardware.biometrics.BiometricPrompt+import android.hardware.biometrics.BiometricPrompt.CryptoObject+import android.os.Bundle+import android.util.Log+import androidx.annotation.RestrictTo++/**+ * Biometric prompt data that can be optionally used by you to provide information needed for the+ * system to show a biometric prompt directly embedded into the Credential Manager selector.+ *+ * If you opt to use this, the meta-data provided through the [CreateEntry] or [CredentialEntry]+ * will be shown along with a biometric / device credential capture mechanism, on a single dialog,+ * hence avoiding navigation through multiple screens. When user confirmation is retrieved+ * through the aforementioned biometric / device capture mechanism, the [android.app.PendingIntent]+ * associated with the entry is invoked, and the flow continues as explained in [CreateEntry] or+ * [CredentialEntry].+ *+ * Note that the value of [allowedAuthenticators] together with the features of a given device,+ * determines whether a biometric auth or a device credential mechanism will / can be shown. It is+ * recommended you use [Authenticators] to select these values, though you can find equivalent+ * behaviour from usage of [BiometricManager.Authenticators]. This documentation will refer to+ * [Authenticators] constants, which easily map to their [BiometricManager.Authenticators]+ * counterparts, and in some cases, provide a more useful abstraction.+ *+ * @property allowedAuthenticators specifies the type(s) of authenticators that may be invoked by+ * the [BiometricPrompt] to authenticate the user, defaults to [BIOMETRIC_WEAK] if+ * not set+ * @property cryptoObject a crypto object to be unlocked after successful authentication; When set,+ * the value of [allowedAuthenticators] must be [BIOMETRIC_STRONG] or else+ * an [IllegalArgumentException] is thrown+ *+ * @throws IllegalArgumentException if [cryptoObject] is not null, and the [allowedAuthenticators]+ * is not set to [BIOMETRIC_STRONG]+ *+ * @see Authenticators+ * @see BiometricManager.Authenticators+ */+@RestrictTo(RestrictTo.Scope.LIBRARY)+class BiometricPromptData internal constructor(+ val cryptoObject: BiometricPrompt.CryptoObject? = null,+ val allowedAuthenticators: @AuthenticatorTypes Int = BIOMETRIC_WEAK,+ private var isCreatedFromBundle: Boolean = false,+) {++ /**+ * Biometric prompt data that can be optionally used by you to provide information needed for the+ * system to show a biometric prompt directly embedded into the Credential Manager selector.+ *+ * If you opt to use this, the meta-data provided through the [CreateEntry] or [CredentialEntry]+ * will be shown along with a biometric / device credential capture mechanism, on a single dialog,+ * hence avoiding navigation through multiple screens. When user confirmation is retrieved+ * through the aforementioned biometric / device capture mechanism, the [android.app.PendingIntent]+ * associated with the entry is invoked, and the flow continues as explained in [CreateEntry] or+ * [CredentialEntry].+ *+ * Note that the value of [allowedAuthenticators] together with the features of a given device,+ * determines whether a biometric auth or a device credential mechanism will / can be shown. It is+ * recommended you use [Authenticators] to select these values, though you can find equivalent+ * behaviour from usage of [BiometricManager.Authenticators]. This documentation will refer to+ * [Authenticators] constants, which easily map to their [BiometricManager.Authenticators]+ * counterparts, and in some cases, provide a more useful abstraction.+ *+ * If you opt to use this constructor, you are confirming you are not building from a slice.+ *+ * @param allowedAuthenticators specifies the type(s) of authenticators that may be invoked by+ * the [BiometricPrompt] to authenticate the user, defaults to [BIOMETRIC_WEAK] if+ * not set+ * @param cryptoObject a crypto object to be unlocked after successful authentication; When set,+ * the value of [allowedAuthenticators] must be [BIOMETRIC_STRONG] or else+ * an [IllegalArgumentException] is thrown+ *+ * @throws IllegalArgumentException If [cryptoObject] is not null, and the [allowedAuthenticators]+ * is not set to [BIOMETRIC_STRONG]+ *+ * @see Authenticators+ * @see BiometricManager.Authenticators+ */+ constructor(+ cryptoObject: BiometricPrompt.CryptoObject? = null,+ allowedAuthenticators: @AuthenticatorTypes Int = BIOMETRIC_WEAK+ ) : this(cryptoObject, allowedAuthenticators, isCreatedFromBundle = false)++ init {+ if (!isCreatedFromBundle) {+ // This is not expected to throw for certain eligible callers who utilize the+ // isCreatedFromBundle hidden property.+ require(ALLOWED_AUTHENTICATOR_VALUES.contains(allowedAuthenticators)) {+ "The allowed authenticator must be specified according to the BiometricPrompt spec."+ }+ }+ if (cryptoObject != null) {+ require(isStrongAuthenticationType(allowedAuthenticators)) {+ "If the cryptoObject is non-null, the allowedAuthenticator value must be " ++ "Authenticators.BIOMETRIC_STRONG."+ }+ }+ }++ internal companion object {++ private const val TAG = "BiometricPromptData"++ internal const val BUNDLE_HINT_ALLOWED_AUTHENTICATORS =+ "androidx.credentials.provider.credentialEntry.BUNDLE_HINT_ALLOWED_AUTHENTICATORS"++ internal const val BUNDLE_HINT_CRYPTO_OP_ID =+ "androidx.credentials.provider.credentialEntry.BUNDLE_HINT_CRYPTO_OP_ID"++ /**+ * Returns an instance of [BiometricPromptData] derived from a [Bundle] object.+ *+ * @param bundle the [Bundle] object constructed through [toBundle] method, often+ */+ // TODO(b/333444288) : Once available from BiometricPrompt, structure CryptoObject / opId+ @JvmStatic+ @RestrictTo(RestrictTo.Scope.LIBRARY)+ fun fromBundle(bundle: Bundle): BiometricPromptData? {+ return try {+ if (!bundle.containsKey(BUNDLE_HINT_ALLOWED_AUTHENTICATORS)) {+ throw IllegalArgumentException("Bundle lacks allowed authenticator key.")+ }+ BiometricPromptData(allowedAuthenticators = bundle.getInt(+ BUNDLE_HINT_ALLOWED_AUTHENTICATORS), isCreatedFromBundle = true)+ } catch (e: Exception) {+ Log.i(TAG, "fromSlice failed with: " + e.message)+ null+ }+ }++ /**+ * Returns a [Bundle] that contains the [BiometricPromptData] representation.+ */+ @JvmStatic+ @RestrictTo(RestrictTo.Scope.LIBRARY)+ fun toBundle(biometricPromptData: BiometricPromptData): Bundle? {+ val bundle = Bundle()+ val biometricPromptMap: MutableMap = mutableMapOf(+ BUNDLE_HINT_ALLOWED_AUTHENTICATORS to biometricPromptData.allowedAuthenticators,+ // TODO(b/325469910) : Use the proper opId method when available+ BUNDLE_HINT_CRYPTO_OP_ID to Integer.MIN_VALUE+ )+ biometricPromptMap.forEach {+ if (it.value != null) {+ bundle.putInt(it.key, it.value!!)+ }+ }+ return bundle+ }++ private fun isStrongAuthenticationType(authenticationTypes: Int?): Boolean {+ if (authenticationTypes == null) {+ return false+ }+ val biometricStrength: Int = authenticationTypes and BIOMETRIC_WEAK+ if (biometricStrength and BiometricManager.Authenticators.BIOMETRIC_STRONG.inv() != 0) {+ return false+ }+ return true+ }++ private val ALLOWED_AUTHENTICATOR_VALUES = setOf(+ BIOMETRIC_STRONG,+ BIOMETRIC_WEAK,+ DEVICE_CREDENTIAL,+ BIOMETRIC_STRONG or DEVICE_CREDENTIAL,+ BIOMETRIC_WEAK or DEVICE_CREDENTIAL+ )+ }++ /** Builder for constructing an instance of [BiometricPromptData] */+ class Builder {+ private var cryptoObject: CryptoObject? = null+ private var allowedAuthenticators: Int? = null++ /**+ * Sets whether this [BiometricPromptData] should have a crypto object associated with this+ * authentication. If opting to pass in a value for cryptoObject, it must not be null.+ *+ * @param cryptoObject the [CryptoObject] to be associated with this biometric+ * authentication flow+ */+ fun setCryptoObject(cryptoObject: CryptoObject?): Builder {+ this.cryptoObject = cryptoObject+ return this+ }++ /**+ * Specifies the type(s) of authenticators that may be invoked to authenticate the user.+ * Available authenticator types are+ * defined in [Authenticators] and can be combined via bitwise OR. Defaults to+ * [BIOMETRIC_WEAK].+ *+ * If this method is used and no authenticator of any of the specified types is+ * available at the time an error code will be supplied as part of+ * [android.content.Intent] that will be launched by the+ * containing [CredentialEntry] or [CreateEntry]'s corresponding+ * [android.app.PendingIntent].+ *+ * @param allowedAuthenticators A bit field representing all valid authenticator types+ * that may be invoked by the Credential Manager selector.+ */+ fun setAllowedAuthenticators(allowedAuthenticators: @AuthenticatorTypes Int): Builder {+ this.allowedAuthenticators = allowedAuthenticators+ return this+ }++ /**+ * Builds the [BiometricPromptData] instance.+ *+ * @throws IllegalArgumentException If [cryptoObject] is not null, and the+ * [allowedAuthenticators] is not set to [BIOMETRIC_STRONG]+ */+ fun build(): BiometricPromptData {+ if (cryptoObject != null && allowedAuthenticators != null) {+ require(isStrongAuthenticationType(this.allowedAuthenticators)) {+ "If the cryptoObject is non-null, the allowedAuthenticator value must be " ++ "Authenticators.BIOMETRIC_STRONG"+ }+ }+ val allowedAuthenticators = this.allowedAuthenticators ?: BIOMETRIC_WEAK+ return BiometricPromptData(+ cryptoObject = cryptoObject,+ allowedAuthenticators = allowedAuthenticators,+ )+ }+ }+}
@@ -0,0 +1,86 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.credentials.provider++import androidx.annotation.RestrictTo+import java.util.Objects++/**+ * The result of a Biometric Prompt authentication flow, that is propagated+ * to the provider if the provider requested for+ * [androidx.credentials.CredentialManager] to handle the authentication+ * flow.+ *+ * An instance of this class will always be part of the final provider+ * request, either the [ProviderGetCredentialRequest] or+ * the [ProviderCreateCredentialRequest] that the provider receives+ * after the user selects a [CredentialEntry] or a [CreateEntry]+ * respectively.+ *+ * @property isSuccessful whether the result is a success result, in which+ * case [authenticationResult] should be non-null+ * @property authenticationResult the result of the authentication flow,+ * non-null if the authentication flow was successful+ * @property authenticationError error information, non-null if the+ * authentication flow has failured, meaning that [isSuccessful] will be false+ * in this case+ */+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+class BiometricPromptResult internal constructor(+ val authenticationResult: AuthenticationResult? = null,+ val authenticationError: AuthenticationError? = null+) {+ val isSuccessful: Boolean = authenticationResult != null++ /**+ * An unsuccessful biometric prompt result, denoting that+ * authentication has failed.+ *+ * @param authenticationError the error that caused the biometric+ * prompt authentication flow to fail+ */+ constructor(authenticationError: AuthenticationError) : this(+ authenticationResult = null, authenticationError = authenticationError+ )++ /**+ * A successful biometric prompt result, denoting that+ * authentication has succeeded.+ *+ * @param authenticationResult the result after a successful biometric+ * prompt authentication operation+ */+ constructor(authenticationResult: AuthenticationResult?) : this(+ authenticationResult = authenticationResult, authenticationError = null+ )++ override fun equals(other: Any?): Boolean {+ if (this === other) {+ return true+ }+ if (other is BiometricPromptResult) {+ return this.isSuccessful == other.isSuccessful &&+ this.authenticationResult == other.authenticationResult &&+ this.authenticationError == other.authenticationError+ }+ return false+ }++ override fun hashCode(): Int {+ return Objects.hash(this.isSuccessful, this.authenticationResult, this.authenticationError)+ }+}
@@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * limitations under the License. */+@file:Suppress("deprecation") // For usage of Slice+ package androidx.credentials.provider import android.annotation.SuppressLint@@ -29,6 +31,7 @@
import androidx.credentials.CredentialManager import androidx.credentials.PasswordCredential import androidx.credentials.PublicKeyCredential+import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS import java.time.Instant import java.util.Collections@@ -40,9 +43,26 @@
* registered. When user selects this entry, the corresponding [PendingIntent] is fired, and the * credential creation can be completed. *+ * @property accountName the name of the account where the credential will be saved+ * @property pendingIntent the [PendingIntent] that will get invoked when the user selects this+ * entry, must be created with a unique request code per entry,+ * with flag [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the+ * final request, and NOT with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple+ * times+ * @property description the localized description shown on UI about where the credential is stored+ * @property icon the icon to be displayed with this entry on the UI, must be created using+ * [Icon.createWithResource] when possible, and especially not with [Icon.createWithBitmap] as+ * the latter consumes more memory and may cause undefined behavior due to memory implications+ * on internal transactions+ * @property lastUsedTime the last time the account underlying this entry was used by the user,+ * distinguishable up to the milli second mark only such that if two entries have the same+ * millisecond precision, they will be considered to have been used at the same time+ * @property isAutoSelectAllowed whether this entry should be auto selected if it is the only+ * entry on the selector+ * * @throws IllegalArgumentException If [accountName] is empty */-@RequiresApi(26)+@RequiresApi(23) class CreateEntry internal constructor( val accountName: CharSequence, val pendingIntent: PendingIntent,@@ -50,7 +70,9 @@
val description: CharSequence?, val lastUsedTime: Instant?, private val credentialCountInformationMap: MutableMap,- val isAutoSelectAllowed: Boolean+ val isAutoSelectAllowed: Boolean,+ @get:RestrictTo(RestrictTo.Scope.LIBRARY)+ val biometricPromptData: BiometricPromptData? = null, ) { /**@@ -83,6 +105,65 @@
* this limit) * @throws NullPointerException If [accountName] or [pendingIntent] is null */+ @RestrictTo(RestrictTo.Scope.LIBRARY) constructor(+ accountName: CharSequence,+ pendingIntent: PendingIntent,+ description: CharSequence? = null,+ lastUsedTime: Instant? = null,+ icon: Icon? = null,+ @Suppress("AutoBoxing")+ passwordCredentialCount: Int? = null,+ @Suppress("AutoBoxing")+ publicKeyCredentialCount: Int? = null,+ @Suppress("AutoBoxing")+ totalCredentialCount: Int? = null,+ isAutoSelectAllowed: Boolean = false,+ biometricPromptData: BiometricPromptData? = null,+ ) : this(+ accountName,+ pendingIntent,+ icon,+ description,+ lastUsedTime,+ mutableMapOf(+ PasswordCredential.TYPE_PASSWORD_CREDENTIAL to passwordCredentialCount,+ PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL to publicKeyCredentialCount,+ TYPE_TOTAL_CREDENTIAL to totalCredentialCount+ ),+ isAutoSelectAllowed,+ biometricPromptData = biometricPromptData,+ )++ /**+ * Creates an entry to be displayed on the selector during create flows.+ *+ * @constructor constructs an instance of [CreateEntry]+ *+ * @param accountName the name of the account where the credential will be saved+ * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this+ * entry, must be created with a unique request code per entry,+ * with flag [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the+ * final request, and NOT with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple+ * times+ * @param description the localized description shown on UI about where the credential is stored+ * @param icon the icon to be displayed with this entry on the UI, must be created using+ * [Icon.createWithResource] when possible, and especially not with [Icon.createWithBitmap] as+ * the latter consumes more memory and may cause undefined behavior due to memory implications+ * on internal transactions+ * @param lastUsedTime the last time the account underlying this entry was used by the user,+ * distinguishable up to the milli second mark only such that if two entries have the same+ * millisecond precision, they will be considered to have been used at the same time+ * @param passwordCredentialCount the no. of password credentials contained by the provider+ * @param publicKeyCredentialCount the no. of public key credentials contained by the provider+ * @param totalCredentialCount the total no. of credentials contained by the provider+ * @param isAutoSelectAllowed whether this entry should be auto selected if it is the only+ * entry on the selector+ *+ * @throws IllegalArgumentException If [accountName] is empty, or if [description] is longer+ * than 300 characters (important: make sure your descriptions across all locales are within+ * this limit)+ * @throws NullPointerException If [accountName] or [pendingIntent] is null+ */ constructor( accountName: CharSequence, pendingIntent: PendingIntent,@@ -95,7 +176,7 @@
publicKeyCredentialCount: Int? = null, @Suppress("AutoBoxing") totalCredentialCount: Int? = null,- isAutoSelectAllowed: Boolean = false+ isAutoSelectAllowed: Boolean = false, ) : this( accountName, pendingIntent,@@ -168,6 +249,7 @@
private var publicKeyCredentialCount: Int? = null private var totalCredentialCount: Int? = null private var autoSelectAllowed: Boolean = false+ private var biometricPromptData: BiometricPromptData? = null /** * Sets whether the entry should be auto-selected.@@ -250,14 +332,31 @@
} /**+ * Sets the biometric prompt data to optionally utilize a credential+ * manager flow that directly handles the biometric verification for you and gives you the+ * response; set to null by default.+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY)+ fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {+ this.biometricPromptData = biometricPromptData+ return this+ }++ /** * Builds an instance of [CreateEntry] * * @throws IllegalArgumentException If [accountName] is empty */ fun build(): CreateEntry { return CreateEntry(- accountName, pendingIntent, icon, description, lastUsedTime,- credentialCountInformationMap, autoSelectAllowed+ accountName = accountName,+ pendingIntent = pendingIntent,+ icon = icon,+ description = description,+ lastUsedTime = lastUsedTime,+ credentialCountInformationMap = credentialCountInformationMap,+ isAutoSelectAllowed = autoSelectAllowed,+ biometricPromptData = biometricPromptData ) } }@@ -287,6 +386,7 @@
val pendingIntent = createEntry.pendingIntent val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(SLICE_SPEC_TYPE, REVISION_ID))+ val biometricPromptData = createEntry.biometricPromptData val autoSelectAllowed = if (createEntry.isAutoSelectAllowed) { AUTO_SELECT_TRUE_STRING@@ -294,6 +394,9 @@
AUTO_SELECT_FALSE_STRING }+ val cryptoObject = biometricPromptData?.cryptoObject+ val allowedAuthenticators = biometricPromptData?.allowedAuthenticators+ sliceBuilder.addText( accountName, /*subType=*/null, listOf(SLICE_HINT_ACCOUNT_NAME)@@ -339,6 +442,25 @@
autoSelectAllowed, /*subType=*/null, listOf(SLICE_HINT_AUTO_SELECT_ALLOWED) )++ if (biometricPromptData != null) {+ // TODO(b/326243730) : Await biometric team dependency for opId, then add+ val cryptoObjectOpId = cryptoObject?.hashCode()++ if (allowedAuthenticators != null) {+ sliceBuilder.addInt(+ allowedAuthenticators, /*subType=*/null,+ listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)+ )+ }+ if (cryptoObjectOpId != null) {+ sliceBuilder.addInt(+ cryptoObjectOpId, /*subType=*/null,+ listOf(SLICE_HINT_CRYPTO_OP_ID)+ )+ }+ }+ return sliceBuilder.build() }@@ -353,6 +475,8 @@
var description: CharSequence? = null var lastUsedTime: Instant? = null var autoSelectAllowed = false+ var allowedAuth: Int? = null+ slice.items.forEach { if (it.hasHint(SLICE_HINT_ACCOUNT_NAME)) { accountName = it.text@@ -374,12 +498,30 @@
if (autoSelectValue == AUTO_SELECT_TRUE_STRING) { autoSelectAllowed = true }+ } else if (it.hasHint(SLICE_HINT_ALLOWED_AUTHENTICATORS)) {+ allowedAuth = it.int } }++ // TODO(b/326243730) : Await biometric team dependency for opId, then add - also decide+ // if we want toBundle to be passed into the framework.+ var biometricPromptDataBundle: Bundle? = null+ if (allowedAuth != null) {+ biometricPromptDataBundle = Bundle()+ biometricPromptDataBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, allowedAuth!!)+ }+ return try { CreateEntry(- accountName!!, pendingIntent!!, icon, description,- lastUsedTime, credentialCountInfo, autoSelectAllowed+ accountName = accountName!!,+ pendingIntent = pendingIntent!!,+ icon = icon,+ description = description,+ lastUsedTime = lastUsedTime,+ credentialCountInformationMap = credentialCountInfo,+ isAutoSelectAllowed = autoSelectAllowed,+ biometricPromptData = if (biometricPromptDataBundle != null)+ BiometricPromptData.fromBundle(biometricPromptDataBundle) else null ) } catch (e: Exception) { Log.i(TAG, "fromSlice failed with: " + e.message)@@ -450,6 +592,12 @@
private const val SLICE_HINT_AUTO_SELECT_ALLOWED = "androidx.credentials.provider.createEntry.SLICE_HINT_AUTO_SELECT_ALLOWED"+ private const val SLICE_HINT_ALLOWED_AUTHENTICATORS =+ "androidx.credentials.provider.createEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS"++ private const val SLICE_HINT_CRYPTO_OP_ID =+ "androidx.credentials.provider.createEntry.SLICE_HINT_CRYPTO_OP_ID"+ private const val AUTO_SELECT_TRUE_STRING = "true" private const val AUTO_SELECT_FALSE_STRING = "false"
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * limitations under the License. */+@file:Suppress("deprecation") // For usage of Slice package androidx.credentials.provider@@ -58,6 +59,8 @@
val entryGroupId: CharSequence, val isDefaultIconPreferredAsSingleProvider: Boolean, val affiliatedDomain: CharSequence? = null,+ @get:RestrictTo(RestrictTo.Scope.LIBRARY)+ val biometricPromptData: BiometricPromptData? = null, ) { @RequiresApi(34)@@ -69,6 +72,7 @@
return fromSlice(slice) } }+ companion object { /**
@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and * limitations under the License. */-package androidx.credentials.provider+@file:Suppress("deprecation") // For usage of Slice+package androidx.credentials.provider import android.annotation.SuppressLint import android.app.PendingIntent import android.app.slice.Slice@@ -29,6 +30,7 @@
import androidx.annotation.RestrictTo import androidx.credentials.CredentialOption import androidx.credentials.R+import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS import java.time.Instant import java.util.Collections@@ -74,7 +76,7 @@
* * @see CredentialEntry */-@RequiresApi(26)+@RequiresApi(23) class CustomCredentialEntry internal constructor( override val type: String, val title: CharSequence,@@ -89,16 +91,18 @@
isDefaultIconPreferredAsSingleProvider: Boolean, entryGroupId: CharSequence? = title, affiliatedDomain: CharSequence? = null,+ biometricPromptData: BiometricPromptData? = null, autoSelectAllowedFromOption: Boolean = CredentialOption.extractAutoSelectValue( beginGetCredentialOption.candidateQueryData), private var isCreatedFromSlice: Boolean = false, private var isDefaultIconFromSlice: Boolean = false, ) : CredentialEntry(- type,- beginGetCredentialOption,- entryGroupId ?: title,+ type = type,+ beginGetCredentialOption = beginGetCredentialOption,+ entryGroupId = entryGroupId ?: title, isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider, affiliatedDomain = affiliatedDomain,+ biometricPromptData = biometricPromptData ) { val isAutoSelectAllowedFromOption = autoSelectAllowedFromOption@@ -159,15 +163,15 @@
@Suppress("AutoBoxing") isAutoSelectAllowed: Boolean = false, ) : this(- beginGetCredentialOption.type,- title,- pendingIntent,- isAutoSelectAllowed,- subtitle,- typeDisplayName,- icon,- lastUsedTime,- beginGetCredentialOption,+ type = beginGetCredentialOption.type,+ title = title,+ pendingIntent = pendingIntent,+ isAutoSelectAllowed = isAutoSelectAllowed,+ subtitle = subtitle,+ typeDisplayName = typeDisplayName,+ icon = icon,+ lastUsedTime = lastUsedTime,+ beginGetCredentialOption = beginGetCredentialOption, isDefaultIconPreferredAsSingleProvider = false )@@ -213,7 +217,65 @@
@Suppress("AutoBoxing") isAutoSelectAllowed: Boolean = false, entryGroupId: CharSequence = title,- isDefaultIconPreferredAsSingleProvider: Boolean = false+ isDefaultIconPreferredAsSingleProvider: Boolean = false,+ ) : this(+ type = beginGetCredentialOption.type,+ title = title,+ pendingIntent = pendingIntent,+ isAutoSelectAllowed = isAutoSelectAllowed,+ subtitle = subtitle,+ typeDisplayName = typeDisplayName,+ icon = icon,+ lastUsedTime = lastUsedTime,+ beginGetCredentialOption = beginGetCredentialOption,+ isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,+ entryGroupId = entryGroupId.ifEmpty { title },+ )++ /**+ * @constructor constructs an instance of [CustomCredentialEntry]+ *+ * @param context the context of the calling app, required to retrieve fallback resources+ * @param title the title shown with this entry on the selector UI+ * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this+ * entry, must be created with flag [PendingIntent.FLAG_MUTABLE] to allow the Android+ * system to attach the final request+ * @param beginGetCredentialOption the option from the original [BeginGetCredentialRequest],+ * for which this credential entry is being added+ * @param subtitle the subTitle shown with this entry on the selector UI+ * @param lastUsedTime the last used time the credential underlying this entry was+ * used by the user, distinguishable up to the milli second mark only such that if two+ * entries have the same millisecond precision, they will be considered to have been used at+ * the same time+ * @param typeDisplayName the friendly name to be displayed on the UI for+ * the type of the credential+ * @param icon the icon to be displayed with this entry on the selector UI, if not set a+ * default icon representing a custom credential type is set by the library+ * @param isAutoSelectAllowed whether this entry is allowed to be auto+ * selected if it is the only one on the UI, only takes effect if the app requesting for+ * credentials also opts for auto select+ * @param entryGroupId an ID to uniquely mark this entry for deduplication or to group entries+ * during display, set to [title] by default+ * @param isDefaultIconPreferredAsSingleProvider when set to true, the UI prefers to render the+ * default credential type icon (see the default value of [icon]) when you are+ * the only available provider; false by default+ *+ * @throws IllegalArgumentException If [type] or [title] are empty+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY) constructor(+ context: Context,+ title: CharSequence,+ pendingIntent: PendingIntent,+ beginGetCredentialOption: BeginGetCredentialOption,+ subtitle: CharSequence? = null,+ typeDisplayName: CharSequence? = null,+ lastUsedTime: Instant? = null,+ icon: Icon = Icon.createWithResource(context, R.drawable.ic_other_sign_in),+ @Suppress("AutoBoxing")+ isAutoSelectAllowed: Boolean = false,+ entryGroupId: CharSequence = title,+ isDefaultIconPreferredAsSingleProvider: Boolean = false,+ biometricPromptData: BiometricPromptData? = null, ) : this( beginGetCredentialOption.type, title,@@ -225,7 +287,8 @@
lastUsedTime, beginGetCredentialOption, isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,- entryGroupId.ifEmpty { title },+ entryGroupId = entryGroupId.ifEmpty { title },+ biometricPromptData = biometricPromptData, ) @RequiresApi(34)@@ -267,18 +330,19 @@
val affiliatedDomain = entry.affiliatedDomain val isDefaultIconPreferredAsSingleProvider = entry.isDefaultIconPreferredAsSingleProvider-+ val biometricPromptData = entry.biometricPromptData val autoSelectAllowed = if (isAutoSelectAllowed) { TRUE_STRING } else { FALSE_STRING }- val isUsingDefaultIconPreferred = if (isDefaultIconPreferredAsSingleProvider) { TRUE_STRING } else { FALSE_STRING }+ val allowedAuthenticators = biometricPromptData?.allowedAuthenticators+ val cryptoObject = biometricPromptData?.cryptoObject val sliceBuilder = Slice.Builder( Uri.EMPTY, SliceSpec( type, REVISION_ID@@ -321,7 +385,6 @@
isUsingDefaultIconPreferred, /*subType=*/null, listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED) )- try { if (entry.hasDefaultIcon) { sliceBuilder.addInt(@@ -332,7 +395,6 @@
} } catch (_: IllegalStateException) { }- if (entry.isAutoSelectAllowedFromOption) { sliceBuilder.addInt( /*true=*/1,@@ -354,6 +416,24 @@
.build(), /*subType=*/null )++ if (biometricPromptData != null) {+ // TODO(b/326243730) : Await biometric team dependency for opId, then add+ val cryptoObjectOpId = cryptoObject?.hashCode()++ if (allowedAuthenticators != null) {+ sliceBuilder.addInt(+ allowedAuthenticators, /*subType=*/null,+ listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)+ )+ }+ if (cryptoObjectOpId != null) {+ sliceBuilder.addInt(+ cryptoObjectOpId, /*subType=*/null,+ listOf(SLICE_HINT_CRYPTO_OP_ID)+ )+ }+ } return sliceBuilder.build() }@@ -381,7 +461,7 @@
var isDefaultIconPreferredAsSingleProvider = false var isDefaultIcon = false var affiliatedDomain: CharSequence? = null-+ var allowedAuth: Int? = null slice.items.forEach { if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) { typeDisplayName = it.text@@ -415,9 +495,19 @@
isDefaultIcon = true } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) { affiliatedDomain = it.text+ } else if (it.hasHint(SLICE_HINT_ALLOWED_AUTHENTICATORS)) {+ allowedAuth = it.int } }+ // TODO(b/326243730) : Await biometric team dependency for opId, then add - also decide+ // if we want toBundle to be passed into the framework.+ var biometricPromptDataBundle: Bundle? = null+ if (allowedAuth != null) {+ biometricPromptDataBundle = Bundle()+ biometricPromptDataBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, allowedAuth!!)+ }+ return try { CustomCredentialEntry( type = type,@@ -439,6 +529,8 @@
autoSelectAllowedFromOption = autoSelectAllowedFromOption, isCreatedFromSlice = true, isDefaultIconFromSlice = isDefaultIcon,+ biometricPromptData = if (biometricPromptDataBundle != null)+ BiometricPromptData.fromBundle(biometricPromptDataBundle) else null ) } catch (e: Exception) { Log.i(TAG, "fromSlice failed with: " + e.message)@@ -489,6 +581,12 @@
private const val SLICE_HINT_DEFAULT_ICON_RES_ID = "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEFAULT_ICON_RES_ID"+ private const val SLICE_HINT_ALLOWED_AUTHENTICATORS =+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS"++ private const val SLICE_HINT_CRYPTO_OP_ID =+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_CRYPTO_OP_ID"+ private const val TRUE_STRING = "true" private const val FALSE_STRING = "false"@@ -512,7 +610,6 @@
} return null }- /** * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object. *@@ -528,7 +625,6 @@
} return null }- /** * Converts a framework [android.service.credentials.CredentialEntry] class to a Jetpack * [CustomCredentialEntry] class@@ -544,7 +640,6 @@
return null } }- /** * Builder for [CustomCredentialEntry] *@@ -576,6 +671,7 @@
private var autoSelectAllowed = false private var entryGroupId: CharSequence = title private var isDefaultIconPreferredAsSingleProvider = false+ private var biometricPromptData: BiometricPromptData? = null /** Sets a displayName to be shown on the UI with this entry. */ fun setSubtitle(subtitle: CharSequence?): Builder {@@ -599,6 +695,17 @@
} /**+ * Sets the biometric prompt data to optionally utilize a credential+ * manager flow that directly handles the biometric verification for you and gives you the+ * response; set to null by default.+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY)+ fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {+ this.biometricPromptData = biometricPromptData+ return this+ }++ /** * Sets whether the entry should be auto-selected. * The value is false by default. */@@ -646,17 +753,18 @@
icon = Icon.createWithResource(context, R.drawable.ic_other_sign_in) } return CustomCredentialEntry(- type,- title,- pendingIntent,- autoSelectAllowed,- subtitle,- typeDisplayName,- icon!!,- lastUsedTime,- beginGetCredentialOption,+ type = type,+ title = title,+ pendingIntent = pendingIntent,+ isAutoSelectAllowed = autoSelectAllowed,+ subtitle = subtitle,+ typeDisplayName = typeDisplayName,+ icon = icon!!,+ lastUsedTime = lastUsedTime,+ beginGetCredentialOption = beginGetCredentialOption, isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider, entryGroupId = entryGroupId,+ biometricPromptData = biometricPromptData, ) } }
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * limitations under the License. */+@file:Suppress("deprecation") // For usage of Slice package androidx.credentials.provider@@ -31,6 +32,7 @@
import androidx.credentials.CredentialOption import androidx.credentials.PasswordCredential import androidx.credentials.R+import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS import androidx.credentials.provider.PasswordCredentialEntry.Companion.toSlice import java.time.Instant import java.util.Collections@@ -71,7 +73,7 @@
* @see CustomCredentialEntry * @see CredentialEntry */-@RequiresApi(26)+@RequiresApi(23) class PasswordCredentialEntry internal constructor( val username: CharSequence, val displayName: CharSequence?,@@ -84,16 +86,18 @@
isDefaultIconPreferredAsSingleProvider: Boolean, entryGroupId: CharSequence? = username, affiliatedDomain: CharSequence? = null,+ biometricPromptData: BiometricPromptData? = null, autoSelectAllowedFromOption: Boolean = CredentialOption.extractAutoSelectValue( beginGetPasswordOption.candidateQueryData), private var isCreatedFromSlice: Boolean = false, private var isDefaultIconFromSlice: Boolean = false ) : CredentialEntry(- PasswordCredential.TYPE_PASSWORD_CREDENTIAL,- beginGetPasswordOption,- entryGroupId ?: username,+ type = PasswordCredential.TYPE_PASSWORD_CREDENTIAL,+ beginGetCredentialOption = beginGetPasswordOption,+ entryGroupId = entryGroupId ?: username, isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider, affiliatedDomain = affiliatedDomain,+ biometricPromptData = biometricPromptData, ) { val isAutoSelectAllowedFromOption = autoSelectAllowedFromOption@@ -147,6 +151,70 @@
* @throws NullPointerException If [context], [username], [pendingIntent], or * [beginGetPasswordOption] is null */+ @RestrictTo(RestrictTo.Scope.LIBRARY) constructor(+ context: Context,+ username: CharSequence,+ pendingIntent: PendingIntent,+ beginGetPasswordOption: BeginGetPasswordOption,+ displayName: CharSequence? = null,+ lastUsedTime: Instant? = null,+ icon: Icon = Icon.createWithResource(context, R.drawable.ic_password),+ isAutoSelectAllowed: Boolean = false,+ affiliatedDomain: CharSequence? = null,+ isDefaultIconPreferredAsSingleProvider: Boolean = false,+ biometricPromptData: BiometricPromptData? = null+ ) : this(+ username,+ displayName,+ typeDisplayName = context.getString(+ R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL+ ),+ pendingIntent,+ lastUsedTime,+ icon,+ isAutoSelectAllowed,+ beginGetPasswordOption,+ isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,+ affiliatedDomain = affiliatedDomain,+ biometricPromptData = biometricPromptData,+ )++ /**+ * @constructor constructs an instance of [PasswordCredentialEntry]+ *+ * The [affiliatedDomain] parameter is filled if you provide a credential+ * that is not directly associated with the requesting entity, but rather originates from an+ * entity that is determined as being associated with the requesting entity through mechanisms+ * such as digital asset links.+ *+ * @param context the context of the calling app, required to retrieve fallback resources+ * @param username the username of the account holding the password credential+ * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this+ * entry, must be created with flag [PendingIntent.FLAG_MUTABLE] to allow the Android+ * system to attach the final request+ * @param beginGetPasswordOption the option from the original [BeginGetCredentialRequest],+ * for which this credential entry is being added+ * @param displayName the displayName of the account holding the password credential+ * @param lastUsedTime the last used time the credential underlying this entry was+ * used by the user, distinguishable up to the milli second mark only such that if two+ * entries have the same millisecond precision, they will be considered to have been used at+ * the same time+ * @param icon the icon to be displayed with this entry on the selector, if not set, a+ * default icon representing a password credential type is set by the library+ * @param isAutoSelectAllowed whether this entry is allowed to be auto+ * selected if it is the only one on the UI, only takes effect if the app requesting for+ * credentials also opts for auto select+ * @param affiliatedDomain the user visible affiliated domain, a CharSequence+ * representation of a web domain or an app package name that the given credential in this+ * entry is associated with when it is different from the requesting entity, default null+ * @param isDefaultIconPreferredAsSingleProvider when set to true, the UI prefers to render the+ * default credential type icon (see the default value of [icon]) when you are the+ * only available provider; false by default+ *+ * @throws IllegalArgumentException If [username] is empty+ * @throws NullPointerException If [context], [username], [pendingIntent], or+ * [beginGetPasswordOption] is null+ */ constructor( context: Context, username: CharSequence,@@ -159,16 +227,16 @@
affiliatedDomain: CharSequence? = null, isDefaultIconPreferredAsSingleProvider: Boolean = false, ) : this(- username,- displayName,+ username = username,+ displayName = displayName, typeDisplayName = context.getString( R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL ),- pendingIntent,- lastUsedTime,- icon,- isAutoSelectAllowed,- beginGetPasswordOption,+ pendingIntent = pendingIntent,+ lastUsedTime = lastUsedTime,+ icon = icon,+ isAutoSelectAllowed = isAutoSelectAllowed,+ beginGetPasswordOption = beginGetPasswordOption, isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider, affiliatedDomain = affiliatedDomain, )@@ -217,16 +285,16 @@
icon: Icon = Icon.createWithResource(context, R.drawable.ic_password), isAutoSelectAllowed: Boolean = false ) : this(- username,- displayName,+ username = username,+ displayName = displayName, typeDisplayName = context.getString( R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL ),- pendingIntent,- lastUsedTime,- icon,- isAutoSelectAllowed,- beginGetPasswordOption,+ pendingIntent = pendingIntent,+ lastUsedTime = lastUsedTime,+ icon = icon,+ isAutoSelectAllowed = isAutoSelectAllowed,+ beginGetPasswordOption = beginGetPasswordOption, isDefaultIconPreferredAsSingleProvider = false )@@ -351,6 +419,23 @@
.build(), /*subType=*/null )+ val biometricPromptData = entry.biometricPromptData+ if (biometricPromptData != null) {+ val allowedAuthenticators = biometricPromptData.allowedAuthenticators+ // TODO(b/326243730) : Await biometric team dependency for opId, then add+ val cryptoObjectOpId = biometricPromptData.cryptoObject?.hashCode()++ sliceBuilder.addInt(+ allowedAuthenticators, /*subType=*/null,+ listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)+ )+ if (cryptoObjectOpId != null) {+ sliceBuilder.addInt(+ cryptoObjectOpId, /*subType=*/null,+ listOf(SLICE_HINT_CRYPTO_OP_ID)+ )+ }+ } return sliceBuilder.build() }@@ -377,6 +462,7 @@
var affiliatedDomain: CharSequence? = null var entryGroupId: CharSequence? = null var isDefaultIcon = false+ var allowedAuth: Int? = null slice.items.forEach { if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {@@ -411,9 +497,19 @@
} } else if (it.hasHint(SLICE_HINT_DEFAULT_ICON_RES_ID)) { isDefaultIcon = true+ } else if (it.hasHint(SLICE_HINT_ALLOWED_AUTHENTICATORS)) {+ allowedAuth = it.int } }+ // TODO(b/326243730) : Await biometric team dependency for opId, then add - also decide+ // if we want toBundle to be passed into the framework.+ var biometricPromptDataBundle: Bundle? = null+ if (allowedAuth != null) {+ biometricPromptDataBundle = Bundle()+ biometricPromptDataBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, allowedAuth!!)+ }+ return try { PasswordCredentialEntry( username = title!!,@@ -433,6 +529,8 @@
autoSelectAllowedFromOption = autoSelectAllowedFromOption, isCreatedFromSlice = true, isDefaultIconFromSlice = isDefaultIcon,+ biometricPromptData = if (biometricPromptDataBundle != null)+ BiometricPromptData.fromBundle(biometricPromptDataBundle) else null ) } catch (e: Exception) { Log.i(TAG, "fromSlice failed with: " + e.message)@@ -483,6 +581,12 @@
private const val SLICE_HINT_AFFILIATED_DOMAIN = "androidx.credentials.provider.credentialEntry.SLICE_HINT_AFFILIATED_DOMAIN"+ private const val SLICE_HINT_ALLOWED_AUTHENTICATORS =+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS"++ private const val SLICE_HINT_CRYPTO_OP_ID =+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_CRYPTO_OP_ID"+ private const val TRUE_STRING = "true" private const val FALSE_STRING = "false"@@ -569,6 +673,7 @@
private var autoSelectAllowed = false private var affiliatedDomain: CharSequence? = null private var isDefaultIconPreferredAsSingleProvider: Boolean = false+ private var biometricPromptData: BiometricPromptData? = null /** Sets a displayName to be shown on the UI with this entry. */ fun setDisplayName(displayName: CharSequence?): Builder {@@ -583,6 +688,17 @@
} /**+ * Sets the biometric prompt data to optionally utilize a credential+ * manager flow that directly handles the biometric verification for you and gives you the+ * response; set to null by default.+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY)+ fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {+ this.biometricPromptData = biometricPromptData+ return this+ }++ /** * Sets whether the entry should be auto-selected. * The value is false by default. */@@ -632,16 +748,17 @@
R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL ) return PasswordCredentialEntry(- username,- displayName,- typeDisplayName,- pendingIntent,- lastUsedTime,- icon!!,- autoSelectAllowed,- beginGetPasswordOption,+ username = username,+ displayName = displayName,+ typeDisplayName = typeDisplayName,+ pendingIntent = pendingIntent,+ lastUsedTime = lastUsedTime,+ icon = icon!!,+ isAutoSelectAllowed = autoSelectAllowed,+ beginGetPasswordOption = beginGetPasswordOption, isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider, affiliatedDomain = affiliatedDomain,+ biometricPromptData = biometricPromptData, ) } }
@@ -16,6 +16,9 @@
package androidx.credentials.provider+import android.util.Log+import androidx.annotation.RestrictTo+import androidx.annotation.VisibleForTesting import androidx.credentials.CreateCredentialRequest /**@@ -27,9 +30,9 @@
* * @constructor constructs an instance of [ProviderCreateCredentialRequest] *- * @param callingRequest the complete [CreateCredentialRequest] coming from+ * @property callingRequest the complete [CreateCredentialRequest] coming from * the calling app that is requesting for credential creation- * @param callingAppInfo information pertaining to the calling app making+ * @property callingAppInfo information pertaining to the calling app making * the request * * @throws NullPointerException If [callingRequest], or [callingAppInfo] is null@@ -37,7 +40,40 @@
* Note : Credential providers are not expected to utilize the constructor in this class for any * production flow. This constructor must only be used for testing purposes. */-class ProviderCreateCredentialRequest constructor(+class ProviderCreateCredentialRequest internal constructor( val callingRequest: CreateCredentialRequest,- val callingAppInfo: CallingAppInfo-)+ val callingAppInfo: CallingAppInfo,+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ val biometricPromptResult: BiometricPromptResult? = null,+ // TODO: Remove when exposing API and make this the only constructor+ isInternal: Boolean = true+) {+ init {+ // TODO: Remove when exposing API+ Log.i("ProvCrCredRequest", isInternal.toString())+ }+ /**+ * Constructs an instance of this class+ *+ * @param callingRequest the complete [CreateCredentialRequest] coming from+ * the calling app that is requesting for credential creation+ * @param callingAppInfo information pertaining to the calling app making+ * the request+ */+ constructor(+ callingRequest: CreateCredentialRequest,+ callingAppInfo: CallingAppInfo,+ ) : this(+ callingRequest = callingRequest,+ callingAppInfo = callingAppInfo,+ isInternal = false+ )++ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ @VisibleForTesting+ constructor(+ callingRequest: CreateCredentialRequest,+ callingAppInfo: CallingAppInfo,+ biometricPromptResult: BiometricPromptResult?+ ) : this(callingRequest, callingAppInfo, biometricPromptResult, isInternal = false)+}
@@ -16,6 +16,8 @@
package androidx.credentials.provider import android.app.PendingIntent+import android.util.Log+import androidx.annotation.RestrictTo import androidx.credentials.CredentialOption /**@@ -30,31 +32,76 @@
* * @constructor constructs an instance of [ProviderGetCredentialRequest] *- * @param credentialOptions the list of credential retrieval options containing the+ * @property credentialOptions the list of credential retrieval options containing the * required parameters, expected to contain a single [CredentialOption] when this * request is retrieved from the [android.app.Activity] invoked by the [android.app.PendingIntent] * set on a [PasswordCredentialEntry] or a [PublicKeyCredentialEntry], or expected to contain * multiple [CredentialOption] when this request is retrieved * from the [android.app.Activity] invoked by the [android.app.PendingIntent] * set on a [RemoteEntry]- * @param callingAppInfo information pertaining to the calling application+ * @property callingAppInfo information pertaining to the calling application * * Note : Credential providers are not expected to utilize the constructor in this class for any * production flow. This constructor must only be used for testing purposes. */-class ProviderGetCredentialRequest constructor(+class ProviderGetCredentialRequest internal constructor( val credentialOptions: List,- val callingAppInfo: CallingAppInfo+ val callingAppInfo: CallingAppInfo,+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ val biometricPromptResult: BiometricPromptResult? = null,+ // TODO: Remove when exposing this API+ isInternal: Boolean = false ) {+ init {+ // TODO: Remove when exposing API+ Log.i("ProvCrCredRequest", isInternal.toString())+ }++ /**+ * Constructs an instance of this class+ *+ * @param credentialOptions the list of credential retrieval options containing the+ * required parameters, expected to contain a single [CredentialOption] when this+ * request is retrieved from the [android.app.Activity] invoked by the [android.app.PendingIntent]+ * set on a [PasswordCredentialEntry] or a [PublicKeyCredentialEntry], or expected to contain+ * multiple [CredentialOption] when this request is retrieved+ * from the [android.app.Activity] invoked by the [android.app.PendingIntent]+ * set on a [RemoteEntry]+ * @param callingAppInfo information pertaining to the calling application+ */+ constructor(+ credentialOptions: List,+ callingAppInfo: CallingAppInfo,+ ) : this(+ credentialOptions = credentialOptions,+ callingAppInfo = callingAppInfo,+ isInternal = false+ )++ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ constructor(+ credentialOptions: List,+ callingAppInfo: CallingAppInfo,+ biometricPromptResult: BiometricPromptResult?+ ) : this(+ credentialOptions = credentialOptions,+ callingAppInfo = callingAppInfo,+ biometricPromptResult = biometricPromptResult,+ isInternal = false+ )+ internal companion object { @JvmStatic internal fun createFrom( options: List,- callingAppInfo: CallingAppInfo+ callingAppInfo: CallingAppInfo,+ biometricPromptResult: BiometricPromptResult? = null ): ProviderGetCredentialRequest { return ProviderGetCredentialRequest( options,- callingAppInfo)+ callingAppInfo,+ biometricPromptResult+ ) } } }
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * limitations under the License. */+@file:Suppress("deprecation") // For usage of Slice package androidx.credentials.provider@@ -31,6 +32,7 @@
import androidx.credentials.CredentialOption import androidx.credentials.PublicKeyCredential import androidx.credentials.R+import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.toSlice import java.time.Instant import java.util.Collections@@ -73,7 +75,8 @@
* * @see CredentialEntry */-@RequiresApi(26)+@RequiresApi(23)+@Suppress("DEPRECATION") // For usage of slice class PublicKeyCredentialEntry internal constructor( val username: CharSequence, val displayName: CharSequence?,@@ -86,17 +89,19 @@
isDefaultIconPreferredAsSingleProvider: Boolean, entryGroupId: CharSequence? = username, affiliatedDomain: CharSequence? = null,+ biometricPromptData: BiometricPromptData? = null, autoSelectAllowedFromOption: Boolean = CredentialOption.extractAutoSelectValue( beginGetPublicKeyCredentialOption.candidateQueryData ), private val isCreatedFromSlice: Boolean = false, private val isDefaultIconFromSlice: Boolean = false, ) : CredentialEntry(- PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,- beginGetPublicKeyCredentialOption,- entryGroupId ?: username,+ type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,+ beginGetCredentialOption = beginGetPublicKeyCredentialOption,+ entryGroupId = entryGroupId ?: username, isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider, affiliatedDomain = affiliatedDomain,+ biometricPromptData = biometricPromptData, ) { val isAutoSelectAllowedFromOption = autoSelectAllowedFromOption@@ -144,7 +149,7 @@
* [beginGetPublicKeyCredentialOption] is null * @throws IllegalArgumentException if [username] is empty */- constructor(+ @RestrictTo(RestrictTo.Scope.LIBRARY) constructor( context: Context, username: CharSequence, pendingIntent: PendingIntent,@@ -154,6 +159,7 @@
icon: Icon = Icon.createWithResource(context, R.drawable.ic_passkey), isAutoSelectAllowed: Boolean = false, isDefaultIconPreferredAsSingleProvider: Boolean = false,+ biometricPromptData: BiometricPromptData? = null, ) : this( username, displayName,@@ -165,7 +171,62 @@
lastUsedTime, isAutoSelectAllowed, beginGetPublicKeyCredentialOption,- isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider+ isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,+ biometricPromptData = biometricPromptData,+ )++ /**+ * @constructor constructs an instance of [PublicKeyCredentialEntry]+ *+ * @param context the context of the calling app, required to retrieve fallback resources+ * @param username the username of the account holding the public key credential+ * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this+ * entry, must be created with a unique request code per entry,+ * with flag [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the+ * final request, and NOT with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple+ * times+ * @param beginGetPublicKeyCredentialOption the option from the original+ * [BeginGetCredentialRequest], for which this credential entry is being added+ * @param displayName the displayName of the account holding the public key credential+ * @param lastUsedTime the last used time the credential underlying this entry was+ * used by the user, distinguishable up to the milli second mark only such that if two+ * entries have the same millisecond precision, they will be considered to have been used at+ * the same time+ * @param icon the icon to be displayed with this entry on the selector, if not set, a+ * default icon representing a public key credential type is set by the library+ * @param isAutoSelectAllowed whether this entry is allowed to be auto+ * selected if it is the only one on the UI, only takes effect if the app requesting for+ * credentials also opts for auto select+ * @param isDefaultIconPreferredAsSingleProvider when set to true, the UI prefers to render the+ * default credential type icon (see the default value of [icon]) when you are the+ * only available provider; false by default+ *+ * @throws NullPointerException If [context], [username], [pendingIntent], or+ * [beginGetPublicKeyCredentialOption] is null+ * @throws IllegalArgumentException if [username] is empty+ */+ constructor(+ context: Context,+ username: CharSequence,+ pendingIntent: PendingIntent,+ beginGetPublicKeyCredentialOption: BeginGetPublicKeyCredentialOption,+ displayName: CharSequence? = null,+ lastUsedTime: Instant? = null,+ icon: Icon = Icon.createWithResource(context, R.drawable.ic_passkey),+ isAutoSelectAllowed: Boolean = false,+ isDefaultIconPreferredAsSingleProvider: Boolean = false+ ) : this(+ username = username,+ displayName = displayName,+ typeDisplayName = context.getString(+ R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL+ ),+ pendingIntent = pendingIntent,+ icon = icon,+ lastUsedTime = lastUsedTime,+ isAutoSelectAllowed = isAutoSelectAllowed,+ beginGetPublicKeyCredentialOption = beginGetPublicKeyCredentialOption,+ isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider, ) /**@@ -195,7 +256,7 @@
* [beginGetPublicKeyCredentialOption] is null * @throws IllegalArgumentException if [username] is empty */- @Deprecated("Use the constructor that allows setting all parameters.",+ @Deprecated("Use the constructor with all parameters dependent on API levels", replaceWith = ReplaceWith("PublicKeyCredentialEntry(context, username, pendingIntent," + "beginGetPublicKeyCredentialOption, displayName, lastUsedTime, icon, " + "isAutoSelectAllowed, isDefaultIconPreferredAsSingleProvider)"),@@ -211,16 +272,16 @@
icon: Icon = Icon.createWithResource(context, R.drawable.ic_passkey), isAutoSelectAllowed: Boolean = false, ) : this(- username,- displayName,- context.getString(+ username = username,+ displayName = displayName,+ typeDisplayName = context.getString( R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL ),- pendingIntent,- icon,- lastUsedTime,- isAutoSelectAllowed,- beginGetPublicKeyCredentialOption,+ pendingIntent = pendingIntent,+ icon = icon,+ lastUsedTime = lastUsedTime,+ isAutoSelectAllowed = isAutoSelectAllowed,+ beginGetPublicKeyCredentialOption = beginGetPublicKeyCredentialOption, isDefaultIconPreferredAsSingleProvider = false )@@ -350,6 +411,24 @@
.build(), /*subType=*/null )++ val biometricPromptData = entry.biometricPromptData+ if (biometricPromptData != null) {+ val allowedAuthenticators = biometricPromptData.allowedAuthenticators+ // TODO(b/326243730) : Await biometric team dependency for opId, then add+ val cryptoObjectOpId = biometricPromptData.cryptoObject?.hashCode()++ sliceBuilder.addInt(+ allowedAuthenticators, /*subType=*/null,+ listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)+ )+ if (cryptoObjectOpId != null) {+ sliceBuilder.addInt(+ cryptoObjectOpId, /*subType=*/null,+ listOf(SLICE_HINT_CRYPTO_OP_ID)+ )+ }+ } return sliceBuilder.build() }@@ -375,6 +454,7 @@
var isDefaultIcon = false var entryGroupId: CharSequence? = null var affiliatedDomain: CharSequence? = null+ var allowedAuth: Int? = null slice.items.forEach { if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {@@ -409,9 +489,19 @@
entryGroupId = it.text } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) { affiliatedDomain = it.text+ } else if (it.hasHint(SLICE_HINT_ALLOWED_AUTHENTICATORS)) {+ allowedAuth = it.int } }+ // TODO(b/326243730) : Await biometric team dependency for opId, then add - also decide+ // if we want toBundle to be passed into the framework.+ var biometricPromptDataBundle: Bundle? = null+ if (allowedAuth != null) {+ biometricPromptDataBundle = Bundle()+ biometricPromptDataBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, allowedAuth!!)+ }+ return try { PublicKeyCredentialEntry( username = title!!,@@ -432,6 +522,8 @@
autoSelectAllowedFromOption = autoSelectAllowedFromOption, isCreatedFromSlice = true, isDefaultIconFromSlice = isDefaultIcon,+ biometricPromptData = if (biometricPromptDataBundle != null)+ BiometricPromptData.fromBundle(biometricPromptDataBundle) else null ) } catch (e: Exception) { Log.i(TAG, "fromSlice failed with: " + e.message)@@ -482,6 +574,12 @@
private const val SLICE_HINT_DEDUPLICATION_ID = "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEDUPLICATION_ID"+ private const val SLICE_HINT_ALLOWED_AUTHENTICATORS =+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS"++ private const val SLICE_HINT_CRYPTO_OP_ID =+ "androidx.credentials.provider.credentialEntry.SLICE_HINT_CRYPTO_OP_ID"+ private const val TRUE_STRING = "true" private const val FALSE_STRING = "false"@@ -555,6 +653,7 @@
private var icon: Icon? = null private var autoSelectAllowed: Boolean = false private var isDefaultIconPreferredAsSingleProvider: Boolean = false+ private var biometricPromptData: BiometricPromptData? = null /** Sets a displayName to be shown on the UI with this entry */ fun setDisplayName(displayName: CharSequence?): Builder {@@ -569,6 +668,17 @@
} /**+ * Sets the biometric prompt data to optionally utilize a credential+ * manager flow that directly handles the biometric verification for you and gives you the+ * response; set to null by default.+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY)+ fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {+ this.biometricPromptData = biometricPromptData+ return this+ }++ /** * Sets whether the entry should be auto-selected. * The value is false by default */@@ -608,15 +718,16 @@
R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL ) return PublicKeyCredentialEntry(- username,- displayName,- typeDisplayName,- pendingIntent,- icon!!,- lastUsedTime,- autoSelectAllowed,- beginGetPublicKeyCredentialOption,- isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider+ username = username,+ displayName = displayName,+ typeDisplayName = typeDisplayName,+ pendingIntent = pendingIntent,+ icon = icon!!,+ lastUsedTime = lastUsedTime,+ isAutoSelectAllowed = autoSelectAllowed,+ beginGetPublicKeyCredentialOption = beginGetPublicKeyCredentialOption,+ isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,+ biometricPromptData = biometricPromptData ) } }
@@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * limitations under the License. */+@file:Suppress("deprecation") // For usage of Slice+ package androidx.credentials.provider import android.annotation.SuppressLint
@@ -5,12 +5,12 @@
#--------------------------------------------------------------------- # Uncomment this option if you want to customize path to IDE config folder. Make sure you're using forward slashes. #----------------------------------------------------------------------idea.config.path=${user.home}/.AndroidStudioAndroidX/config+idea.config.path=${user.home}/.AndroidStudioAndroidXPlatform/config #--------------------------------------------------------------------- # Uncomment this option if you want to customize path to IDE system folder. Make sure you're using forward slashes. #----------------------------------------------------------------------idea.system.path=${user.home}/.AndroidStudioAndroidX/system+idea.system.path=${user.home}/.AndroidStudioAndroidXPlatform/system #--------------------------------------------------------------------- # Uncomment this option if you want to customize path to user installed plugins folder. Make sure you're using forward slashes.@@ -194,4 +194,4 @@
#----------------------------------------------------------------------- # Enable compose @Preview rendering #------------------------------------------------------------------------compose.project.uses.compose.override=true+compose.project.uses.compose.override=true
\ No newline at end of file
@@ -30,11 +30,11 @@
# Remove when AGP defaults to 2.1.0 android.prefabVersion=2.1.0-# Do generate versioned API files-androidx.writeVersionedApiFiles=true+# Don't generate versioned API files+androidx.writeVersionedApiFiles=false-# Do run the CheckAarMetadata task-android.experimental.disableCompileSdkChecks=false+# Don't run the CheckAarMetadata task+android.experimental.disableCompileSdkChecks=true # Don't warn about needing to update AGP android.suppressUnsupportedCompileSdk=UpsideDownCake,VanillaIceCream,33,34@@ -42,7 +42,7 @@
androidx.compileSdk=34 androidx.targetSdkVersion=34 androidx.allowCustomCompileSdk=true-androidx.includeOptionalProjects=false+androidx.includeOptionalProjects=true # Keep ComposeCompiler pinned unless performing Kotlin upgrade & ComposeCompiler release androidx.unpinComposeCompiler=false
@@ -0,0 +1,109 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+@file:Suppress("DEPRECATION")++package androidx.recyclerview.widget++import android.graphics.Color+import android.os.Build+import android.view.View+import android.view.ViewGroup+import android.view.ViewTreeObserver+import android.widget.TextView+import androidx.core.os.BuildCompat+import androidx.test.ext.junit.runners.AndroidJUnit4+import androidx.test.filters.MediumTest+import androidx.test.filters.SdkSuppress+import androidx.test.rule.ActivityTestRule+import com.google.common.truth.Truth.assertThat+import java.util.concurrent.CountDownLatch+import java.util.concurrent.TimeUnit+import org.junit.Rule+import org.junit.Test+import org.junit.runner.RunWith++@MediumTest+// TODO: change to VANILLA_ICE_CREAM when it is ready+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)+@RunWith(AndroidJUnit4::class)+class RecyclerViewScrollFrameRateTest {+ @get:Rule+ val rule = ActivityTestRule(TestContentViewActivity::class.java)++ @Test+ fun smoothScrollFrameRateBoost() {+ // TODO: Remove when VANILLA_ICE_CREAM is ready and the SdkSuppress is modified+ if (!BuildCompat.isAtLeastV()) {+ return+ }+ val rv = RecyclerView(rule.activity)+ rule.runOnUiThread {+ rv.layoutManager =+ LinearLayoutManager(rule.activity, LinearLayoutManager.VERTICAL, false)+ rv.adapter = object : RecyclerView.Adapter() {+ override fun onCreateViewHolder(+ parent: ViewGroup,+ viewType: Int+ ): RecyclerView.ViewHolder {+ val view = TextView(parent.context)+ view.textSize = 40f+ view.setTextColor(Color.WHITE)+ return object : RecyclerView.ViewHolder(view) {}+ }++ override fun getItemCount(): Int = 10000++ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {+ val view = holder.itemView as TextView+ view.text = "Text $position"+ val color = if (position % 2 == 0) Color.BLACK else 0xFF000080.toInt()+ view.setBackgroundColor(color)+ }+ }+ rule.activity.contentView.addView(rv)+ }+ runOnDraw(rv, { rv.smoothScrollBy(0, 1000) }) {+ // First Frame+ assertThat(rv.frameContentVelocity).isGreaterThan(0f)+ }++ // Second frame+ runOnDraw(rv) {+ assertThat(rv.frameContentVelocity).isGreaterThan(0f)+ }++ // Third frame+ runOnDraw(rv) {+ assertThat(rv.frameContentVelocity).isGreaterThan(0f)+ }+ }++ private fun runOnDraw(view: View, setup: () -> Unit = { }, onDraw: () -> Unit) {+ val latch = CountDownLatch(1)+ val onDrawListener = ViewTreeObserver.OnDrawListener {+ latch.countDown()+ onDraw()+ }+ rule.runOnUiThread {+ view.viewTreeObserver.addOnDrawListener(onDrawListener)+ setup()+ }+ assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue()+ rule.runOnUiThread {+ view.viewTreeObserver.removeOnDrawListener(onDrawListener)+ }+ }+}
@@ -19,71 +19,66 @@
import android.text.TextUtils; import androidx.annotation.NonNull;+import androidx.annotation.Nullable;++import com.example.androidx.mediarouting.activities.systemrouting.source.SystemRoutesSource; import java.util.Objects;-import javax.annotation.Nullable;--/**- * An abstract model that holds information about routes from different sources.- *- * Can represent media routers' routes, bluetooth routes, or audio routes.- */+/** Holds information about a system route. */ public final class SystemRouteItem implements SystemRoutesAdapterItem {- @NonNull- private final String mId;+ /**+ * Describes the support of a route for selection.+ *+ *
We understand by selection the action that makes a specific route the active route. Note
+ * that this terminology may not match the terminology used by the underlying {@link+ * SystemRoutesSource}.+ */+ public enum SelectionSupportState {+ /** The underlying route source doesn't support selection. */+ UNSUPPORTED,+ /**+ * The corresponding route is already selected, but can be reselected.+ *+ *
Selecting an already selected route (reselection) can change the metadata of the route
+ * source. For example, reselecting a MediaRouter2 route can alter the transfer reason.+ */+ RESELECTABLE,+ /** The route is available for selection. */+ SELECTABLE+ }- @NonNull- private final String mName;+ /** The {@link SystemRoutesSource#getSourceId()} of the source that created this item. */+ @NonNull public final String mSourceId;- @Nullable- private final String mAddress;+ /** An id that uniquely identifies this route item within the source. */+ @NonNull public final String mId;- @Nullable- private final String mDescription;+ @NonNull public final String mName;++ @Nullable public final String mAddress;++ @Nullable public final String mDescription;++ @Nullable public final String mSuitabilityStatus;++ @Nullable public final Boolean mTransferInitiatedBySelf;++ @Nullable public final String mTransferReason;++ @NonNull public final SelectionSupportState mSelectionSupportState; private SystemRouteItem(@NonNull Builder builder) {- Objects.requireNonNull(builder.mId);- Objects.requireNonNull(builder.mName);-- mId = builder.mId;- mName = builder.mName;-+ mSourceId = Objects.requireNonNull(builder.mSourceId);+ mId = Objects.requireNonNull(builder.mId);+ mName = Objects.requireNonNull(builder.mName); mAddress = builder.mAddress; mDescription = builder.mDescription;- }-- /**- * Returns a unique identifier of a route.- */- @NonNull- public String getId() {- return mId;- }-- /**- * Returns a human-readable name of the route.- */- @NonNull- public String getName() {- return mName;- }-- /**- * Returns address if the route is a Bluetooth route and {@code null} otherwise.- */- @Nullable- public String getAddress() {- return mAddress;- }-- /**- * Returns a route description or {@code null} if empty.- */- @Nullable- public String getDescription() {- return mDescription;+ mSuitabilityStatus = builder.mSuitabilityStatus;+ mTransferInitiatedBySelf = builder.mTransferInitiatedBySelf;+ mTransferReason = builder.mTransferReason;+ mSelectionSupportState = builder.mSelectionSupportState; } @Override@@ -91,14 +86,29 @@
if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SystemRouteItem that = (SystemRouteItem) o;- return mId.equals(that.mId) && mName.equals(that.mName)- && Objects.equals(mAddress, that.mAddress) && Objects.equals(- mDescription, that.mDescription);+ return mSourceId.equals(that.mSourceId)+ && mId.equals(that.mId)+ && mName.equals(that.mName)+ && Objects.equals(mAddress, that.mAddress)+ && Objects.equals(mDescription, that.mDescription)+ && Objects.equals(mSuitabilityStatus, that.mSuitabilityStatus)+ && Objects.equals(mTransferInitiatedBySelf, that.mTransferInitiatedBySelf)+ && Objects.equals(mTransferReason, that.mTransferReason)+ && mSelectionSupportState.equals(that.mSelectionSupportState); } @Override public int hashCode() {- return Objects.hash(mId, mName, mAddress, mDescription);+ return Objects.hash(+ mSourceId,+ mId,+ mName,+ mAddress,+ mDescription,+ mSuitabilityStatus,+ mTransferInitiatedBySelf,+ mTransferReason,+ mSelectionSupportState); } /**@@ -106,20 +116,26 @@
*/ public static final class Builder {- @NonNull- private final String mId;+ @NonNull private String mSourceId;+ @NonNull private final String mId;+ @NonNull private String mName;+ @Nullable private String mAddress;+ @Nullable private String mDescription;+ @Nullable private String mSuitabilityStatus;+ @Nullable private Boolean mTransferInitiatedBySelf;+ @Nullable private String mTransferReason;+ @NonNull public SelectionSupportState mSelectionSupportState;- @NonNull- private String mName;-- @Nullable- private String mAddress;-- @Nullable- private String mDescription;-- public Builder(@NonNull String id) {+ /**+ * Creates a builder with the mandatory properties.+ *+ * @param sourceId See {@link SystemRouteItem#mSourceId}.+ * @param id See {@link SystemRouteItem#mId}.+ */+ public Builder(@NonNull String sourceId, @NonNull String id) {+ mSourceId = sourceId; mId = id;+ mSelectionSupportState = SelectionSupportState.UNSUPPORTED; } /**@@ -154,6 +170,43 @@
} /**+ * Sets a human-readable string describing the transfer suitability of the route, or null if+ * not applicable.+ */+ @NonNull+ public Builder setSuitabilityStatus(@Nullable String suitabilityStatus) {+ mSuitabilityStatus = suitabilityStatus;+ return this;+ }++ /**+ * Sets whether the corresponding route's selection is the result of an action of this app,+ * or null if not applicable.+ */+ @NonNull+ public Builder setTransferInitiatedBySelf(@Nullable Boolean transferInitiatedBySelf) {+ mTransferInitiatedBySelf = transferInitiatedBySelf;+ return this;+ }++ /**+ * Sets a human-readable string describing the transfer reason, or null if not applicable.+ */+ @NonNull+ public Builder setTransferReason(@Nullable String transferReason) {+ mTransferReason = transferReason;+ return this;+ }++ /** Sets the {@link SelectionSupportState} for the corresponding route. */+ @NonNull+ public Builder setSelectionSupportState(+ @NonNull SelectionSupportState selectionSupportState) {+ mSelectionSupportState = Objects.requireNonNull(selectionSupportState);+ return this;+ }++ /** * Builds {@link SystemRouteItem}. */ @NonNull
@@ -17,7 +17,6 @@
package com.example.androidx.mediarouting.activities.systemrouting.source; import androidx.annotation.NonNull;-import androidx.annotation.Nullable; import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem; import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutesSourceItem;@@ -29,23 +28,11 @@
*/ public abstract class SystemRoutesSource {- private static final NoOpOnRoutesChangedListener NO_OP_ON_ROUTES_CHANGED_LISTENER =- new NoOpOnRoutesChangedListener();+ @NonNull protected Runnable mOnRoutesChangedListener = () -> {};- @NonNull- protected OnRoutesChangedListener mOnRoutesChangedListener = NO_OP_ON_ROUTES_CHANGED_LISTENER;-- /**- * Sets {@link OnRoutesChangedListener} and subscribes to the source updates.- * To unsubscribe from the routes update pass {@code null} instead of the listener.- */- public void setOnRoutesChangedListener(- @Nullable OnRoutesChangedListener onRoutesChangedListener) {- if (onRoutesChangedListener != null) {- mOnRoutesChangedListener = onRoutesChangedListener;- } else {- mOnRoutesChangedListener = NO_OP_ON_ROUTES_CHANGED_LISTENER;- }+ /** Sets a {@link Runnable} to invoke whenever routes change. */+ public void setOnRoutesChangedListener(@NonNull Runnable onRoutesChangedListener) {+ mOnRoutesChangedListener = onRoutesChangedListener; } /**@@ -63,6 +50,12 @@
// Empty on purpose. }+ /** Returns a string that uniquely identifies this source. */+ @NonNull+ public final String getSourceId() {+ return getClass().getSimpleName();+ }+ /** * Gets a source item containing source type. */@@ -76,39 +69,11 @@
public abstract List fetchSourceRouteItems(); /**- * An interface for listening to routes changes: whether the route has been added or removed- * from the source.+ * Selects the route that corresponds to the given item.+ *+ * @param item An item with {@link SystemRouteItem#mSelectionSupportState} {@link+ * SystemRouteItem.SelectionSupportState#SELECTABLE}.+ * @return Whether the selection was successful. */- public interface OnRoutesChangedListener {-- /**- * Called when a route has been added to the source's routes list.- *- * @param routeItem a newly added route.- */- void onRouteAdded(@NonNull SystemRouteItem routeItem);-- /**- * Called when a route has been removed from the source's routes list.- *- * @param routeItem a recently removed route.- */- void onRouteRemoved(@NonNull SystemRouteItem routeItem);- }-- /**- * Default no-op implementation of {@link OnRoutesChangedListener}.- * Used as a fallback implement when there is no listener.- */- private static final class NoOpOnRoutesChangedListener implements OnRoutesChangedListener {- @Override- public void onRouteAdded(@NonNull SystemRouteItem routeItem) {- // Empty on purpose.- }-- @Override- public void onRouteRemoved(@NonNull SystemRouteItem routeItem) {- // Empty on purpose.- }- }+ public abstract boolean select(@NonNull SystemRouteItem item); }
@@ -58,12 +58,16 @@
/** Reload all routes provided by this service. */ public void reloadRoutes() {- mDynamicGroupMediaRouteProvider.reloadRoutes();+ if (mDynamicGroupMediaRouteProvider != null) {+ mDynamicGroupMediaRouteProvider.reloadRoutes();+ } } /** Reload the flag for isDynamicRouteEnabled. */ public void reloadDynamicRoutesEnabled() {- mDynamicGroupMediaRouteProvider.reloadDynamicRoutesEnabled();+ if (mDynamicGroupMediaRouteProvider != null) {+ mDynamicGroupMediaRouteProvider.reloadDynamicRoutesEnabled();+ } } /**
@@ -43,9 +43,7 @@
/** * Support for opening android intents on other devices. *- * * The following example opens play store for the given app on another device:- * * ``` * val remoteActivityHelper = RemoteActivityHelper(context, executor) *@@ -57,23 +55,24 @@
* nodeId) * ``` *- * [startRemoteActivity] returns a [ListenableFuture], which is completed after the intent has- * been sent or failed if there was an issue with sending the intent.+ * [startRemoteActivity] returns a [ListenableFuture], which is completed after the intent has been+ * sent or failed if there was an issue with sending the intent. * * nodeId is the opaque string that represents a- * [node](https://developers.google.com/android/reference/com/google/android/gms/wearable/Node)- * in the Android Wear network. For the given device, it can obtained by `NodeClient.getLocalNode()`+ * [node](https://developers.google.com/android/reference/com/google/android/gms/wearable/Node) in+ * the Android Wear network. For the given device, it can obtained by `NodeClient.getLocalNode()` * and the list of nodes to which this device is currently connected can be obtained by * `NodeClient.getConnectedNodes()`. More information about this can be found * [here](https://developers.google.com/android/reference/com/google/android/gms/wearable/NodeClient). * * @param context The [Context] of the application for sending the intent.- * @param executor [Executor] used for getting data to be passed in remote intent. If not- * specified, default will be `Executors.newSingleThreadExecutor()`.+ * @param executor [Executor] used for getting data to be passed in remote intent. If not specified,+ * default will be `Executors.newSingleThreadExecutor()`. */ /* ktlint-enable max-line-length */ public class RemoteActivityHelper- @JvmOverloads constructor(+@JvmOverloads+constructor( private val context: Context, private val executor: Executor = Executors.newSingleThreadExecutor() ) {@@ -121,7 +120,7 @@
*/ public const val RESULT_OK: Int = 0- /** Result code passed to [ResultReceiver.send] when a remote intent failed to send. */+ /** Result code passed to [ResultReceiver.send] when a remote intent failed to send. */ public const val RESULT_FAILED: Int = 1 internal const val DEFAULT_PACKAGE = "com.google.android.wearable.app"@@ -144,8 +143,7 @@
* @return The node id, or null if none was set. */ @JvmStatic- public fun getTargetNodeId(intent: Intent): String? =- intent.getStringExtra(EXTRA_NODE_ID)+ public fun getTargetNodeId(intent: Intent): String? = intent.getStringExtra(EXTRA_NODE_ID) /** * Returns the [android.os.ResultReceiver] extra of remote intent.@@ -158,7 +156,7 @@
internal fun getRemoteIntentResultReceiver(intent: Intent): ResultReceiver? = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER)- /** Re-package a result receiver as a vanilla version for cross-process sending */+ /** Re-package a result receiver as a vanilla version for cross-process sending */ @JvmStatic internal fun getResultReceiverForSending(receiver: ResultReceiver): ResultReceiver { val parcel = Parcel.obtain()@@ -170,11 +168,8 @@
} }- /**- * Used for testing only, so we can set mock NodeClient.- */- @VisibleForTesting- internal var nodeClient: NodeClient = Wearable.getNodeClient(context)+ /** Used for testing only, so we can set mock NodeClient. */+ @VisibleForTesting internal var nodeClient: NodeClient = Wearable.getNodeClient(context) /** Used for testing only, so we can mock wear sdk dependency. */ @VisibleForTesting internal var remoteInteractionsManager: IRemoteInteractionsManager = RemoteInteractionsManagerCompat(context)@@ -231,24 +226,23 @@
} /**- * Start an activity on another device. This api currently supports sending intents with- * action set to [android.content.Intent.ACTION_VIEW], a data uri populated using+ * Start an activity on another device. This api currently supports sending intents with action+ * set to [android.content.Intent.ACTION_VIEW], a data uri populated using * [android.content.Intent.setData], and with the category- * [android.content.Intent.CATEGORY_BROWSABLE] present. If the current device is a watch,- * the activity will start on the companion phone device. Otherwise, the activity will- * start on all connected watch devices.+ * [android.content.Intent.CATEGORY_BROWSABLE] present. If the current device is a watch, the+ * activity will start on the companion phone device. Otherwise, the activity will start on all+ * connected watch devices. *- * @param targetIntent The intent to open on the remote device. Action must be set to- * [android.content.Intent.ACTION_VIEW], a data uri must be populated- * using [android.content.Intent.setData], and the category- * [android.content.Intent.CATEGORY_BROWSABLE] must be present.- * @param targetNodeId Wear OS node id for the device where the activity should be- * started. If null, and the current device is a watch, the- * activity will start on the companion phone device. Otherwise,- * the activity will start on all connected watch devices.- * @return The [ListenableFuture] which resolves if starting activity was successful or- * throws [Exception] if any errors happens. If there's a problem with starting remote- * activity, [RemoteIntentException] will be thrown.+ * @param targetIntent The intent to open on the remote device. Action must be set to+ * [android.content.Intent.ACTION_VIEW], a data uri must be populated using+ * [android.content.Intent.setData], and the category+ * [android.content.Intent.CATEGORY_BROWSABLE] must be present.+ * @param targetNodeId Wear OS node id for the device where the activity should be started. If+ * null, and the current device is a watch, the activity will start on the companion phone+ * device. Otherwise, the activity will start on all connected watch devices.+ * @return The [ListenableFuture] which resolves if starting activity was successful or throws+ * [Exception] if any errors happens. If there's a problem with starting remote activity,+ * [RemoteIntentException] will be thrown. */ @JvmOverloads public fun startRemoteActivity(@@ -268,7 +262,10 @@
} startCreatingIntentForRemoteActivity(- targetIntent, targetNodeId, it, nodeClient,+ targetIntent,+ targetNodeId,+ it,+ nodeClient, object : Callback { override fun intentCreated(intent: Intent) { context.sendBroadcast(intent)@@ -302,10 +299,9 @@
} if (nodeId != null) {- nodeClient.getCompanionPackageForNode(nodeId)- .addOnSuccessListener(- executor- ) { taskPackageName ->+ nodeClient+ .getCompanionPackageForNode(nodeId)+ .addOnSuccessListener(executor) { taskPackageName -> val packageName = taskPackageName ?: DEFAULT_PACKAGE if (packageName.isEmpty()) {@@ -320,35 +316,37 @@
) ) }- }.addOnFailureListener(executor) { callback.onFailure(it) }+ }+ .addOnFailureListener(executor) { callback.onFailure(it) } return }- nodeClient.connectedNodes.addOnSuccessListener(- executor- ) { connectedNodes ->- if (connectedNodes.size == 0) {- callback.onFailure(NotFoundException("No devices connected"))- } else {- val resultReceiver = RemoteIntentResultReceiver(completer, connectedNodes.size)- for (node in connectedNodes) {- nodeClient.getCompanionPackageForNode(node.id).addOnSuccessListener(- executor- ) { taskPackageName ->- val packageName = taskPackageName ?: DEFAULT_PACKAGE- callback.intentCreated(- createIntent(intent, resultReceiver, node.id, packageName)- )- }.addOnFailureListener(executor) { callback.onFailure(it) }+ nodeClient.connectedNodes+ .addOnSuccessListener(executor) { connectedNodes ->+ if (connectedNodes.size == 0) {+ callback.onFailure(NotFoundException("No devices connected"))+ } else {+ val resultReceiver = RemoteIntentResultReceiver(completer, connectedNodes.size)+ for (node in connectedNodes) {+ nodeClient+ .getCompanionPackageForNode(node.id)+ .addOnSuccessListener(executor) { taskPackageName ->+ val packageName = taskPackageName ?: DEFAULT_PACKAGE+ callback.intentCreated(+ createIntent(intent, resultReceiver, node.id, packageName)+ )+ }+ .addOnFailureListener(executor) { callback.onFailure(it) }+ } } }- }.addOnFailureListener(executor) { callback.onFailure(it) }+ .addOnFailureListener(executor) { callback.onFailure(it) } } /**- * Creates [android.content.Intent] with action specifying remote intent. If any of- * additional extras are specified, they will be added to it. If specified, [ResultReceiver]- * will be re-packed to be parcelable. If specified, packageName will be set.+ * Creates [android.content.Intent] with action specifying remote intent. If any of additional+ * extras are specified, they will be added to it. If specified, [ResultReceiver] will be+ * re-packed to be parcelable. If specified, packageName will be set. */ @VisibleForTesting internal fun createIntent(@@ -371,9 +369,7 @@
return remoteIntent }- /**- * Result code passed to [ResultReceiver.send] for the status of remote intent.- */+ /** Result code passed to [ResultReceiver.send] for the status of remote intent. */ @IntDef(RESULT_OK, RESULT_FAILED) @Retention(AnnotationRetention.SOURCE) internal annotation class SendResult@@ -382,6 +378,7 @@
private interface Callback { fun intentCreated(intent: Intent)+ fun onFailure(exception: Exception) }
@@ -26,19 +26,17 @@
* * ones creating Intents * * ones receiving and responding to those Intents. *- *- * To register a configuration activity for a watch face, add a `` entry to the- * watch face component in its Android Manifest file with an intent action to be fired to start the+ * To register a configuration activity for a watch face, add a `` entry to the watch+ * face component in its Android Manifest file with an intent action to be fired to start the * activity. The following meta-data will register the- * `androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR` action to be started when- * configuring a watch face on the wearable device:+ * `androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR` action to be started when configuring a+ * watch face on the wearable device: * ``` * * android:name="com.google.android.wearable.watchface.wearableConfigurationAction" * android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" /> * ``` *- * * To register a configuration activity to be started on a companion phone, add the following * alternative meta-data entry to the watch face component: * ```@@ -47,9 +45,8 @@
* android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" /> * ``` *- *- * The activity should have an intent filter which lists the action specified in the meta-data- * block above, in addition to the two categories present in the following example:+ * The activity should have an intent filter which lists the action specified in the meta-data block+ * above, in addition to the two categories present in the following example: * ``` * * @@ -61,7 +58,6 @@
* * ``` *- * * For phone side configuration activities, substitute the category * `com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION` for * `com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION`.@@ -81,9 +77,9 @@
* given [Intent]. [ComponentName] is being used to identify the APK and the class of the * watch face service. *- * @param watchFaceIntent The intent holding config activity launch.- * @return the value of an item previously added with [putWatchFaceComponentExtra], or- * null if no value was found.+ * @param watchFaceIntent The intent holding config activity launch.+ * @return the value of an item previously added with [putWatchFaceComponentExtra], or null+ * if no value was found. */ @Suppress("DEPRECATION") @JvmStatic@@ -111,7 +107,7 @@
* * @param watchFaceIntent The intent holding config activity launch. * @return the value of an item previously added with [putPeerIdExtra], or null if no value- * was found.+ * was found. */ @JvmStatic @Nullable
@@ -52,17 +52,36 @@
package androidx.window.extensions.embedding { public interface ActivityEmbeddingComponent {+ method public default void clearActivityStackAttributesCalculator();+ method public default void clearEmbeddedActivityWindowInfoCallback(); method public void clearSplitAttributesCalculator(); method public void clearSplitInfoCallback();- method public default void finishActivityStacks(java.util.Set);+ method @Deprecated public default void finishActivityStacks(java.util.Set);+ method public default void finishActivityStacksWithTokens(java.util.Set);+ method public default androidx.window.extensions.embedding.ActivityStack.Token? getActivityStackToken(String);+ method public default androidx.window.extensions.embedding.EmbeddedActivityWindowInfo? getEmbeddedActivityWindowInfo(android.app.Activity);+ method public default androidx.window.extensions.embedding.ParentContainerInfo? getParentContainerInfo(androidx.window.extensions.embedding.ActivityStack.Token); method public default void invalidateTopVisibleSplitAttributes(); method public boolean isActivityEmbedded(android.app.Activity);+ method public default boolean pinTopActivityStack(int, androidx.window.extensions.embedding.SplitPinRule);+ method public default void registerActivityStackCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer!>);+ method public default void setActivityStackAttributesCalculator(androidx.window.extensions.core.util.function.Function);+ method public default void setEmbeddedActivityWindowInfoCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer); method public void setEmbeddingRules(java.util.Set);- method public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder);+ method @Deprecated public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder); method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function); method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer!>); method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer!>);- method public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);+ method public default void unpinTopActivityStack(int);+ method public default void unregisterActivityStackCallback(androidx.window.extensions.core.util.function.Consumer!>);+ method public default void updateActivityStackAttributes(androidx.window.extensions.embedding.ActivityStack.Token, androidx.window.extensions.embedding.ActivityStackAttributes);+ method @Deprecated public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);+ method public default void updateSplitAttributes(androidx.window.extensions.embedding.SplitInfo.Token, androidx.window.extensions.embedding.SplitAttributes);+ }++ public class ActivityEmbeddingOptionsProperties {+ field public static final String KEY_ACTIVITY_STACK_TOKEN = "androidx.window.extensions.embedding.ActivityStackToken";+ field public static final String KEY_OVERLAY_TAG = "androidx.window.extensions.embedding.OverlayTag"; } public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {@@ -81,23 +100,103 @@
public class ActivityStack { method public java.util.List getActivities();+ method public androidx.window.extensions.embedding.ActivityStack.Token getActivityStackToken();+ method public String? getTag(); method public boolean isEmpty(); }+ public static final class ActivityStack.Token {+ method public static androidx.window.extensions.embedding.ActivityStack.Token createFromBinder(android.os.IBinder);+ method public static androidx.window.extensions.embedding.ActivityStack.Token readFromBundle(android.os.Bundle);+ method public android.os.Bundle toBundle();+ field public static final androidx.window.extensions.embedding.ActivityStack.Token INVALID_ACTIVITY_STACK_TOKEN;+ }++ public final class ActivityStackAttributes {+ method public android.graphics.Rect getRelativeBounds();+ method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();+ }++ public static final class ActivityStackAttributes.Builder {+ ctor public ActivityStackAttributes.Builder();+ method public androidx.window.extensions.embedding.ActivityStackAttributes build();+ method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setRelativeBounds(android.graphics.Rect);+ method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);+ }++ public class ActivityStackAttributesCalculatorParams {+ method public String getActivityStackTag();+ method public android.os.Bundle getLaunchOptions();+ method public androidx.window.extensions.embedding.ParentContainerInfo getParentContainerInfo();+ }++ public abstract class AnimationBackground {+ method public static androidx.window.extensions.embedding.AnimationBackground.ColorBackground createColorBackground(@ColorInt int);+ field public static final androidx.window.extensions.embedding.AnimationBackground ANIMATION_BACKGROUND_DEFAULT;+ }++ public static class AnimationBackground.ColorBackground extends androidx.window.extensions.embedding.AnimationBackground {+ method @ColorInt public int getColor();+ }++ public final class DividerAttributes {+ method @ColorInt public int getDividerColor();+ method public int getDividerType();+ method public float getPrimaryMaxRatio();+ method public float getPrimaryMinRatio();+ method @Dimension public int getWidthDp();+ method public boolean isDraggingToFullscreenAllowed();+ field public static final int DIVIDER_TYPE_DRAGGABLE = 2; // 0x2+ field public static final int DIVIDER_TYPE_FIXED = 1; // 0x1+ field public static final float RATIO_SYSTEM_DEFAULT = -1.0f;+ field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff+ }++ public static final class DividerAttributes.Builder {+ ctor public DividerAttributes.Builder(androidx.window.extensions.embedding.DividerAttributes);+ ctor public DividerAttributes.Builder(int);+ method public androidx.window.extensions.embedding.DividerAttributes build();+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setDividerColor(@ColorInt int);+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setDraggingToFullscreenAllowed(boolean);+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMaxRatio(float);+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMinRatio(float);+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setWidthDp(@Dimension int);+ }++ public class EmbeddedActivityWindowInfo {+ method public android.app.Activity getActivity();+ method public android.graphics.Rect getActivityStackBounds();+ method public android.graphics.Rect getTaskBounds();+ method public boolean isEmbedded();+ }+ public abstract class EmbeddingRule { method public String? getTag(); }- public class SplitAttributes {+ public class ParentContainerInfo {+ method public android.content.res.Configuration getConfiguration();+ method public androidx.window.extensions.layout.WindowLayoutInfo getWindowLayoutInfo();+ method public android.view.WindowMetrics getWindowMetrics();+ }++ public final class SplitAttributes {+ method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();+ method public androidx.window.extensions.embedding.DividerAttributes? getDividerAttributes(); method public int getLayoutDirection(); method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();+ method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes(); } public static final class SplitAttributes.Builder { ctor public SplitAttributes.Builder();+ ctor public SplitAttributes.Builder(androidx.window.extensions.embedding.SplitAttributes); method public androidx.window.extensions.embedding.SplitAttributes build();+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.extensions.embedding.DividerAttributes?); method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int); method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes); } public static final class SplitAttributes.LayoutDirection {@@ -139,8 +238,13 @@
method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack(); method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack(); method public androidx.window.extensions.embedding.SplitAttributes getSplitAttributes();+ method public androidx.window.extensions.embedding.SplitInfo.Token getSplitInfoToken(); method @Deprecated public float getSplitRatio();- method public android.os.IBinder getToken();+ method @Deprecated public android.os.IBinder getToken();+ }++ public static final class SplitInfo.Token {+ method public static androidx.window.extensions.embedding.SplitInfo.Token createFromBinder(android.os.IBinder); } public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {@@ -166,6 +270,17 @@
method public androidx.window.extensions.embedding.SplitPairRule.Builder setTag(String); }+ public class SplitPinRule extends androidx.window.extensions.embedding.SplitRule {+ method public boolean isSticky();+ }++ public static final class SplitPinRule.Builder {+ ctor public SplitPinRule.Builder(androidx.window.extensions.embedding.SplitAttributes, androidx.window.extensions.core.util.function.Predicate);+ method public androidx.window.extensions.embedding.SplitPinRule build();+ method public androidx.window.extensions.embedding.SplitPinRule.Builder setSticky(boolean);+ method public androidx.window.extensions.embedding.SplitPinRule.Builder setTag(String);+ }+ public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule { method public int getFinishPrimaryWithPlaceholder(); method @Deprecated public int getFinishPrimaryWithSecondary();@@ -198,6 +313,13 @@
field public static final int FINISH_NEVER = 0; // 0x0 }+ public final class WindowAttributes {+ ctor public WindowAttributes(int);+ method public int getDimAreaBehavior();+ field public static final int DIM_AREA_ON_ACTIVITY_STACK = 1; // 0x1+ field public static final int DIM_AREA_ON_TASK = 2; // 0x2+ }+ } package androidx.window.extensions.layout {@@ -206,6 +328,24 @@
method public android.graphics.Rect getBounds(); }+ public final class DisplayFoldFeature {+ method public int getType();+ method public boolean hasProperties(int...);+ method public boolean hasProperty(int);+ field public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1; // 0x1+ field public static final int TYPE_HINGE = 1; // 0x1+ field public static final int TYPE_SCREEN_FOLD_IN = 2; // 0x2+ field public static final int TYPE_UNKNOWN = 0; // 0x0+ }++ public static final class DisplayFoldFeature.Builder {+ ctor public DisplayFoldFeature.Builder(int);+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperties(int...);+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperty(int);+ method public androidx.window.extensions.layout.DisplayFoldFeature build();+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder clearProperties();+ }+ public class FoldingFeature implements androidx.window.extensions.layout.DisplayFeature { ctor public FoldingFeature(android.graphics.Rect, int, int); method public android.graphics.Rect getBounds();@@ -217,9 +357,19 @@
field public static final int TYPE_HINGE = 2; // 0x2 }+ public final class SupportedWindowFeatures {+ method public java.util.List getDisplayFoldFeatures();+ }++ public static final class SupportedWindowFeatures.Builder {+ ctor public SupportedWindowFeatures.Builder(java.util.List);+ method public androidx.window.extensions.layout.SupportedWindowFeatures build();+ }+ public interface WindowLayoutComponent { method @Deprecated public void addWindowLayoutInfoListener(android.app.Activity, java.util.function.Consumer); method public default void addWindowLayoutInfoListener(@UiContext android.content.Context, androidx.window.extensions.core.util.function.Consumer);+ method public default androidx.window.extensions.layout.SupportedWindowFeatures getSupportedWindowFeatures(); method public default void removeWindowLayoutInfoListener(androidx.window.extensions.core.util.function.Consumer); method @Deprecated public void removeWindowLayoutInfoListener(java.util.function.Consumer); }
@@ -52,17 +52,36 @@
package androidx.window.extensions.embedding { public interface ActivityEmbeddingComponent {+ method public default void clearActivityStackAttributesCalculator();+ method public default void clearEmbeddedActivityWindowInfoCallback(); method public void clearSplitAttributesCalculator(); method public void clearSplitInfoCallback();- method public default void finishActivityStacks(java.util.Set);+ method @Deprecated public default void finishActivityStacks(java.util.Set);+ method public default void finishActivityStacksWithTokens(java.util.Set);+ method public default androidx.window.extensions.embedding.ActivityStack.Token? getActivityStackToken(String);+ method public default androidx.window.extensions.embedding.EmbeddedActivityWindowInfo? getEmbeddedActivityWindowInfo(android.app.Activity);+ method public default androidx.window.extensions.embedding.ParentContainerInfo? getParentContainerInfo(androidx.window.extensions.embedding.ActivityStack.Token); method public default void invalidateTopVisibleSplitAttributes(); method public boolean isActivityEmbedded(android.app.Activity);+ method public default boolean pinTopActivityStack(int, androidx.window.extensions.embedding.SplitPinRule);+ method public default void registerActivityStackCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer!>);+ method public default void setActivityStackAttributesCalculator(androidx.window.extensions.core.util.function.Function);+ method public default void setEmbeddedActivityWindowInfoCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer); method public void setEmbeddingRules(java.util.Set);- method public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder);+ method @Deprecated public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder); method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function); method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer!>); method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer!>);- method public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);+ method public default void unpinTopActivityStack(int);+ method public default void unregisterActivityStackCallback(androidx.window.extensions.core.util.function.Consumer!>);+ method public default void updateActivityStackAttributes(androidx.window.extensions.embedding.ActivityStack.Token, androidx.window.extensions.embedding.ActivityStackAttributes);+ method @Deprecated public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);+ method public default void updateSplitAttributes(androidx.window.extensions.embedding.SplitInfo.Token, androidx.window.extensions.embedding.SplitAttributes);+ }++ public class ActivityEmbeddingOptionsProperties {+ field public static final String KEY_ACTIVITY_STACK_TOKEN = "androidx.window.extensions.embedding.ActivityStackToken";+ field public static final String KEY_OVERLAY_TAG = "androidx.window.extensions.embedding.OverlayTag"; } public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {@@ -81,23 +100,104 @@
public class ActivityStack { method public java.util.List getActivities();+ method public androidx.window.extensions.embedding.ActivityStack.Token getActivityStackToken();+ method public String? getTag();+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.os.IBinder getToken(); method public boolean isEmpty(); }+ public static final class ActivityStack.Token {+ method public static androidx.window.extensions.embedding.ActivityStack.Token createFromBinder(android.os.IBinder);+ method public static androidx.window.extensions.embedding.ActivityStack.Token readFromBundle(android.os.Bundle);+ method public android.os.Bundle toBundle();+ field public static final androidx.window.extensions.embedding.ActivityStack.Token INVALID_ACTIVITY_STACK_TOKEN;+ }++ public final class ActivityStackAttributes {+ method public android.graphics.Rect getRelativeBounds();+ method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();+ }++ public static final class ActivityStackAttributes.Builder {+ ctor public ActivityStackAttributes.Builder();+ method public androidx.window.extensions.embedding.ActivityStackAttributes build();+ method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setRelativeBounds(android.graphics.Rect);+ method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);+ }++ public class ActivityStackAttributesCalculatorParams {+ method public String getActivityStackTag();+ method public android.os.Bundle getLaunchOptions();+ method public androidx.window.extensions.embedding.ParentContainerInfo getParentContainerInfo();+ }++ public abstract class AnimationBackground {+ method public static androidx.window.extensions.embedding.AnimationBackground.ColorBackground createColorBackground(@ColorInt int);+ field public static final androidx.window.extensions.embedding.AnimationBackground ANIMATION_BACKGROUND_DEFAULT;+ }++ public static class AnimationBackground.ColorBackground extends androidx.window.extensions.embedding.AnimationBackground {+ method @ColorInt public int getColor();+ }++ public final class DividerAttributes {+ method @ColorInt public int getDividerColor();+ method public int getDividerType();+ method public float getPrimaryMaxRatio();+ method public float getPrimaryMinRatio();+ method @Dimension public int getWidthDp();+ method public boolean isDraggingToFullscreenAllowed();+ field public static final int DIVIDER_TYPE_DRAGGABLE = 2; // 0x2+ field public static final int DIVIDER_TYPE_FIXED = 1; // 0x1+ field public static final float RATIO_SYSTEM_DEFAULT = -1.0f;+ field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff+ }++ public static final class DividerAttributes.Builder {+ ctor public DividerAttributes.Builder(androidx.window.extensions.embedding.DividerAttributes);+ ctor public DividerAttributes.Builder(int);+ method public androidx.window.extensions.embedding.DividerAttributes build();+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setDividerColor(@ColorInt int);+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setDraggingToFullscreenAllowed(boolean);+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMaxRatio(float);+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMinRatio(float);+ method public androidx.window.extensions.embedding.DividerAttributes.Builder setWidthDp(@Dimension int);+ }++ public class EmbeddedActivityWindowInfo {+ method public android.app.Activity getActivity();+ method public android.graphics.Rect getActivityStackBounds();+ method public android.graphics.Rect getTaskBounds();+ method public boolean isEmbedded();+ }+ public abstract class EmbeddingRule { method public String? getTag(); }- public class SplitAttributes {+ public class ParentContainerInfo {+ method public android.content.res.Configuration getConfiguration();+ method public androidx.window.extensions.layout.WindowLayoutInfo getWindowLayoutInfo();+ method public android.view.WindowMetrics getWindowMetrics();+ }++ public final class SplitAttributes {+ method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();+ method public androidx.window.extensions.embedding.DividerAttributes? getDividerAttributes(); method public int getLayoutDirection(); method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();+ method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes(); } public static final class SplitAttributes.Builder { ctor public SplitAttributes.Builder();+ ctor public SplitAttributes.Builder(androidx.window.extensions.embedding.SplitAttributes); method public androidx.window.extensions.embedding.SplitAttributes build();+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.extensions.embedding.DividerAttributes?); method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int); method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);+ method public androidx.window.extensions.embedding.SplitAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes); } public static final class SplitAttributes.LayoutDirection {@@ -139,8 +239,13 @@
method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack(); method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack(); method public androidx.window.extensions.embedding.SplitAttributes getSplitAttributes();+ method public androidx.window.extensions.embedding.SplitInfo.Token getSplitInfoToken(); method @Deprecated public float getSplitRatio();- method public android.os.IBinder getToken();+ method @Deprecated public android.os.IBinder getToken();+ }++ public static final class SplitInfo.Token {+ method public static androidx.window.extensions.embedding.SplitInfo.Token createFromBinder(android.os.IBinder); } public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {@@ -166,6 +271,17 @@
method public androidx.window.extensions.embedding.SplitPairRule.Builder setTag(String); }+ public class SplitPinRule extends androidx.window.extensions.embedding.SplitRule {+ method public boolean isSticky();+ }++ public static final class SplitPinRule.Builder {+ ctor public SplitPinRule.Builder(androidx.window.extensions.embedding.SplitAttributes, androidx.window.extensions.core.util.function.Predicate);+ method public androidx.window.extensions.embedding.SplitPinRule build();+ method public androidx.window.extensions.embedding.SplitPinRule.Builder setSticky(boolean);+ method public androidx.window.extensions.embedding.SplitPinRule.Builder setTag(String);+ }+ public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule { method public int getFinishPrimaryWithPlaceholder(); method @Deprecated public int getFinishPrimaryWithSecondary();@@ -198,6 +314,13 @@
field public static final int FINISH_NEVER = 0; // 0x0 }+ public final class WindowAttributes {+ ctor public WindowAttributes(int);+ method public int getDimAreaBehavior();+ field public static final int DIM_AREA_ON_ACTIVITY_STACK = 1; // 0x1+ field public static final int DIM_AREA_ON_TASK = 2; // 0x2+ }+ } package androidx.window.extensions.layout {@@ -206,6 +329,24 @@
method public android.graphics.Rect getBounds(); }+ public final class DisplayFoldFeature {+ method public int getType();+ method public boolean hasProperties(int...);+ method public boolean hasProperty(int);+ field public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1; // 0x1+ field public static final int TYPE_HINGE = 1; // 0x1+ field public static final int TYPE_SCREEN_FOLD_IN = 2; // 0x2+ field public static final int TYPE_UNKNOWN = 0; // 0x0+ }++ public static final class DisplayFoldFeature.Builder {+ ctor public DisplayFoldFeature.Builder(int);+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperties(int...);+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperty(int);+ method public androidx.window.extensions.layout.DisplayFoldFeature build();+ method public androidx.window.extensions.layout.DisplayFoldFeature.Builder clearProperties();+ }+ public class FoldingFeature implements androidx.window.extensions.layout.DisplayFeature { ctor public FoldingFeature(android.graphics.Rect, int, int); method public android.graphics.Rect getBounds();@@ -217,9 +358,19 @@
field public static final int TYPE_HINGE = 2; // 0x2 }+ public final class SupportedWindowFeatures {+ method public java.util.List getDisplayFoldFeatures();+ }++ public static final class SupportedWindowFeatures.Builder {+ ctor public SupportedWindowFeatures.Builder(java.util.List);+ method public androidx.window.extensions.layout.SupportedWindowFeatures build();+ }+ public interface WindowLayoutComponent { method @Deprecated public void addWindowLayoutInfoListener(android.app.Activity, java.util.function.Consumer); method public default void addWindowLayoutInfoListener(@UiContext android.content.Context, androidx.window.extensions.core.util.function.Consumer);+ method public default androidx.window.extensions.layout.SupportedWindowFeatures getSupportedWindowFeatures(); method public default void removeWindowLayoutInfoListener(androidx.window.extensions.core.util.function.Consumer); method @Deprecated public void removeWindowLayoutInfoListener(java.util.function.Consumer); }
@@ -0,0 +1,77 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_ACTIVITY_STACK;+import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;++import static com.google.common.truth.Truth.assertThat;++import android.graphics.Rect;++import org.junit.Test;++/**+ * Verifies {@link ActivityStackAttributes} behavior.+ */+public class ActivityStackAttributesTest {++ @Test+ public void testActivityStackAttributesDefaults() {+ final ActivityStackAttributes defaultAttrs = new ActivityStackAttributes.Builder().build();+ assertThat(defaultAttrs.getRelativeBounds().isEmpty()).isTrue();+ assertThat(defaultAttrs.getWindowAttributes().getDimAreaBehavior())+ .isEqualTo(DIM_AREA_ON_ACTIVITY_STACK);+ }++ @Test+ public void testActivityStackAttributesEqualsMatchHashCode() {+ final ActivityStackAttributes attrs1 = new ActivityStackAttributes.Builder()+ .setRelativeBounds(new Rect(0, 0, 10, 10))+ .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK))+ .build();++ final ActivityStackAttributes attrs2 = new ActivityStackAttributes.Builder()+ .setRelativeBounds(new Rect(0, 0, 10, 10))+ .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK))+ .build();++ final ActivityStackAttributes attrs3 = new ActivityStackAttributes.Builder()+ .setRelativeBounds(new Rect(10, 0, 20, 10))+ .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK))+ .build();++ final ActivityStackAttributes attrs4 = new ActivityStackAttributes.Builder()+ .setRelativeBounds(new Rect(10, 0, 20, 10))+ .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK))+ .build();++ final ActivityStackAttributes attrs5 = new ActivityStackAttributes.Builder()+ .setRelativeBounds(new Rect(0, 0, 10, 10))+ .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK))+ .build();++ assertThat(attrs1).isNotEqualTo(attrs2);+ assertThat(attrs1.hashCode()).isNotEqualTo(attrs2.hashCode());+ assertThat(attrs1).isNotEqualTo(attrs3);+ assertThat(attrs1.hashCode()).isNotEqualTo(attrs3.hashCode());+ assertThat(attrs1).isNotEqualTo(attrs4);+ assertThat(attrs1.hashCode()).isNotEqualTo(attrs4.hashCode());+ assertThat(attrs1).isEqualTo(attrs5);+ assertThat(attrs1.hashCode()).isEqualTo(attrs5.hashCode());+ }+}
@@ -0,0 +1,102 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import static org.junit.Assert.assertEquals;+import static org.junit.Assert.assertNotEquals;+import static org.junit.Assert.assertTrue;++import android.app.Activity;+import android.graphics.Rect;++import androidx.test.ext.junit.runners.AndroidJUnit4;+import androidx.test.filters.SmallTest;++import org.junit.Before;+import org.junit.Test;+import org.junit.runner.RunWith;+import org.mockito.Mock;+import org.mockito.MockitoAnnotations;++/** Tests for {@link EmbeddedActivityWindowInfo} class. */+@SmallTest+@RunWith(AndroidJUnit4.class)+public class EmbeddedActivityWindowInfoTest {++ @Mock+ private Activity mActivity;+ @Mock+ private Activity mActivity2;++ @Before+ public void setUp() {+ MockitoAnnotations.initMocks(this);+ }++ @Test+ public void testGetter() {+ final Rect taskBounds = new Rect(0, 0, 1000, 2000);+ final Rect activityStackBounds = new Rect(0, 0, 1000, 1000);+ final EmbeddedActivityWindowInfo info = new EmbeddedActivityWindowInfo(mActivity,+ true /* isEmbedded */, taskBounds, activityStackBounds);++ assertEquals(mActivity, info.getActivity());+ assertTrue(info.isEmbedded());+ assertEquals(taskBounds, info.getTaskBounds());+ assertEquals(activityStackBounds, info.getActivityStackBounds());+ }++ @Test+ public void testEqualsAndHashCode() {+ final EmbeddedActivityWindowInfo info1 = new EmbeddedActivityWindowInfo(mActivity,+ true /* isEmbedded */,+ new Rect(0, 0, 1000, 2000),+ new Rect(0, 0, 1000, 1000));+ final EmbeddedActivityWindowInfo info2 = new EmbeddedActivityWindowInfo(mActivity2,+ true /* isEmbedded */,+ new Rect(0, 0, 1000, 2000),+ new Rect(0, 0, 1000, 1000));+ final EmbeddedActivityWindowInfo info3 = new EmbeddedActivityWindowInfo(mActivity,+ false /* isEmbedded */,+ new Rect(0, 0, 1000, 2000),+ new Rect(0, 0, 1000, 1000));+ final EmbeddedActivityWindowInfo info4 = new EmbeddedActivityWindowInfo(mActivity,+ true /* isEmbedded */,+ new Rect(0, 0, 1000, 1000),+ new Rect(0, 0, 1000, 1000));+ final EmbeddedActivityWindowInfo info5 = new EmbeddedActivityWindowInfo(mActivity,+ true /* isEmbedded */,+ new Rect(0, 0, 1000, 2000),+ new Rect(0, 0, 1000, 1500));+ final EmbeddedActivityWindowInfo info6 = new EmbeddedActivityWindowInfo(mActivity,+ true /* isEmbedded */,+ new Rect(0, 0, 1000, 2000),+ new Rect(0, 0, 1000, 1000));++ assertNotEquals(info1, info2);+ assertNotEquals(info1, info3);+ assertNotEquals(info1, info4);+ assertNotEquals(info1, info5);+ assertEquals(info1, info6);++ assertNotEquals(info1.hashCode(), info2.hashCode());+ assertNotEquals(info1.hashCode(), info3.hashCode());+ assertNotEquals(info1.hashCode(), info4.hashCode());+ assertNotEquals(info1.hashCode(), info5.hashCode());+ assertEquals(info1.hashCode(), info6.hashCode());+ }+}
@@ -22,13 +22,17 @@
import android.view.WindowMetrics; import androidx.annotation.NonNull;+import androidx.annotation.Nullable; import androidx.window.extensions.RequiresVendorApiLevel; import androidx.window.extensions.WindowExtensions; import androidx.window.extensions.core.util.function.Consumer; import androidx.window.extensions.core.util.function.Function;+import androidx.window.extensions.util.SetCompat;+ import java.util.List; import java.util.Set;+import java.util.concurrent.Executor; /** * Extension component definition that is used by the WindowManager library to trigger custom@@ -57,8 +61,7 @@
/** * Sets the callback that notifies WM Jetpack about changes in split states from the Extensions * Sidecar implementation. The listener should be registered for the lifetime of the process.- * There are no threading guarantees where the events are dispatched from. All messages are- * re-posted to the executors provided by developers.+ * There are no threading guarantees where the events are dispatched from. * * @param consumer the callback to notify {@link SplitInfo} list changes */@@ -86,6 +89,50 @@
boolean isActivityEmbedded(@NonNull Activity activity); /**+ * Pins the top-most {@link ActivityStack} to keep the stack of the Activities to be+ * positioned on top. The rest of the activities in the Task will be split with the pinned+ * {@link ActivityStack}. The pinned {@link ActivityStack} would also have isolated activity+ * navigation in which only the activities that are started from the pinned+ * {@link ActivityStack} can be added on top of the {@link ActivityStack}.+ * + * The pinned {@link ActivityStack} is unpinned whenever the parent Task bounds don't+ * satisfy the dimensions and aspect ratio requirements {@link SplitRule#checkParentMetrics}+ * to show two {@link ActivityStack}s. See {@link SplitPinRule.Builder#setSticky} if+ * the same {@link ActivityStack} should be pinned again whenever the parent Task bounds+ * satisfies the dimensions and aspect ratios requirements defined in the rule.+ *+ * @param taskId The id of the Task that top {@link ActivityStack} should be pinned.+ * @param splitPinRule The SplitRule that specifies how the top {@link ActivityStack} should+ * be split with others.+ * @return Returns {@code true} if the top {@link ActivityStack} is successfully pinned.+ * Otherwise, {@code false}. Few examples are:+ * 1. There's no {@link ActivityStack}.+ * 2. There is already an existing pinned {@link ActivityStack}.+ * 3. There's no other {@link ActivityStack} to split with the top+ * {@link ActivityStack}.+ */+ @RequiresVendorApiLevel(level = 5)+ default boolean pinTopActivityStack(int taskId, @NonNull SplitPinRule splitPinRule) {+ throw new UnsupportedOperationException("This method must not be called unless there is a"+ + " corresponding override implementation on the device.");+ }++ /**+ * Unpins the pinned {@link ActivityStack}. The {@link ActivityStack} will still be the+ * top-most {@link ActivityStack} right after unpinned, and the {@link ActivityStack} could+ * be expanded or continue to be split with the next top {@link ActivityStack} if the current+ * state matches any of the existing {@link SplitPairRule}. It is a no-op call if the task+ * does not have a pinned {@link ActivityStack}.+ *+ * @param taskId The id of the Task that top {@link ActivityStack} should be unpinned.+ */+ @RequiresVendorApiLevel(level = 5)+ default void unpinTopActivityStack(int taskId) {+ throw new UnsupportedOperationException("This method must not be called unless there is a"+ + " corresponding override implementation on the device.");+ }++ /** * Sets a function to compute the {@link SplitAttributes} for the {@link SplitRule} and current * window state provided in {@link SplitAttributesCalculatorParams}. * @@ -127,12 +174,10 @@
void clearSplitAttributesCalculator(); /**- * Sets the launching {@link ActivityStack} to the given {@link ActivityOptions}.- *- * @param options The {@link ActivityOptions} to be updated.- * @param token The {@link ActivityStack#getToken()} to represent the {@link ActivityStack}+ * @deprecated Use {@link ActivityEmbeddingOptionsProperties#KEY_ACTIVITY_STACK_TOKEN} instead. */- @RequiresVendorApiLevel(level = 3)+ @Deprecated+ @RequiresVendorApiLevel(level = 3, deprecatedSince = 5) @NonNull default ActivityOptions setLaunchingActivityStack(@NonNull ActivityOptions options, @NonNull IBinder token) {@@ -141,6 +186,16 @@
} /**+ * @deprecated Use {@link #finishActivityStacksWithTokens(Set)} with instead.+ */+ @Deprecated+ @RequiresVendorApiLevel(level = 3, deprecatedSince = 5)+ default void finishActivityStacks(@NonNull Set activityStackTokens) {+ throw new UnsupportedOperationException("This method must not be called unless there is a"+ + " corresponding override implementation on the device.");+ }++ /** * Finishes a set of {@link ActivityStack}s. When an {@link ActivityStack} that was in an active * split is finished, the other {@link ActivityStack} in the same {@link SplitInfo} can be * expanded to fill the parent task container.@@ -148,10 +203,16 @@
* @param activityStackTokens The set of tokens of {@link ActivityStack}-s that is going to be * finished. */- @RequiresVendorApiLevel(level = 3)- default void finishActivityStacks(@NonNull Set activityStackTokens) {- throw new UnsupportedOperationException("This method must not be called unless there is a"- + " corresponding override implementation on the device.");+ @SuppressWarnings("deprecation") // Use finishActivityStacks(Set) as its core implementation.+ @RequiresVendorApiLevel(level = 5)+ default void finishActivityStacksWithTokens(+ @NonNull Set activityStackTokens) {+ final Set binderSet = SetCompat.create();++ for (ActivityStack.Token token : activityStackTokens) {+ binderSet.add(token.getRawToken());+ }+ finishActivityStacks(binderSet); } /**@@ -169,15 +230,215 @@
} /**+ * @deprecated Use {@link #updateSplitAttributes(SplitInfo.Token, SplitAttributes)} instead.+ */+ @Deprecated+ @RequiresVendorApiLevel(level = 3, deprecatedSince = 5)+ default void updateSplitAttributes(@NonNull IBinder splitInfoToken,+ @NonNull SplitAttributes splitAttributes) {+ throw new UnsupportedOperationException("This method must not be called unless there is a"+ + " corresponding override implementation on the device.");+ }++ /** * Updates the {@link SplitAttributes} of a split pair. This is an alternative to using * a split attributes calculator callback, applicable when apps only need to update the * splits in a few cases but rely on the default split attributes otherwise. * @param splitInfoToken The identifier of the split pair to update. * @param splitAttributes The {@link SplitAttributes} to apply to the split pair. */- @RequiresVendorApiLevel(level = 3)- default void updateSplitAttributes(@NonNull IBinder splitInfoToken,+ @SuppressWarnings("deprecation") // Use finishActivityStacks(Set).+ @RequiresVendorApiLevel(level = 5)+ default void updateSplitAttributes(@NonNull SplitInfo.Token splitInfoToken, @NonNull SplitAttributes splitAttributes) {+ updateSplitAttributes(splitInfoToken.getRawToken(), splitAttributes);+ }++ /**+ * Returns the {@link ParentContainerInfo} by the {@link ActivityStack} token, or {@code null}+ * if there's not such {@link ActivityStack} associated with the {@code token}.+ *+ * @param activityStackToken the token of an {@link ActivityStack}.+ */+ @RequiresVendorApiLevel(level = 6)+ @Nullable+ default ParentContainerInfo getParentContainerInfo(+ @NonNull ActivityStack.Token activityStackToken) {+ throw new UnsupportedOperationException("This method must not be called unless there is a"+ + " corresponding override implementation on the device.");+ }++ /**+ * Sets a function to compute the {@link ActivityStackAttributes} for the ActivityStack given+ * for the current window and device state provided in+ * {@link ActivityStackAttributesCalculatorParams} on the main thread.+ * + * This calculator function is only triggered if the {@link ActivityStack#getTag()} is+ * specified. Similar to {@link #setSplitAttributesCalculator(Function)}, the calculator+ * function could be triggered multiple times. It will be triggered whenever there's a+ * launching standalone {@link ActivityStack} with {@link ActivityStack#getTag()} specified,+ * or a parent window or device state update, such as device rotation, folding state change,+ * or the host task goes to multi-window mode.+ *+ * @param calculator The calculator function to calculate {@link ActivityStackAttributes} based+ * on {@link ActivityStackAttributesCalculatorParams}.+ */+ @RequiresVendorApiLevel(level = 6)+ default void setActivityStackAttributesCalculator(@NonNull Function<+ ActivityStackAttributesCalculatorParams, ActivityStackAttributes> calculator) {+ throw new UnsupportedOperationException("This method must not be called unless there is a"+ + " corresponding override implementation on the device.");+ }++ /**+ * Clears the calculator function previously set by+ * {@link #setActivityStackAttributesCalculator(Function)}+ */+ @RequiresVendorApiLevel(level = 6)+ default void clearActivityStackAttributesCalculator() {+ throw new UnsupportedOperationException("This method must not be called unless there is a"+ + " corresponding override implementation on the device.");+ }++ /**+ * Updates {@link ActivityStackAttributes} to an {@link ActivityStack} specified with+ * {@code token} and applies the change directly. If there's no such an {@link ActivityStack},+ * this method is no-op.+ *+ * @param token The {@link ActivityStack} to update.+ * @param activityStackAttributes The attributes to be applied+ */+ @RequiresVendorApiLevel(level = 6)+ default void updateActivityStackAttributes(@NonNull ActivityStack.Token token,+ @NonNull ActivityStackAttributes activityStackAttributes) {+ throw new UnsupportedOperationException("This method must not be called unless there is a"+ + " corresponding override implementation on the device.");+ }++ /**+ * Gets the {@link ActivityStack}'s token by {@code tag}, or {@code null} if there's no+ * {@link ActivityStack} associated with the {@code tag}. For example, the {@link ActivityStack}+ * is dismissed before the is method is called.+ * + * The {@link ActivityStack} token can be obtained immediately after the {@link ActivityStack}+ * is created. This method is usually used when Activity Embedding library wants to+ * {@link #updateActivityStackAttributes} before receiving+ * the {@link ActivityStack} record from the callback set by+ * {@link #registerActivityStackCallback}.+ * + * For example, an app launches an overlay container and calls+ * {@link #updateActivityStackAttributes} immediately right before the overlay+ * {@link ActivityStack} is received from {@link #registerActivityStackCallback}.+ *+ * @param tag A unique identifier of an {@link ActivityStack} if set+ * @return The {@link ActivityStack}'s token that the tag is associated with, or {@code null}+ * if there's no such an {@link ActivityStack}.+ */+ @RequiresVendorApiLevel(level = 6)+ @Nullable+ default ActivityStack.Token getActivityStackToken(@NonNull String tag) {+ throw new UnsupportedOperationException("This method must not be called unless there is a"+ + " corresponding override implementation on the device.");+ }++ /**+ * Registers a callback that notifies WindowManager Jetpack about changes in+ * {@link ActivityStack}.+ * + * In most cases, {@link ActivityStack} are a part of {@link SplitInfo} as+ * {@link SplitInfo#getPrimaryActivityStack() the primary ActivityStack} or+ * {@link SplitInfo#getSecondaryActivityStack() the secondary ActivityStack} of a+ * {@link SplitInfo}.+ * + * However, there are some cases that {@link ActivityStack} is standalone and usually+ * expanded. Cases are:+ *
+ *
A started {@link Activity} matches {@link ActivityRule} with
The {@code ActivityStack} is an overlay {@code ActivityStack}.
+ *+ *
The associated {@link ActivityStack activityStacks} of a {@code ActivityStack} are
+ * dismissed by {@link #finishActivityStacks(Set)}.+ *+ *
One {@link ActivityStack} of {@link SplitInfo}(Either
+ * {@link SplitInfo#getPrimaryActivityStack() the primary ActivityStack} or+ * {@link SplitInfo#getSecondaryActivityStack() the secondary ActivityStack}) is+ * empty and finished, while the other {@link ActivityStack} is not finished with the+ * finishing {@link ActivityStack}.+ * + * An example is a pair of activities matches a {@link SplitPairRule}, and its+ * {@link SplitPairRule#getFinishPrimaryWithSecondary()} is {@link SplitRule#FINISH_NEVER}.+ * Then if the last activity of+ * {@link SplitInfo#getSecondaryActivityStack() the secondary ActivityStack}) is finished,+ * {@link SplitInfo#getPrimaryActivityStack() the primary ActivityStack} will still remain.+ * + *+ * @param executor the executor to dispatch {@link ActivityStack} list changes.+ * @param callback the callback to notify {@link ActivityStack} list changes.+ *+ * @see ActivityEmbeddingComponent#finishActivityStacks(Set)+ */+ @RequiresVendorApiLevel(level = 5)+ default void registerActivityStackCallback(@NonNull Executor executor,+ @NonNull Consumer> callback) {+ throw new UnsupportedOperationException("This method must not be called unless there is a"+ + " corresponding override implementation on the device.");+ }++ /**+ * Removes the callback previously registered in {@link #registerActivityStackCallback}, or+ * no-op if the callback hasn't been registered yet.+ *+ * @param callback The callback to remove, which should have been registered.+ */+ @RequiresVendorApiLevel(level = 5)+ default void unregisterActivityStackCallback(+ @NonNull Consumer> callback) {+ throw new UnsupportedOperationException("This method must not be called unless there is a"+ + " corresponding override implementation on the device.");+ }++ /**+ * Sets a callback that notifies WindowManager Jetpack about changes for a given+ * {@link Activity} to its {@link EmbeddedActivityWindowInfo}.+ * + * The callback will be invoked when the {@link EmbeddedActivityWindowInfo} is changed after+ * the {@link Activity} is launched. Similar to {@link Activity#onConfigurationChanged}, the+ * callback will only be invoked for visible {@link Activity}.+ *+ * @param executor the executor to dispatch {@link EmbeddedActivityWindowInfo} change.+ * @param callback the callback to notify {@link EmbeddedActivityWindowInfo} change.+ */+ @RequiresVendorApiLevel(level = 6)+ default void setEmbeddedActivityWindowInfoCallback(@NonNull Executor executor,+ @NonNull Consumer callback) {+ throw new UnsupportedOperationException("This method must not be called unless there is a"+ + " corresponding override implementation on the device.");+ }++ /**+ * Clears the callback previously set by+ * {@link #setEmbeddedActivityWindowInfoCallback(Executor, Consumer)}+ */+ @RequiresVendorApiLevel(level = 6)+ default void clearEmbeddedActivityWindowInfoCallback() {+ throw new UnsupportedOperationException("This method must not be called unless there is a"+ + " corresponding override implementation on the device.");+ }++ /**+ * Returns the {@link EmbeddedActivityWindowInfo} of the given {@link Activity}, or+ * {@code null} if the {@link Activity} is not attached.+ * + * This API can be used when {@link #setEmbeddedActivityWindowInfoCallback} is not set before+ * the Activity is attached.+ *+ * @param activity the {@link Activity} to get {@link EmbeddedActivityWindowInfo} for.+ */+ @RequiresVendorApiLevel(level = 6)+ @Nullable+ default EmbeddedActivityWindowInfo getEmbeddedActivityWindowInfo(@NonNull Activity activity) { throw new UnsupportedOperationException("This method must not be called unless there is a" + " corresponding override implementation on the device."); }
@@ -0,0 +1,56 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import android.content.Intent;+import android.os.Bundle;+import android.os.IBinder;++/**+ * A class that contains activity embedding properties that puts to or retrieves from+ * {@link android.app.ActivityOptions}.+ */+public class ActivityEmbeddingOptionsProperties {++ private ActivityEmbeddingOptionsProperties() {}++ /**+ * The key of the unique identifier that put into {@link android.app.ActivityOptions}.+ * + * Type: {@link android.os.Bundle#putString(String, String) String}+ * + * An {@code OverlayCreateParams} property that represents the unique identifier of the overlay+ * container.+ */+ public static final String KEY_OVERLAY_TAG =+ "androidx.window.extensions.embedding.OverlayTag";++ /**+ * The key of {@link ActivityStack.Token#toBundle()} that put into+ * {@link android.app.ActivityOptions}.+ * + * Type: {@link Bundle#putBundle}+ * + * Apps can launch an activity into the {@link ActivityStack} that associated with+ * {@code token} by {@link android.app.Activity#startActivity(Intent, Bundle)}.+ *+ * @see androidx.window.extensions.embedding.ActivityStack.Token#toBundle()+ * @see androidx.window.extensions.embedding.ActivityStack.Token#createFromBinder(IBinder)+ */+ public static final String KEY_ACTIVITY_STACK_TOKEN =+ "androidx.window.extensions.embedding.ActivityStackToken";+}
@@ -0,0 +1,143 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_ACTIVITY_STACK;++import android.graphics.Rect;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.window.extensions.RequiresVendorApiLevel;++/**+ * Attributes used to update the layout and configuration of an {@link ActivityStack}.+ */+public final class ActivityStackAttributes {++ @NonNull+ private final Rect mRelativeBounds;++ @NonNull+ private final WindowAttributes mWindowAttributes;++ private ActivityStackAttributes(@NonNull Rect relativeBounds,+ @NonNull WindowAttributes windowAttributes) {+ mRelativeBounds = relativeBounds;+ mWindowAttributes = windowAttributes;+ }++ /**+ * Returns the requested bounds of an {@link ActivityStack} which relative to its parent+ * container.+ * + * {@link Rect#isEmpty() Empty} bounds mean that this {@link ActivityStack} should fill its+ * parent container bounds.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public Rect getRelativeBounds() {+ return mRelativeBounds;+ }++ /**+ * Returns the {@link WindowAttributes} which contains the configurations of the embedded+ * Activity windows with this attributes.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public WindowAttributes getWindowAttributes() {+ return mWindowAttributes;+ }++ @Override+ public int hashCode() {+ return mRelativeBounds.hashCode() * 31 + mWindowAttributes.hashCode();+ }++ @Override+ public boolean equals(@Nullable Object obj) {+ if (this == obj) return true;+ if (!(obj instanceof ActivityStackAttributes)) return false;+ final ActivityStackAttributes attrs = (ActivityStackAttributes) obj;+ return mRelativeBounds.equals(attrs.mRelativeBounds)+ && mWindowAttributes.equals(attrs.mWindowAttributes);+ }++ @NonNull+ @Override+ public String toString() {+ return ActivityStackAttributes.class.getSimpleName() + ": {"+ + " relativeBounds=" + mRelativeBounds+ + ", windowAttributes=" + mWindowAttributes+ + "}";+ }++ /** The builder class of {@link ActivityStackAttributes}. */+ public static final class Builder {++ /** The {@link ActivityStackAttributes} builder constructor. */+ @RequiresVendorApiLevel(level = 6)+ public Builder() {}++ @NonNull+ private final Rect mRelativeBounds = new Rect();++ @NonNull+ private WindowAttributes mWindowAttributes =+ new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK);++ /**+ * Sets the requested relative bounds of the {@link ActivityStack}. If this value is+ * not specified, {@link #getRelativeBounds()} defaults to {@link Rect#isEmpty() empty}+ * bounds, which means to follow the parent container bounds.+ *+ * @param relativeBounds The requested relative bounds.+ * @return This {@code Builder}.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public Builder setRelativeBounds(@NonNull Rect relativeBounds) {+ mRelativeBounds.set(relativeBounds);+ return this;+ }++ /**+ * Sets the window attributes. If this value is not specified, the+ * {@link WindowAttributes#getDimAreaBehavior()} will be only applied on the+ * {@link ActivityStack} of the requested activity.+ *+ * @param attributes The {@link WindowAttributes}+ * @return This {@code Builder}.+ */+ @NonNull+ @RequiresVendorApiLevel(level = 6)+ public Builder setWindowAttributes(@NonNull WindowAttributes attributes) {+ mWindowAttributes = attributes;+ return this;+ }++ /**+ * Builds an {@link ActivityStackAttributes} instance.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public ActivityStackAttributes build() {+ return new ActivityStackAttributes(mRelativeBounds, mWindowAttributes);+ }+ }+}
@@ -0,0 +1,107 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import android.os.Bundle;++import androidx.annotation.NonNull;+import androidx.window.extensions.RequiresVendorApiLevel;+import androidx.window.extensions.core.util.function.Function;++/**+ * The parameter container used in standalone {@link ActivityStack} calculator function to report+ * {@link ParentContainerInfo} and associated {@link ActivityStack#getTag()} to calculate+ * {@link ActivityStackAttributes} when there's a parent container information update or a+ * standalone {@link ActivityStack} is going to be launched.+ *+ * @see ActivityEmbeddingComponent#setActivityStackAttributesCalculator(Function)+ */+public class ActivityStackAttributesCalculatorParams {++ @NonNull+ private final ParentContainerInfo mParentContainerInfo;++ @NonNull+ private final String mActivityStackTag;++ @NonNull+ private final Bundle mLaunchOptions;++ /**+ * {@code ActivityStackAttributesCalculatorParams} constructor.+ *+ * @param parentContainerInfo The {@link ParentContainerInfo} of the standalone+ * {@link ActivityStack} to apply the+ * {@link ActivityStackAttributes}.+ * @param activityStackTag The unique identifier of {@link ActivityStack} to apply the+ * {@link ActivityStackAttributes}.+ * @param launchOptions The options to launch the {@link ActivityStack}.+ */+ ActivityStackAttributesCalculatorParams(@NonNull ParentContainerInfo parentContainerInfo,+ @NonNull String activityStackTag, @NonNull Bundle launchOptions) {+ mParentContainerInfo = parentContainerInfo;+ mActivityStackTag = activityStackTag;+ mLaunchOptions = launchOptions;+ }++ /**+ * Returns {@link ParentContainerInfo} of the standalone {@link ActivityStack} to calculate.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public ParentContainerInfo getParentContainerInfo() {+ return mParentContainerInfo;+ }++ /**+ * Returns unique identifier of the standalone {@link ActivityStack} to calculate.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public String getActivityStackTag() {+ return mActivityStackTag;+ }++ /**+ * Returns options that passed from WM Jetpack to WM Extensions library to launch an+ * {@link ActivityStack}. {@link Bundle#isEmpty() empty} options mean there's no launch options.+ * + * For example, an {@link ActivityStack} launch options could be an+ * {@link android.app.ActivityOptions} bundle that contains information to build an overlay+ * {@link ActivityStack}.+ * + * The launch options will be used for initializing standalone {@link ActivityStack} with+ * {@link #getActivityStackTag()} specified. The logic is owned by WM Jetpack, which is usually+ * from the {@link android.app.ActivityOptions}, WM Extensions library must not touch the+ * options.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public Bundle getLaunchOptions() {+ return mLaunchOptions;+ }++ @NonNull+ @Override+ public String toString() {+ return ActivityStackAttributesCalculatorParams.class.getSimpleName() + ":{"+ + "parentContainerInfo=" + mParentContainerInfo+ + "activityStackTag=" + mActivityStackTag+ + "launchOptions=" + mLaunchOptions+ + "}";+ }+}
@@ -0,0 +1,117 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import android.graphics.Color;++import androidx.annotation.ColorInt;+import androidx.annotation.NonNull;+import androidx.window.extensions.RequiresVendorApiLevel;++import java.util.Objects;++/**+ * A class to represent the background to show while animating embedding activity containers if the+ * animation requires a background.+ *+ * @see SplitAttributes.Builder#setAnimationBackground+ */+public abstract class AnimationBackground {++ /**+ * The special {@link AnimationBackground} object representing the default option.+ * When used, the system will determine the color to use, such as using the current theme+ * window background color.+ */+ @RequiresVendorApiLevel(level = 5)+ @NonNull+ public static final AnimationBackground ANIMATION_BACKGROUND_DEFAULT = new DefaultBackground();++ /**+ * Creates a {@link ColorBackground} that wraps the given color.+ * Only opaque background is supported.+ *+ * @param color the color to be stored.+ * @throws IllegalArgumentException if the color is not opaque.+ */+ @RequiresVendorApiLevel(level = 5)+ @NonNull+ public static ColorBackground createColorBackground(@ColorInt int color) {+ return new ColorBackground(color);+ }++ private AnimationBackground() {}++ /** @see #ANIMATION_BACKGROUND_DEFAULT */+ private static class DefaultBackground extends AnimationBackground {+ @Override+ public String toString() {+ return DefaultBackground.class.getSimpleName();+ }+ }++ /**+ * An {@link AnimationBackground} to specify of using a developer-defined color as the+ * animation background.+ * Only opaque background is supported.+ *+ * @see #createColorBackground(int)+ */+ @RequiresVendorApiLevel(level = 5)+ public static class ColorBackground extends AnimationBackground {++ @ColorInt+ private final int mColor;++ private ColorBackground(@ColorInt int color) {+ final int alpha = Color.alpha(color);+ if (alpha != 255) {+ throw new IllegalArgumentException(+ "Color must be fully opaque, current alpha is " + alpha);+ }+ mColor = color;+ }++ /**+ * Returns the color to use as the animation background.+ */+ @RequiresVendorApiLevel(level = 5)+ @ColorInt+ public int getColor() {+ return mColor;+ }++ @Override+ public boolean equals(Object o) {+ if (this == o) return true;+ if (!(o instanceof ColorBackground)) return false;+ final ColorBackground that = (ColorBackground) o;+ return mColor == that.mColor;+ }++ @Override+ public int hashCode() {+ return Objects.hash(mColor);+ }++ @NonNull+ @Override+ public String toString() {+ return ColorBackground.class.getSimpleName() + " { color: " + mColor + " }";+ }+ }+}
@@ -0,0 +1,420 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import android.graphics.Color;++import androidx.annotation.ColorInt;+import androidx.annotation.Dimension;+import androidx.annotation.IntDef;+import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.window.extensions.RequiresVendorApiLevel;++import java.lang.annotation.Retention;+import java.lang.annotation.RetentionPolicy;+import java.util.Objects;++/**+ * The attributes of the divider layout and behavior.+ *+ * @see SplitAttributes.Builder#setDividerAttributes(DividerAttributes)+ */+public final class DividerAttributes {++ /**+ * A divider type that draws a static line between the primary and secondary containers.+ */+ public static final int DIVIDER_TYPE_FIXED = 1;++ /**+ * A divider type that draws a line between the primary and secondary containers with a drag+ * handle that the user can drag and resize the containers.+ */+ public static final int DIVIDER_TYPE_DRAGGABLE = 2;++ @IntDef({DIVIDER_TYPE_FIXED, DIVIDER_TYPE_DRAGGABLE})+ @Retention(RetentionPolicy.SOURCE)+ @interface DividerType {+ }++ /**+ * A special value to indicate that the ratio is unset. which means the system will choose a+ * default value based on the display size and form factor.+ *+ * @see #getPrimaryMinRatio()+ * @see #getPrimaryMaxRatio()+ */+ public static final float RATIO_SYSTEM_DEFAULT = -1.0f;++ /**+ * A special value to indicate that the width is unset. which means the system will choose a+ * default value based on the display size and form factor.+ *+ * @see #getWidthDp()+ */+ public static final int WIDTH_SYSTEM_DEFAULT = -1;++ /** The {@link DividerType}. */+ private final @DividerType int mDividerType;++ /**+ * The divider width in dp. It defaults to {@link #WIDTH_SYSTEM_DEFAULT}, which means the system+ * will choose a default value based on the display size and form factor.+ */+ private final @Dimension int mWidthDp;++ /**+ * The min split ratio for the primary container. It defaults to {@link #RATIO_SYSTEM_DEFAULT},+ * the system will choose a default value based on the display size and form factor. Will only+ * be used when the divider type is {@link #DIVIDER_TYPE_DRAGGABLE}.+ *+ * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag+ * beyond this ratio, and when dragging is finished, the system will choose to either fully+ * expand the secondary container or move the divider back to this ratio.+ *+ * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to+ * drag beyond this ratio.+ *+ * @see SplitAttributes.SplitType.RatioSplitType#getRatio()+ */+ private final float mPrimaryMinRatio;++ /**+ * The max split ratio for the primary container. It defaults to {@link #RATIO_SYSTEM_DEFAULT},+ * the system will choose a default value based on the display size and form factor. Will only+ * be used when the divider type is {@link #DIVIDER_TYPE_DRAGGABLE}.+ *+ * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag+ * beyond this ratio, and when dragging is finished, the system will choose to either fully+ * expand the primary container or move the divider back to this ratio.+ *+ * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to+ * drag beyond this ratio.+ *+ * @see SplitAttributes.SplitType.RatioSplitType#getRatio()+ */+ private final float mPrimaryMaxRatio;++ /** The color of the divider. */+ private final @ColorInt int mDividerColor;++ /** Whether it is allowed to expand a container to full screen by dragging the divider. */+ private final boolean mIsDraggingToFullscreenAllowed;++ /**+ * Constructor of {@link DividerAttributes}.+ *+ * @param dividerType the divider type. See {@link DividerType}.+ * @param widthDp the width of the divider.+ * @param primaryMinRatio the min split ratio for the primary container.+ * @param primaryMaxRatio the max split ratio for the primary container.+ * @param dividerColor the color of the divider.+ * @param isDraggingToFullscreenAllowed whether it is allowed to expand a container to full+ * screen by dragging the divider.+ * @throws IllegalStateException if the provided values are invalid.+ */+ private DividerAttributes(+ @DividerType int dividerType,+ @Dimension int widthDp,+ float primaryMinRatio,+ float primaryMaxRatio,+ @ColorInt int dividerColor,+ boolean isDraggingToFullscreenAllowed) {+ if (dividerType == DIVIDER_TYPE_FIXED+ && (primaryMinRatio != RATIO_SYSTEM_DEFAULT+ || primaryMaxRatio != RATIO_SYSTEM_DEFAULT)) {+ throw new IllegalStateException(+ "primaryMinRatio and primaryMaxRatio must be RATIO_SYSTEM_DEFAULT for "+ + "DIVIDER_TYPE_FIXED.");+ }+ if (primaryMinRatio != RATIO_SYSTEM_DEFAULT && primaryMaxRatio != RATIO_SYSTEM_DEFAULT+ && primaryMinRatio > primaryMaxRatio) {+ throw new IllegalStateException(+ "primaryMinRatio must be less than or equal to primaryMaxRatio");+ }+ mDividerType = dividerType;+ mWidthDp = widthDp;+ mPrimaryMinRatio = primaryMinRatio;+ mPrimaryMaxRatio = primaryMaxRatio;+ mDividerColor = dividerColor;+ mIsDraggingToFullscreenAllowed = isDraggingToFullscreenAllowed;+ }++ /**+ * Returns the divider type.+ *+ * @see #DIVIDER_TYPE_FIXED+ * @see #DIVIDER_TYPE_DRAGGABLE+ */+ @RequiresVendorApiLevel(level = 6)+ public @DividerType int getDividerType() {+ return mDividerType;+ }++ /**+ * Returns the width of the divider. It defaults to {@link #WIDTH_SYSTEM_DEFAULT}, which means+ * the system will choose a default value based on the display size and form factor.+ */+ @RequiresVendorApiLevel(level = 6)+ public @Dimension int getWidthDp() {+ return mWidthDp;+ }++ /**+ * Returns the min split ratio for the primary container the divider can be dragged to. It+ * defaults to {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value+ * based on the display size and form factor. Will only be used when the divider type is+ * {@link #DIVIDER_TYPE_DRAGGABLE}.+ *+ * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag+ * beyond this ratio, and when dragging is finished, the system will choose to either fully+ * expand the secondary container or move the divider back to this ratio.+ *+ * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to+ * drag beyond this ratio.+ *+ * @see SplitAttributes.SplitType.RatioSplitType#getRatio()+ */+ @RequiresVendorApiLevel(level = 6)+ public float getPrimaryMinRatio() {+ return mPrimaryMinRatio;+ }++ /**+ * Returns the max split ratio for the primary container the divider can be dragged to. It+ * defaults to {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value+ * based on the display size and form factor. Will only be used when the divider type is+ * {@link #DIVIDER_TYPE_DRAGGABLE}.+ *+ * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag+ * beyond this ratio, and when dragging is finished, the system will choose to either fully+ * expand the primary container or move the divider back to this ratio.+ *+ * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to+ * drag beyond this ratio.+ *+ * @see SplitAttributes.SplitType.RatioSplitType#getRatio()+ */+ @RequiresVendorApiLevel(level = 6)+ public float getPrimaryMaxRatio() {+ return mPrimaryMaxRatio;+ }++ /** Returns the color of the divider. */+ @RequiresVendorApiLevel(level = 6)+ public @ColorInt int getDividerColor() {+ return mDividerColor;+ }++ /**+ * Returns whether it is allowed to expand a container to full screen by dragging the+ * divider. Default is {@code true}.+ */+ @RequiresVendorApiLevel(level = 7)+ public boolean isDraggingToFullscreenAllowed() {+ return mIsDraggingToFullscreenAllowed;+ }++ @Override+ public boolean equals(@Nullable Object obj) {+ if (this == obj) return true;+ if (!(obj instanceof DividerAttributes)) return false;+ final DividerAttributes other = (DividerAttributes) obj;+ return mDividerType == other.mDividerType+ && mWidthDp == other.mWidthDp+ && mPrimaryMinRatio == other.mPrimaryMinRatio+ && mPrimaryMaxRatio == other.mPrimaryMaxRatio+ && mDividerColor == other.mDividerColor+ && mIsDraggingToFullscreenAllowed == other.mIsDraggingToFullscreenAllowed;+ }++ @Override+ public int hashCode() {+ return Objects.hash(mDividerType, mWidthDp, mPrimaryMinRatio, mPrimaryMaxRatio,+ mIsDraggingToFullscreenAllowed);+ }++ @NonNull+ @Override+ public String toString() {+ return DividerAttributes.class.getSimpleName() + "{"+ + "dividerType=" + mDividerType+ + ", width=" + mWidthDp+ + ", minPrimaryRatio=" + mPrimaryMinRatio+ + ", maxPrimaryRatio=" + mPrimaryMaxRatio+ + ", dividerColor=" + mDividerColor+ + ", isDraggingToFullscreenAllowed=" + mIsDraggingToFullscreenAllowed+ + "}";+ }++ /** The {@link DividerAttributes} builder. */+ public static final class Builder {++ private final @DividerType int mDividerType;++ private @Dimension int mWidthDp = WIDTH_SYSTEM_DEFAULT;++ private float mPrimaryMinRatio = RATIO_SYSTEM_DEFAULT;++ private float mPrimaryMaxRatio = RATIO_SYSTEM_DEFAULT;++ private @ColorInt int mDividerColor = Color.BLACK;++ private boolean mIsDraggingToFullscreenAllowed = false;++ /**+ * The {@link DividerAttributes} builder constructor.+ *+ * @param dividerType the divider type, possible values are {@link #DIVIDER_TYPE_FIXED} and+ * {@link #DIVIDER_TYPE_DRAGGABLE}.+ */+ @RequiresVendorApiLevel(level = 6)+ public Builder(@DividerType int dividerType) {+ mDividerType = dividerType;+ }++ /**+ * The {@link DividerAttributes} builder constructor initialized by an existing+ * {@link DividerAttributes}.+ *+ * @param original the original {@link DividerAttributes} to initialize the {@link Builder}.+ */+ @RequiresVendorApiLevel(level = 6)+ public Builder(@NonNull DividerAttributes original) {+ Objects.requireNonNull(original);+ mDividerType = original.mDividerType;+ mWidthDp = original.mWidthDp;+ mPrimaryMinRatio = original.mPrimaryMinRatio;+ mPrimaryMaxRatio = original.mPrimaryMaxRatio;+ mDividerColor = original.mDividerColor;+ mIsDraggingToFullscreenAllowed = original.mIsDraggingToFullscreenAllowed;+ }++ /**+ * Sets the divider width. It defaults to {@link #WIDTH_SYSTEM_DEFAULT}, which means the+ * system will choose a default value based on the display size and form factor.+ *+ * @throws IllegalArgumentException if the provided value is invalid.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public Builder setWidthDp(@Dimension int widthDp) {+ if (widthDp != WIDTH_SYSTEM_DEFAULT && widthDp < 0) {+ throw new IllegalArgumentException(+ "widthDp must be greater than or equal to 0 or WIDTH_SYSTEM_DEFAULT.");+ }+ mWidthDp = widthDp;+ return this;+ }++ /**+ * Sets the min split ratio for the primary container. It defaults to+ * {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value based+ * on the display size and form factor. Will only be used when the divider type is+ * {@link #DIVIDER_TYPE_DRAGGABLE}.+ *+ * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag+ * beyond this ratio, and when dragging is finished, the system will choose to either fully+ * expand the secondary container or move the divider back to this ratio.+ *+ * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to+ * drag beyond this ratio.+ *+ * @param primaryMinRatio the min ratio for the primary container. Must be in range+ * [0.0, 1.0) or {@link #RATIO_SYSTEM_DEFAULT}.+ * @throws IllegalArgumentException if the provided value is invalid.+ * @see SplitAttributes.SplitType.RatioSplitType#getRatio()+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public Builder setPrimaryMinRatio(float primaryMinRatio) {+ if (primaryMinRatio != RATIO_SYSTEM_DEFAULT+ && (primaryMinRatio >= 1.0 || primaryMinRatio < 0.0)) {+ throw new IllegalArgumentException(+ "primaryMinRatio must be in [0.0, 1.0) or RATIO_SYSTEM_DEFAULT.");+ }+ mPrimaryMinRatio = primaryMinRatio;+ return this;+ }++ /**+ * Sets the max split ratio for the primary container. It defaults to+ * {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value+ * based on the display size and form factor. Will only be used when the divider type is+ * {@link #DIVIDER_TYPE_DRAGGABLE}.+ *+ * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag+ * beyond this ratio, and when dragging is finished, the system will choose to either fully+ * expand the primary container or move the divider back to this ratio.+ *+ * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to+ * drag beyond this ratio.+ *+ * @param primaryMaxRatio the max ratio for the primary container. Must be in range+ * (0.0, 1.0] or {@link #RATIO_SYSTEM_DEFAULT}.+ * @throws IllegalArgumentException if the provided value is invalid.+ * @see SplitAttributes.SplitType.RatioSplitType#getRatio()+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public Builder setPrimaryMaxRatio(float primaryMaxRatio) {+ if (primaryMaxRatio != RATIO_SYSTEM_DEFAULT+ && (primaryMaxRatio > 1.0 || primaryMaxRatio <= 0.0)) {+ throw new IllegalArgumentException(+ "primaryMaxRatio must be in (0.0, 1.0] or RATIO_SYSTEM_DEFAULT.");+ }+ mPrimaryMaxRatio = primaryMaxRatio;+ return this;+ }++ /**+ * Sets the color of the divider. If not set, the default color {@link Color#BLACK} is+ * used.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public Builder setDividerColor(@ColorInt int dividerColor) {+ mDividerColor = dividerColor;+ return this;+ }++ /**+ * Sets whether it is allowed to expand a container to full screen by dragging the divider.+ * Default is {@code true}.+ */+ @RequiresVendorApiLevel(level = 7)+ @NonNull+ public Builder setDraggingToFullscreenAllowed(boolean isDraggingToFullscreenAllowed) {+ mIsDraggingToFullscreenAllowed = isDraggingToFullscreenAllowed;+ return this;+ }++ /**+ * Builds a {@link DividerAttributes} instance.+ *+ * @return a {@link DividerAttributes} instance.+ * @throws IllegalArgumentException if the provided values are invalid.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public DividerAttributes build() {+ return new DividerAttributes(mDividerType, mWidthDp, mPrimaryMinRatio,+ mPrimaryMaxRatio, mDividerColor, mIsDraggingToFullscreenAllowed);+ }+ }+}
@@ -0,0 +1,119 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import android.app.Activity;+import android.graphics.Rect;++import androidx.annotation.NonNull;+import androidx.window.extensions.RequiresVendorApiLevel;++import java.util.Objects;++/**+ * Describes the embedded window related info of an activity.+ *+ * @see ActivityEmbeddingComponent#setEmbeddedActivityWindowInfoCallback+ * @see ActivityEmbeddingComponent#getEmbeddedActivityWindowInfo+ */+public class EmbeddedActivityWindowInfo {++ @NonNull+ private final Activity mActivity;+ private final boolean mIsEmbedded;+ @NonNull+ private final Rect mTaskBounds;+ @NonNull+ private final Rect mActivityStackBounds;++ EmbeddedActivityWindowInfo(@NonNull Activity activity, boolean isEmbedded,+ @NonNull Rect taskBounds, @NonNull Rect activityStackBounds) {+ mActivity = Objects.requireNonNull(activity);+ mIsEmbedded = isEmbedded;+ mTaskBounds = Objects.requireNonNull(taskBounds);+ mActivityStackBounds = Objects.requireNonNull(activityStackBounds);+ }++ /**+ * Returns the {@link Activity} this {@link EmbeddedActivityWindowInfo} is about.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public Activity getActivity() {+ return mActivity;+ }++ /**+ * Whether this activity is embedded, which means it is in an ActivityStack window that+ * doesn't fill the Task.+ */+ @RequiresVendorApiLevel(level = 6)+ public boolean isEmbedded() {+ return mIsEmbedded;+ }++ /**+ * Returns the bounds of the Task window in display space.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public Rect getTaskBounds() {+ return mTaskBounds;+ }++ /**+ * Returns the bounds of the ActivityStack window in display space.+ * This can be referring to the bounds of the same window as {@link #getTaskBounds()} when+ * the activity is not embedded.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public Rect getActivityStackBounds() {+ return mActivityStackBounds;+ }++ @Override+ public boolean equals(Object o) {+ if (this == o) return true;+ if (!(o instanceof EmbeddedActivityWindowInfo)) return false;+ final EmbeddedActivityWindowInfo that = (EmbeddedActivityWindowInfo) o;+ return mActivity.equals(that.mActivity)+ && mIsEmbedded == that.mIsEmbedded+ && mTaskBounds.equals(that.mTaskBounds)+ && mActivityStackBounds.equals(that.mActivityStackBounds);+ }++ @Override+ public int hashCode() {+ int result = mActivity.hashCode();+ result = result * 31 + (mIsEmbedded ? 1 : 0);+ result = result * 31 + mTaskBounds.hashCode();+ result = result * 31 + mActivityStackBounds.hashCode();+ return result;+ }++ @NonNull+ @Override+ public String toString() {+ return "EmbeddedActivityWindowInfo{"+ + "activity=" + mActivity+ + ", isEmbedded=" + mIsEmbedded+ + ", taskBounds=" + mTaskBounds+ + ", activityStackBounds=" + mActivityStackBounds+ + "}";+ }+}
@@ -0,0 +1,106 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import android.content.res.Configuration;+import android.view.WindowMetrics;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.window.extensions.RequiresVendorApiLevel;+import androidx.window.extensions.layout.WindowLayoutInfo;++/**+ * The parent container information of an {@link ActivityStack}.+ * The data class is designed to provide information to calculate the presentation of+ * an {@link ActivityStack}.+ */+@RequiresVendorApiLevel(level = 6)+public class ParentContainerInfo {+ @NonNull+ private final WindowMetrics mWindowMetrics;++ @NonNull+ private final Configuration mConfiguration;++ @NonNull+ private final WindowLayoutInfo mWindowLayoutInfo;++ /**+ * {@link ParentContainerInfo} constructor, which is used in Window Manager Extensions to+ * provide information of a parent window container.+ *+ * @param windowMetrics The parent container's {@link WindowMetrics}+ * @param configuration The parent container's {@link Configuration}+ * @param windowLayoutInfo The parent container's {@link WindowLayoutInfo}+ */+ ParentContainerInfo(@NonNull WindowMetrics windowMetrics, @NonNull Configuration configuration,+ @NonNull WindowLayoutInfo windowLayoutInfo) {+ mWindowMetrics = windowMetrics;+ mConfiguration = configuration;+ mWindowLayoutInfo = windowLayoutInfo;+ }++ /** Returns the parent container's {@link WindowMetrics}. */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public WindowMetrics getWindowMetrics() {+ return mWindowMetrics;+ }++ /** Returns the parent container's {@link Configuration}. */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public Configuration getConfiguration() {+ return mConfiguration;+ }++ /** Returns the parent container's {@link WindowLayoutInfo}. */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public WindowLayoutInfo getWindowLayoutInfo() {+ return mWindowLayoutInfo;+ }++ @Override+ public int hashCode() {+ int result = mWindowMetrics.hashCode();+ result = 31 * result + mConfiguration.hashCode();+ result = 31 * result + mWindowLayoutInfo.hashCode();+ return result;+ }++ @Override+ public boolean equals(@Nullable Object obj) {+ if (this == obj) return true;+ if (!(obj instanceof ParentContainerInfo)) return false;+ final ParentContainerInfo parentContainerInfo = (ParentContainerInfo) obj;+ return mWindowMetrics.equals(parentContainerInfo.mWindowMetrics)+ && mConfiguration.equals(parentContainerInfo.mConfiguration)+ && mWindowLayoutInfo.equals(parentContainerInfo.mWindowLayoutInfo);+ }++ @NonNull+ @Override+ public String toString() {+ return ParentContainerInfo.class.getSimpleName() + ": {"+ + "windowMetrics=" + WindowMetricsCompat.toString(mWindowMetrics)+ + ", configuration=" + mConfiguration+ + ", windowLayoutInfo=" + mWindowLayoutInfo+ + "}";+ }+}
@@ -16,27 +16,25 @@
package androidx.window.extensions.embedding;-import static androidx.annotation.RestrictTo.Scope.LIBRARY; import static androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.BOTTOM_TO_TOP; import static androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.LEFT_TO_RIGHT; import static androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.LOCALE; import static androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.RIGHT_TO_LEFT; import static androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.TOP_TO_BOTTOM;+import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; import android.annotation.SuppressLint;-import android.graphics.Color;-import androidx.annotation.ColorInt; import androidx.annotation.FloatRange; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable;-import androidx.annotation.RestrictTo; import androidx.window.extensions.RequiresVendorApiLevel; import androidx.window.extensions.core.util.function.Function; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy;+import java.util.Objects; /** * Attributes that describe how the parent window (typically the activity task@@ -50,9 +48,9 @@
* vertically or horizontally and in which direction the primary and * secondary containers are respectively positioned (left to right, * right to left, top to bottom, and so forth)- *
Animation background color -- The color of the background during
- * animation of the split involving this {@code SplitAttributes} object- * if the animation requires a background+ *
Animation background -- The background to show during animation of
+ * the split involving this {@code SplitAttributes} object if the+ * animation requires a background * * *
Attributes can be configured by:
@@ -67,22 +65,10 @@
* * @see SplitAttributes.SplitType * @see SplitAttributes.LayoutDirection+ * @see AnimationBackground */ @RequiresVendorApiLevel(level = 2)-public class SplitAttributes {-- /**- * The default value for animation background color, which means to use the current theme window- * background color.- *- * Only opaque color is supported, so {@code 0} is used as the default. Any other non-opaque- * color will be treated as the default.- *- * @see Builder#setAnimationBackgroundColor(int)- */- @ColorInt- @RestrictTo(LIBRARY)- public static final int DEFAULT_ANIMATION_BACKGROUND_COLOR = 0;+public final class SplitAttributes { /** * The type of window split, which defines the proportion of the parent@@ -132,8 +118,9 @@
} /**- * A window split that's based on the ratio of the size of the primary- * container to the size of the parent window.+ * A window split that's based on the ratio of the size of the primary container to the+ * size of the parent window (excluding area unavailable for the containers such as the+ * divider. See {@link DividerAttributes}). * *
Values in the non-inclusive range (0.0, 1.0) define the size of
* the primary container relative to the size of the parent window:@@ -155,11 +142,12 @@
/** * Creates an instance of this {@code RatioSplitType}. *- * @param ratio The proportion of the parent window occupied by the- * primary container of the split. Can be a value in the- * non-inclusive range (0.0, 1.0). Use- * {@link SplitType.ExpandContainersSplitType} to create a split- * type that occupies the entire parent window.+ * @param ratio The proportion of the parent window occupied by the primary container+ * of the split (excluding area unavailable for the containers such as+ * the divider. See {@link DividerAttributes}). Can be a value in the+ * non-inclusive range (0.0, 1.0). Use+ * {@link SplitType.ExpandContainersSplitType} to create a split+ * type that occupies the entire parent window. */ public RatioSplitType( @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)@@ -173,11 +161,12 @@
} /**- * Gets the proportion of the parent window occupied by the primary- * activity container of the split.+ * Gets the proportion of the parent window occupied by the primary activity+ * container of the split (excluding area unavailable for the containers such as the+ * divider. See {@link DividerAttributes}) . * * @return The proportion of the split occupied by the primary- * container.+ * container. */ @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) public float getRatio() {@@ -193,7 +182,7 @@
* specified. * * @return A {@code RatioSplitType} in which the activity containers- * occupy equal portions of the parent window.+ * occupy equal portions of the parent window. */ @NonNull public static RatioSplitType splitEqually() {@@ -235,9 +224,9 @@
* Creates an instance of this {@code HingeSplitType}. * * @param fallbackSplitType The split type to use if a split based- * on the device hinge or separating fold cannot be determined.- * Can be a {@link RatioSplitType} or- * {@link ExpandContainersSplitType}.+ * on the device hinge or separating fold cannot be determined.+ * Can be a {@link RatioSplitType} or+ * {@link ExpandContainersSplitType}. */ public HingeSplitType(@NonNull SplitType fallbackSplitType) { super("hinge, fallbackType=" + fallbackSplitType);@@ -284,15 +273,15 @@
* * A possible return value of {@link SplitType#getLayoutDirection()}. */- //- // -------------------------- // | | |- // | Primary | Secondary |- // | | |- // -------------------------- //- // Must match {@link LayoutDirection#LTR} for backwards compatibility- // with prior versions of Extensions.+ //+ // -------------------------+ // | | |+ // | Primary | Secondary |+ // | | |+ // -------------------------+ //+ // Must match {@link LayoutDirection#LTR} for backwards compatibility+ // with prior versions of Extensions. public static final int LEFT_TO_RIGHT = 0; /**@@ -304,14 +293,14 @@
* * A possible return value of {@link SplitType#getLayoutDirection()}. */- // -------------------------- // | | |- // | Secondary | Primary |- // | | |- // -------------------------- //- // Must match {@link LayoutDirection#RTL} for backwards compatibility- // with prior versions of Extensions.+ // -------------------------+ // | | |+ // | Secondary | Primary |+ // | | |+ // -------------------------+ //+ // Must match {@link LayoutDirection#RTL} for backwards compatibility+ // with prior versions of Extensions. public static final int RIGHT_TO_LEFT = 1; /**@@ -323,8 +312,8 @@
* * A possible return value of {@link SplitType#getLayoutDirection()}. */- // Must match {@link LayoutDirection#LOCALE} for backwards- // compatibility with prior versions of Extensions.+ // Must match {@link LayoutDirection#LOCALE} for backwards+ // compatibility with prior versions of Extensions. public static final int LOCALE = 3; /**@@ -339,15 +328,15 @@
* * A possible return value of {@link SplitType#getLayoutDirection()}. */- // -------------- // | |- // | Primary |- // | |- // -------------- // | |- // | Secondary |- // | |- // -------------+ // -------------+ // | |+ // | Primary |+ // | |+ // -------------+ // | |+ // | Secondary |+ // | |+ // ------------- public static final int TOP_TO_BOTTOM = 4; /**@@ -362,49 +351,70 @@
* * A possible return value of {@link SplitType#getLayoutDirection()}. */- // -------------- // | |- // | Secondary |- // | |- // -------------- // | |- // | Primary |- // | |- // -------------+ // -------------+ // | |+ // | Secondary |+ // | |+ // -------------+ // | |+ // | Primary |+ // | |+ // ------------- public static final int BOTTOM_TO_TOP = 5;- private LayoutDirection() {}+ private LayoutDirection() {+ } } @IntDef({LEFT_TO_RIGHT, RIGHT_TO_LEFT, LOCALE, TOP_TO_BOTTOM, BOTTOM_TO_TOP}) @Retention(RetentionPolicy.SOURCE)- @interface ExtLayoutDirection {}+ @interface ExtLayoutDirection {+ } @ExtLayoutDirection private final int mLayoutDirection;+ @NonNull private final SplitType mSplitType;- @ColorInt- private final int mAnimationBackgroundColor;+ @NonNull+ private final AnimationBackground mAnimationBackground;++ @NonNull+ private final WindowAttributes mWindowAttributes;++ /** The attributes of a divider. If {@code null}, no divider is requested. */+ @Nullable+ private final DividerAttributes mDividerAttributes; /** * Creates an instance of this {@code SplitAttributes}. *- * @param splitType The type of split. See- * {@link SplitAttributes.SplitType}.- * @param layoutDirection The layout direction of the split, such as left to- * right or top to bottom. See {@link SplitAttributes.LayoutDirection}.- * @param animationBackgroundColor The {@link ColorInt} to use for the- * background color during animation of the split involving this- * {@code SplitAttributes} object if the animation requires a- * background.+ * @param splitType The type of split. See+ * {@link SplitAttributes.SplitType}.+ * @param layoutDirection The layout direction of the split, such as left to+ * right or top to bottom. See+ * {@link SplitAttributes.LayoutDirection}.+ * @param animationBackground The {@link AnimationBackground} to use for the during animation+ * of the split involving this {@code SplitAttributes} object if the+ * animation requires a background.+ * @param attributes The {@link WindowAttributes} of the split, such as dim area+ * behavior.+ * @param dividerAttributes The {@link DividerAttributes}. If {@code null}, no divider is+ * requested. */- SplitAttributes(@NonNull SplitType splitType, @ExtLayoutDirection int layoutDirection,- @ColorInt int animationBackgroundColor) {+ SplitAttributes(+ @NonNull SplitType splitType,+ @ExtLayoutDirection int layoutDirection,+ @NonNull AnimationBackground animationBackground,+ @NonNull WindowAttributes attributes,+ @Nullable DividerAttributes dividerAttributes+ ) { mSplitType = splitType; mLayoutDirection = layoutDirection;- mAnimationBackgroundColor = animationBackgroundColor;+ mAnimationBackground = animationBackground;+ mWindowAttributes = attributes;+ mDividerAttributes = dividerAttributes; } /**@@ -428,18 +438,30 @@
} /**- * Gets the {@link ColorInt} to use for the background color during the+ * Returns the {@link AnimationBackground} to use for the background during the * animation of the split involving this {@code SplitAttributes} object.- *- * The default is {@link #DEFAULT_ANIMATION_BACKGROUND_COLOR}, which means- * to use the current theme window background color.- *- * @return The animation background {@code ColorInt}. */- @ColorInt- @RestrictTo(LIBRARY)- public int getAnimationBackgroundColor() {- return mAnimationBackgroundColor;+ @NonNull+ @RequiresVendorApiLevel(level = 5)+ public AnimationBackground getAnimationBackground() {+ return mAnimationBackground;+ }++ /**+ * Returns the {@link WindowAttributes} which contains the configurations of the embedded+ * Activity windows in this SplitAttributes.+ */+ @NonNull+ @RequiresVendorApiLevel(level = 5)+ public WindowAttributes getWindowAttributes() {+ return mWindowAttributes;+ }++ /** Returns the {@link DividerAttributes}. If {@code null}, no divider is requested. */+ @RequiresVendorApiLevel(level = 6)+ @Nullable+ public DividerAttributes getDividerAttributes() {+ return mDividerAttributes; } /**@@ -447,16 +469,42 @@
* * - The default split type is an equal split between primary and secondary containers. * - The default layout direction is based on locale.- * - The default animation background color is to use the current theme window background color.+ * - The default animation background is to use the current theme window background color. */ public static final class Builder { @NonNull- private SplitType mSplitType = new SplitType.RatioSplitType(0.5f);+ private SplitType mSplitType = new SplitType.RatioSplitType(0.5f); @ExtLayoutDirection private int mLayoutDirection = LOCALE;- @ColorInt- private int mAnimationBackgroundColor = 0;+ @NonNull+ private AnimationBackground mAnimationBackground =+ AnimationBackground.ANIMATION_BACKGROUND_DEFAULT;++ @NonNull+ private WindowAttributes mWindowAttributes =+ new WindowAttributes(DIM_AREA_ON_TASK);++ @Nullable+ private DividerAttributes mDividerAttributes;++ /** Creates a new {@link Builder} to create {@link SplitAttributes}. */+ public Builder() {+ }++ /**+ * Creates a {@link Builder} with values cloned from the original {@link SplitAttributes}.+ *+ * @param original the original {@link SplitAttributes} to initialize the {@link Builder}.+ */+ @RequiresVendorApiLevel(level = 6)+ public Builder(@NonNull SplitAttributes original) {+ mSplitType = original.mSplitType;+ mLayoutDirection = original.mLayoutDirection;+ mAnimationBackground = original.mAnimationBackground;+ mWindowAttributes = original.mWindowAttributes;+ mDividerAttributes = original.mDividerAttributes;+ } /** * Sets the split type attribute.@@ -498,64 +546,76 @@
} /**- * Sets the {@link ColorInt} to use for the background during the+ * Sets the {@link AnimationBackground} to use for the background during the * animation of the split involving this {@code SplitAttributes} object * if the animation requires a background. *- * Only opaque color is supported.+ * The default value is {@link AnimationBackground#ANIMATION_BACKGROUND_DEFAULT}, which+ * means to use the current theme window background color. *- * The default value is {@link #DEFAULT_ANIMATION_BACKGROUND_COLOR}, which- * means to use the current theme window background color. Any non-opaque- * animation color will be treated as- * {@link #DEFAULT_ANIMATION_BACKGROUND_COLOR}.- *- * @param color A packed color int of the form {@code AARRGGBB} for the- * animation background color.+ * @param background An {@link AnimationBackground} to be used for the animation of the+ * split. * @return This {@code Builder}. */ @NonNull- @RestrictTo(LIBRARY)- public Builder setAnimationBackgroundColor(@ColorInt int color) {- // Any non-opaque color will be treated as the default.- mAnimationBackgroundColor = Color.alpha(color) != 255- ? DEFAULT_ANIMATION_BACKGROUND_COLOR- : color;+ @RequiresVendorApiLevel(level = 5)+ public Builder setAnimationBackground(@NonNull AnimationBackground background) {+ mAnimationBackground = background;+ return this;+ }++ /**+ * Sets the window attributes. If this value is not specified, the+ * {@link WindowAttributes#getDimAreaBehavior()} will be only applied on the+ * {@link ActivityStack} of the requested activity.+ *+ * @param attributes The {@link WindowAttributes}+ * @return This {@code Builder}.+ */+ @NonNull+ @RequiresVendorApiLevel(level = 5)+ public Builder setWindowAttributes(@NonNull WindowAttributes attributes) {+ mWindowAttributes = attributes;+ return this;+ }++ /** Sets the {@link DividerAttributes}. If {@code null}, no divider is requested. */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public Builder setDividerAttributes(@Nullable DividerAttributes dividerAttributes) {+ mDividerAttributes = dividerAttributes; return this; } /** * Builds a {@link SplitAttributes} instance with the attributes * specified by {@link #setSplitType}, {@link #setLayoutDirection}, and- * {@link #setAnimationBackgroundColor}.+ * {@link #setAnimationBackground}. * * @return The new {@code SplitAttributes} instance. */ @NonNull public SplitAttributes build() {- return new SplitAttributes(mSplitType, mLayoutDirection, mAnimationBackgroundColor);+ return new SplitAttributes(mSplitType, mLayoutDirection, mAnimationBackground,+ mWindowAttributes, mDividerAttributes); } } @Override+ public boolean equals(Object o) {+ if (this == o) return true;+ if (!(o instanceof SplitAttributes)) return false;+ SplitAttributes that = (SplitAttributes) o;+ return mLayoutDirection == that.mLayoutDirection && mSplitType.equals(that.mSplitType)+ && mAnimationBackground.equals(that.mAnimationBackground)+ && mWindowAttributes.equals(that.mWindowAttributes)+ && Objects.equals(mDividerAttributes, that.mDividerAttributes);+ }++ @Override public int hashCode() {- int result = mSplitType.hashCode();- result = result * 31 + mLayoutDirection;- result = result * 31 + mAnimationBackgroundColor;- return result;- }-- @Override- public boolean equals(Object other) {- if (other == this) {- return true;- }- if (!(other instanceof SplitAttributes)) {- return false;- }- final SplitAttributes otherAttributes = (SplitAttributes) other;- return mLayoutDirection == otherAttributes.mLayoutDirection- && mSplitType.equals(otherAttributes.mSplitType)- && mAnimationBackgroundColor == otherAttributes.mAnimationBackgroundColor;+ return Objects.hash(mLayoutDirection, mSplitType, mAnimationBackground, mWindowAttributes,+ mDividerAttributes); } @NonNull@@ -563,14 +623,16 @@
public String toString() { return SplitAttributes.class.getSimpleName() + "{" + "layoutDir=" + layoutDirectionToString()- + ", ratio=" + mSplitType- + ", animationBgColor=" + Integer.toHexString(mAnimationBackgroundColor)+ + ", splitType=" + mSplitType+ + ", animationBackground=" + mAnimationBackground+ + ", windowAttributes=" + mWindowAttributes+ + ", dividerAttributes=" + mDividerAttributes + "}"; } @NonNull private String layoutDirectionToString() {- switch(mLayoutDirection) {+ switch (mLayoutDirection) { case LEFT_TO_RIGHT: return "LEFT_TO_RIGHT"; case RIGHT_TO_LEFT:
@@ -0,0 +1,145 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import android.view.WindowMetrics;++import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.window.extensions.RequiresVendorApiLevel;+import androidx.window.extensions.core.util.function.Predicate;++import java.util.Objects;++/**+ * Split configuration rules for keeping an {@link ActivityStack} in the split in a pin state to+ * provide an isolated Activity navigation from the split. A pin state here is referring the+ * {@link ActivityStack} to be fixed on top.+ *+ * @see ActivityEmbeddingComponent#pinTopActivityStack+ */+@RequiresVendorApiLevel(level = 5)+public class SplitPinRule extends SplitRule {+ /**+ * Whether the rule should be applied whenever the parent Task satisfied the parent window+ * metrics predicate. See {@link ActivityEmbeddingComponent#pinTopActivityStack}.+ */+ private final boolean mIsSticky;++ SplitPinRule(@NonNull SplitAttributes defaultSplitAttributes,+ @NonNull Predicate parentWindowMetricsPredicate,+ boolean isSticky, @Nullable String tag) {+ super(parentWindowMetricsPredicate, defaultSplitAttributes, tag);+ mIsSticky = isSticky;+ }++ /**+ * Whether the rule is sticky. This configuration rule can only be applied once when+ * possible. That is, the rule will be abandoned whenever the pinned {@link ActivityStack} no+ * longer able to be split with another {@link ActivityStack} once the configuration of the+ * parent Task is changed. Sets the rule to be sticky if the rule should be permanent until+ * the {@link ActivityStack} explicitly unpin.+ *+ * @see ActivityEmbeddingComponent#pinTopActivityStack+ */+ public boolean isSticky() {+ return mIsSticky;+ }++ /**+ * Builder for {@link SplitPinRule}.+ */+ public static final class Builder {+ @NonNull+ private final SplitAttributes mDefaultSplitAttributes;+ @NonNull+ private final Predicate mParentWindowMetricsPredicate;+ private boolean mIsSticky;+ @Nullable+ private String mTag;++ /**+ * The {@link SplitPinRule} builder constructor.+ *+ * @param defaultSplitAttributes the default {@link SplitAttributes} to apply+ * @param parentWindowMetricsPredicate the {@link Predicate} to verify if the pinned+ * {@link ActivityStack} and the one behind are+ * allowed to show adjacent to each other with the+ * given parent {@link WindowMetrics}+ */+ public Builder(@NonNull SplitAttributes defaultSplitAttributes,+ @NonNull Predicate parentWindowMetricsPredicate) {+ mDefaultSplitAttributes = defaultSplitAttributes;+ mParentWindowMetricsPredicate = parentWindowMetricsPredicate;+ }++ /**+ * Sets the rule to be sticky.+ *+ * @see SplitPinRule#isSticky()+ */+ @NonNull+ public Builder setSticky(boolean isSticky) {+ mIsSticky = isSticky;+ return this;+ }++ /**+ * @see SplitPinRule#getTag()+ */+ @NonNull+ public Builder setTag(@NonNull String tag) {+ mTag = Objects.requireNonNull(tag);+ return this;+ }++ /**+ * Builds a new instance of {@link SplitPinRule}.+ */+ @NonNull+ public SplitPinRule build() {+ return new SplitPinRule(mDefaultSplitAttributes, mParentWindowMetricsPredicate,+ mIsSticky, mTag);+ }+ }++ @Override+ public boolean equals(Object o) {+ if (this == o) return true;+ if (!(o instanceof SplitPinRule)) return false;+ SplitPinRule that = (SplitPinRule) o;+ return super.equals(o)+ && mIsSticky == that.mIsSticky;+ }++ @Override+ public int hashCode() {+ int result = super.hashCode();+ result = 31 * result + (mIsSticky ? 1 : 0);+ return result;+ }++ @NonNull+ @Override+ public String toString() {+ return "SplitPinRule{"+ + "mTag=" + getTag()+ + ", mDefaultSplitAttributes=" + getDefaultSplitAttributes()+ + ", mIsSticky=" + mIsSticky+ + '}';+ }+}
@@ -129,7 +129,6 @@
* Determines what happens with the primary container when all activities are finished in the * associated secondary/placeholder container. */- // TODO(b/238905747): Add api guard for extensions. @RequiresVendorApiLevel(level = 2) @SplitPlaceholderFinishBehavior public int getFinishPrimaryWithPlaceholder() {@@ -270,7 +269,6 @@
/** * @see SplitPlaceholderRule#getFinishPrimaryWithPlaceholder() */- // TODO(b/238905747): Add api guard for extensions. @RequiresVendorApiLevel(level = 2) @NonNull public Builder setFinishPrimaryWithPlaceholder(
@@ -0,0 +1,98 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import androidx.annotation.IntDef;+import androidx.annotation.NonNull;+import androidx.annotation.Nullable;+import androidx.window.extensions.RequiresVendorApiLevel;++import java.lang.annotation.Retention;+import java.lang.annotation.RetentionPolicy;+import java.util.Objects;++/**+ * The attributes of the embedded Activity Window.+ */+public final class WindowAttributes {++ /**+ * The dim effect is applying on the {@link ActivityStack} of the Activity window when+ * needed. If the {@link ActivityStack} is not expanded to fill the parent container, the dim+ * effect is applying only on the {@link ActivityStack} of+ * the requested Activity.+ */+ public static final int DIM_AREA_ON_ACTIVITY_STACK = 1;++ /**+ * The dim effect is applying on the area of the whole Task when needed. If the embedded+ * transparent activity is split and displayed side-by-side with another activity, the dim+ * effect is applying on the Task, which across over the two {@link ActivityStack}s.+ */+ public static final int DIM_AREA_ON_TASK = 2;++ @IntDef({+ DIM_AREA_ON_ACTIVITY_STACK,+ DIM_AREA_ON_TASK+ })+ @Retention(RetentionPolicy.SOURCE)+ @interface DimAreaBehavior {+ }++ @DimAreaBehavior+ private final int mDimAreaBehavior;++ /**+ * The {@link WindowAttributes} constructor.+ *+ * @param dimAreaBehavior the type of area that the dim layer is applying.+ */+ @RequiresVendorApiLevel(level = 5)+ public WindowAttributes(@DimAreaBehavior int dimAreaBehavior) {+ mDimAreaBehavior = dimAreaBehavior;+ }++ /**+ * Returns the {@link DimAreaBehavior} to use when dim behind the Activity window is+ * needed.+ * @return The dim area behavior.+ */+ @DimAreaBehavior+ @RequiresVendorApiLevel(level = 5)+ public int getDimAreaBehavior() {+ return mDimAreaBehavior;+ }++ @Override+ public boolean equals(@Nullable Object obj) {+ if (this == obj) return true;+ if (obj == null || !(obj instanceof WindowAttributes)) return false;+ final WindowAttributes other = (WindowAttributes) obj;+ return mDimAreaBehavior == other.getDimAreaBehavior();+ }++ @Override+ public int hashCode() {+ return Objects.hash(mDimAreaBehavior);+ }++ @NonNull+ @Override+ public String toString() {+ return "dimAreaBehavior=" + mDimAreaBehavior;+ }+}
@@ -0,0 +1,53 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import android.os.Build;+import android.view.WindowMetrics;++import androidx.annotation.NonNull;+import androidx.annotation.RequiresApi;++/**+ * A helper class to access {@link WindowMetrics#toString()} with compatibility.+ */+class WindowMetricsCompat {+ private WindowMetricsCompat() {}++ @NonNull+ static String toString(@NonNull WindowMetrics windowMetrics) {+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {+ // WindowMetrics#toString is implemented in U.+ return windowMetrics.toString();+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {+ return Api30Impl.toString(windowMetrics);+ }+ // Should be safe since ActivityEmbedding is not enabled before R.+ throw new UnsupportedOperationException("WindowMetrics didn't exist in R.");+ }++ @RequiresApi(30)+ private static final class Api30Impl {+ @NonNull+ static String toString(@NonNull WindowMetrics windowMetrics) {+ return WindowMetrics.class.getSimpleName() + ":{"+ + "bounds=" + windowMetrics.getBounds()+ + ", windowInsets=" + windowMetrics.getWindowInsets()+ + "}";+ }+ }+}
@@ -0,0 +1,223 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.layout;++import android.annotation.SuppressLint;+import android.content.Context;+import android.os.Build;++import androidx.annotation.IntDef;+import androidx.annotation.NonNull;+import androidx.annotation.RestrictTo;+import androidx.window.extensions.RequiresVendorApiLevel;+import androidx.window.extensions.core.util.function.Consumer;+import androidx.window.extensions.util.SetUtilApi23;++import java.lang.annotation.ElementType;+import java.lang.annotation.Retention;+import java.lang.annotation.RetentionPolicy;+import java.lang.annotation.Target;+import java.util.HashSet;+import java.util.Objects;+import java.util.Set;++/**+ * Represents a fold on a display that may intersect a window. The presence of a fold does not+ * imply that it intersects the window an {@link android.app.Activity} is running in. For+ * example, on a device that can fold like a book and has an outer screen, the fold should be+ * reported regardless of the folding state, or which screen is on to indicate that there may+ * be a fold when the user opens the device.+ *+ * @see WindowLayoutComponent#addWindowLayoutInfoListener(Context, Consumer) to listen to features+ * that affect the window.+ */+public final class DisplayFoldFeature {++ /**+ * The type of fold is unknown. This is here for compatibility reasons if a new type is added,+ * and cannot be reported to an incompatible application.+ */+ public static final int TYPE_UNKNOWN = 0;++ /**+ * The type of fold is a physical hinge separating two display panels.+ */+ public static final int TYPE_HINGE = 1;++ /**+ * The type of fold is a screen that folds from 0-180.+ */+ public static final int TYPE_SCREEN_FOLD_IN = 2;++ @RestrictTo(RestrictTo.Scope.LIBRARY)+ @Retention(RetentionPolicy.SOURCE)+ @IntDef(value = {TYPE_UNKNOWN, TYPE_HINGE, TYPE_SCREEN_FOLD_IN})+ public @interface FoldType {+ }++ /**+ * The fold supports the half opened state.+ */+ public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1;++ @Target(ElementType.TYPE_USE)+ @RestrictTo(RestrictTo.Scope.LIBRARY)+ @Retention(RetentionPolicy.SOURCE)+ @IntDef(value = {FOLD_PROPERTY_SUPPORTS_HALF_OPENED})+ public @interface FoldProperty {+ }++ @FoldType+ private final int mType;++ private final Set<@FoldProperty Integer> mProperties;++ /**+ * Creates an instance of [FoldDisplayFeature].+ *+ * @param type the type of fold, either [FoldDisplayFeature.TYPE_HINGE] or+ * [FoldDisplayFeature.TYPE_FOLDABLE_SCREEN]+ */+ DisplayFoldFeature(@FoldType int type, @NonNull Set<@FoldProperty Integer> properties) {+ mType = type;+ if (Build.VERSION.SDK_INT >= 23) {+ mProperties = SetUtilApi23.createSet();+ } else {+ mProperties = new HashSet<>();+ }+ mProperties.addAll(properties);+ }++ /**+ * Returns the type of fold that is either a hinge or a fold.+ */+ @RequiresVendorApiLevel(level = 6)+ @FoldType+ public int getType() {+ return mType;+ }++ /**+ * Returns {@code true} if the fold has the given property, {@code false} otherwise.+ */+ @RequiresVendorApiLevel(level = 6)+ public boolean hasProperty(@FoldProperty int property) {+ return mProperties.contains(property);+ }+ /**+ * Returns {@code true} if the fold has all the given properties, {@code false} otherwise.+ */+ @RequiresVendorApiLevel(level = 6)+ public boolean hasProperties(@NonNull @FoldProperty int... properties) {+ for (int i = 0; i < properties.length; i++) {+ if (!mProperties.contains(properties[i])) {+ return false;+ }+ }+ return true;+ }++ @Override+ public boolean equals(Object o) {+ if (this == o) return true;+ if (o == null || getClass() != o.getClass()) return false;+ DisplayFoldFeature that = (DisplayFoldFeature) o;+ return mType == that.mType && Objects.equals(mProperties, that.mProperties);+ }++ @Override+ public int hashCode() {+ return Objects.hash(mType, mProperties);+ }++ @Override+ @NonNull+ public String toString() {+ return "ScreenFoldDisplayFeature{mType=" + mType + ", mProperties=" + mProperties + '}';+ }++ /**+ * A builder to construct an instance of {@link DisplayFoldFeature}.+ */+ public static final class Builder {++ @FoldType+ private int mType;++ private Set<@FoldProperty Integer> mProperties;++ /**+ * Constructs a builder to create an instance of {@link DisplayFoldFeature}.+ *+ * @param type the type of hinge for the {@link DisplayFoldFeature}.+ * @see DisplayFoldFeature.FoldType+ */+ @RequiresVendorApiLevel(level = 6)+ public Builder(@FoldType int type) {+ mType = type;+ if (Build.VERSION.SDK_INT >= 23) {+ mProperties = SetUtilApi23.createSet();+ } else {+ mProperties = new HashSet<>();+ }+ }++ /**+ * Add a property to the set of properties exposed by {@link DisplayFoldFeature}.+ */+ @SuppressLint("MissingGetterMatchingBuilder")+ @NonNull+ @RequiresVendorApiLevel(level = 6)+ public Builder addProperty(@FoldProperty int property) {+ mProperties.add(property);+ return this;+ }++ /**+ * Add a list of properties to the set of properties exposed by+ * {@link DisplayFoldFeature}.+ */+ @SuppressLint("MissingGetterMatchingBuilder")+ @NonNull+ @RequiresVendorApiLevel(level = 6)+ public Builder addProperties(@NonNull @FoldProperty int... properties) {+ for (int i = 0; i < properties.length; i++) {+ mProperties.add(properties[i]);+ }+ return this;+ }++ /**+ * Clear the properties in the builder.+ */+ @NonNull+ @RequiresVendorApiLevel(level = 6)+ public Builder clearProperties() {+ mProperties.clear();+ return this;+ }++ /**+ * Returns an instance of {@link DisplayFoldFeature}.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ public DisplayFoldFeature build() {+ return new DisplayFoldFeature(mType, mProperties);+ }+ }+}
@@ -0,0 +1,72 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.layout;++import androidx.annotation.NonNull;+import androidx.window.extensions.RequiresVendorApiLevel;++import java.util.ArrayList;+import java.util.List;++/**+ * A class to represent all the possible features that may interact with or appear in a window,+ * that an application might want to respond to.+ */+public final class SupportedWindowFeatures {++ private final List mDisplayFoldFeatureList;++ private SupportedWindowFeatures(+ @NonNull List displayFoldFeatureList) {+ mDisplayFoldFeatureList = new ArrayList<>(displayFoldFeatureList);+ }++ /**+ * Returns the possible {@link DisplayFoldFeature}s that can be reported to an application.+ */+ @NonNull+ @RequiresVendorApiLevel(level = 6)+ public List getDisplayFoldFeatures() {+ return new ArrayList<>(mDisplayFoldFeatureList);+ }+++ /**+ * A class to create a {@link SupportedWindowFeatures} instance.+ */+ public static final class Builder {++ private final List mDisplayFoldFeatures;++ /**+ * Creates a new instance of {@link Builder}+ */+ @RequiresVendorApiLevel(level = 6)+ public Builder(@NonNull List displayFoldFeatures) {+ mDisplayFoldFeatures = new ArrayList<>(displayFoldFeatures);+ }++ /**+ * Creates an instance of {@link SupportedWindowFeatures} with the features set.+ */+ @NonNull+ @RequiresVendorApiLevel(level = 6)+ public SupportedWindowFeatures build() {+ return new SupportedWindowFeatures(mDisplayFoldFeatures);+ }+ }+}
@@ -96,4 +96,17 @@
throw new UnsupportedOperationException("This method must not be called unless there is a" + " corresponding override implementation on the device."); }++ /**+ * Returns the {@link SupportedWindowFeatures} for the device. This value will not change+ * over time.+ * @see WindowLayoutComponent#addWindowLayoutInfoListener(Context, Consumer) to register a+ * listener for features that impact the window.+ */+ @RequiresVendorApiLevel(level = 6)+ @NonNull+ default SupportedWindowFeatures getSupportedWindowFeatures() {+ throw new UnsupportedOperationException("This method will not be called unless there is a"+ + " corresponding override implementation on the device");+ } }
@@ -0,0 +1,60 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.util;++import android.os.Build;+import android.util.ArraySet;++import androidx.annotation.NonNull;+import androidx.annotation.RequiresApi;+import androidx.annotation.RestrictTo;++import java.util.HashSet;+import java.util.Set;++/**+ * A {@link Set} wrapper for compatibility. It {@link ArraySet} if it's available, and uses+ * other compatible {@link Set} class, otherwise.+ */+@RestrictTo(RestrictTo.Scope.LIBRARY)+public final class SetCompat {++ private SetCompat() {}++ /**+ * Creates a {@link Set}.+ *+ * @param the type of the {@link Set}.+ */+ @NonNull+ public static Set create() {+ if (Build.VERSION.SDK_INT < 23) {+ return new HashSet<>();+ } else {+ return Api23Impl.create();+ }+ }++ @RequiresApi(23)+ private static class Api23Impl {++ @NonNull+ static Set create() {+ return new ArraySet<>();+ }+ }+}
@@ -0,0 +1,43 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.util;++import android.util.ArraySet;++import androidx.annotation.NonNull;+import androidx.annotation.RequiresApi;+import androidx.annotation.RestrictTo;++import java.util.Set;++/**+ * Set utilities for creating working with newer {@link ArraySet} apis.+ */+@RestrictTo(RestrictTo.Scope.LIBRARY)+@RequiresApi(23)+public final class SetUtilApi23 {++ private SetUtilApi23() {}++ /**+ * Creates an instance of {@link ArraySet}.+ */+ @NonNull+ public static Set createSet() {+ return new ArraySet<>();+ }+}
@@ -0,0 +1,58 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import static org.junit.Assert.assertEquals;+import static org.junit.Assert.assertFalse;+import static org.junit.Assert.assertNotEquals;+import static org.junit.Assert.assertThrows;++import android.graphics.Color;++import androidx.test.filters.SmallTest;++import org.junit.Test;+import org.junit.runner.RunWith;+import org.robolectric.RobolectricTestRunner;++/** Test for {@link AnimationBackground} */+@SmallTest+@RunWith(RobolectricTestRunner.class)+public class AnimationBackgroundTest {++ @Test+ public void testDefaultBackground() {+ assertEquals(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT,+ AnimationBackground.ANIMATION_BACKGROUND_DEFAULT);+ assertFalse(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT+ instanceof AnimationBackground.ColorBackground);+ }++ @Test+ public void testCreateColorBackground() {+ final AnimationBackground.ColorBackground background =+ AnimationBackground.createColorBackground(Color.BLUE);++ assertEquals(Color.BLUE, background.getColor());+ assertEquals(background, AnimationBackground.createColorBackground(+ Color.BLUE));+ assertNotEquals(background, AnimationBackground.createColorBackground(+ Color.GREEN));+ assertThrows(IllegalArgumentException.class,+ () -> AnimationBackground.createColorBackground(Color.TRANSPARENT));+ }+}
@@ -0,0 +1,167 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.embedding;++import static androidx.window.extensions.embedding.DividerAttributes.DIVIDER_TYPE_DRAGGABLE;+import static androidx.window.extensions.embedding.DividerAttributes.DIVIDER_TYPE_FIXED;+import static androidx.window.extensions.embedding.DividerAttributes.RATIO_SYSTEM_DEFAULT;+import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT;++import static com.google.common.truth.Truth.assertThat;++import static org.junit.Assert.assertThrows;++import androidx.test.filters.SmallTest;++import org.junit.Test;+import org.junit.runner.RunWith;+import org.robolectric.RobolectricTestRunner;++/** Verifies {@link DividerAttributes} behavior. */+@SmallTest+@RunWith(RobolectricTestRunner.class)+public class DividerAttributesTest {++ @Test+ public void testDividerAttributesDefaults() {+ final DividerAttributes defaultAttrs =+ new DividerAttributes.Builder(DIVIDER_TYPE_FIXED).build();+ assertThat(defaultAttrs.getDividerType()).isEqualTo(DIVIDER_TYPE_FIXED);+ assertThat(defaultAttrs.getWidthDp()).isEqualTo(WIDTH_SYSTEM_DEFAULT);+ assertThat(defaultAttrs.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);+ assertThat(defaultAttrs.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);+ }++ @Test+ public void testDividerAttributesBuilder() {+ final DividerAttributes dividerAttributes1 =+ new DividerAttributes.Builder(DIVIDER_TYPE_FIXED)+ .setWidthDp(20)+ .build();+ assertThat(dividerAttributes1.getDividerType()).isEqualTo(DIVIDER_TYPE_FIXED);+ assertThat(dividerAttributes1.getWidthDp()).isEqualTo(20);+ assertThat(dividerAttributes1.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);+ assertThat(dividerAttributes1.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);++ final DividerAttributes dividerAttributes2 =+ new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)+ .setWidthDp(20)+ .setPrimaryMinRatio(0.2f)+ .setPrimaryMaxRatio(0.8f)+ .build();+ assertThat(dividerAttributes2.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);+ assertThat(dividerAttributes2.getWidthDp()).isEqualTo(20);+ assertThat(dividerAttributes2.getPrimaryMinRatio()).isEqualTo(0.2f);+ assertThat(dividerAttributes2.getPrimaryMaxRatio()).isEqualTo(0.8f);++ final DividerAttributes dividerAttributes3 =+ new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)+ .setWidthDp(20)+ .build();+ assertThat(dividerAttributes3.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);+ assertThat(dividerAttributes3.getWidthDp()).isEqualTo(20);+ assertThat(dividerAttributes3.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);+ assertThat(dividerAttributes3.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);++ final DividerAttributes dividerAttributes4 =+ new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)+ .setWidthDp(20)+ .setPrimaryMinRatio(0.2f)+ .build();+ assertThat(dividerAttributes4.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);+ assertThat(dividerAttributes4.getWidthDp()).isEqualTo(20);+ assertThat(dividerAttributes4.getPrimaryMinRatio()).isEqualTo(0.2f);+ assertThat(dividerAttributes4.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);++ final DividerAttributes dividerAttributes5 =+ new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)+ .setWidthDp(20)+ .setPrimaryMaxRatio(0.2f)+ .build();+ assertThat(dividerAttributes5.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);+ assertThat(dividerAttributes5.getWidthDp()).isEqualTo(20);+ assertThat(dividerAttributes5.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);+ assertThat(dividerAttributes5.getPrimaryMaxRatio()).isEqualTo(0.2f);+ }++ @Test+ public void testDividerAttributesEquals() {+ final DividerAttributes dividerAttributes1 =+ new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)+ .setWidthDp(20)+ .setPrimaryMinRatio(0.2f)+ .setPrimaryMaxRatio(0.8f)+ .build();++ final DividerAttributes dividerAttributes2 =+ new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)+ .setWidthDp(20)+ .setPrimaryMinRatio(0.2f)+ .setPrimaryMaxRatio(0.8f)+ .build();++ final DividerAttributes dividerAttributes3 =+ new DividerAttributes.Builder(DIVIDER_TYPE_FIXED)+ .setWidthDp(20)+ .build();++ assertThat(dividerAttributes1).isEqualTo(dividerAttributes2);+ assertThat(dividerAttributes1).isNotEqualTo(dividerAttributes3);+ }++ @Test+ public void testDividerAttributesValidation() {+ assertThrows(+ "Must not set min max ratio for DIVIDER_TYPE_FIXED",+ IllegalStateException.class,+ () -> new DividerAttributes.Builder(DIVIDER_TYPE_FIXED)+ .setPrimaryMinRatio(0.2f)+ .setPrimaryMaxRatio(0.8f)+ .build()+ );++ assertThrows(+ "Min ratio must be less than or equal to max ratio",+ IllegalStateException.class,+ () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)+ .setPrimaryMinRatio(0.8f)+ .setPrimaryMaxRatio(0.2f)+ .build()+ );++ assertThrows(+ "Min ratio must be in range [0.0, 1.0] or RATIO_UNSET",+ IllegalArgumentException.class,+ () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)+ .setPrimaryMinRatio(2.0f)+ );++ assertThrows(+ "Max ratio must be in range [0.0, 1.0] or RATIO_UNSET",+ IllegalArgumentException.class,+ () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)+ .setPrimaryMaxRatio(2.0f)+ );++ assertThrows(+ "Width must be greater than or equal to zero or WIDTH_UNSET",+ IllegalArgumentException.class,+ () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)+ .setWidthDp(-10)+ );+ }+}
@@ -0,0 +1,67 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.extensions.layout;++import static androidx.window.extensions.layout.DisplayFoldFeature.FOLD_PROPERTY_SUPPORTS_HALF_OPENED;+import static androidx.window.extensions.layout.DisplayFoldFeature.TYPE_HINGE;++import static org.junit.Assert.assertEquals;+import static org.junit.Assert.assertTrue;++import org.junit.Test;++import java.util.HashSet;+import java.util.Set;++public class DisplayFoldFeatureTest {++ @Test+ public void test_builder_matches_constructor() {+ Set<@DisplayFoldFeature.FoldProperty Integer> properties = new HashSet<>();+ properties.add(FOLD_PROPERTY_SUPPORTS_HALF_OPENED);+ DisplayFoldFeature expected = new DisplayFoldFeature(TYPE_HINGE, properties);++ DisplayFoldFeature actual = new DisplayFoldFeature.Builder(TYPE_HINGE)+ .addProperty(FOLD_PROPERTY_SUPPORTS_HALF_OPENED)+ .build();++ assertEquals(expected, actual);+ }++ @Test+ public void test_equals_matches() {+ Set<@DisplayFoldFeature.FoldProperty Integer> properties = new HashSet<>();+ properties.add(FOLD_PROPERTY_SUPPORTS_HALF_OPENED);+ DisplayFoldFeature first = new DisplayFoldFeature(TYPE_HINGE, properties);+ DisplayFoldFeature second = new DisplayFoldFeature(TYPE_HINGE, properties);++ assertEquals(first, second);+ assertEquals(first.hashCode(), second.hashCode());+ }++ @Test+ public void test_getter_matches_values() {+ final int type = TYPE_HINGE;+ DisplayFoldFeature actual = new DisplayFoldFeature.Builder(type)+ .addProperty(FOLD_PROPERTY_SUPPORTS_HALF_OPENED)+ .build();++ assertEquals(type, actual.getType());+ assertTrue(actual.hasProperty(FOLD_PROPERTY_SUPPORTS_HALF_OPENED));+ assertTrue(actual.hasProperties(FOLD_PROPERTY_SUPPORTS_HALF_OPENED));+ }+}
@@ -0,0 +1,92 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.demo2.embedding++import android.content.Intent+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK+import android.os.Bundle+import android.widget.TextView+import androidx.appcompat.app.AppCompatActivity+import androidx.lifecycle.Lifecycle+import androidx.lifecycle.lifecycleScope+import androidx.lifecycle.repeatOnLifecycle+import androidx.window.WindowSdkExtensions+import androidx.window.demo.common.util.PictureInPictureUtil+import androidx.window.demo2.R+import androidx.window.demo2.databinding.ActivityEmbeddedBinding+import androidx.window.embedding.ActivityEmbeddingController+import kotlinx.coroutines.Dispatchers+import kotlinx.coroutines.launch++open class EmbeddedActivityBase : AppCompatActivity() {+ lateinit var viewBinding: ActivityEmbeddedBinding+ private lateinit var activityEmbeddingController: ActivityEmbeddingController+ private lateinit var windowInfoView: TextView++ override fun onCreate(savedInstanceState: Bundle?) {+ super.onCreate(savedInstanceState)+ viewBinding = ActivityEmbeddedBinding.inflate(layoutInflater)+ setContentView(viewBinding.root)+ viewBinding.buttonPip.setOnClickListener {+ PictureInPictureUtil.startPictureInPicture(this, false)+ }+ viewBinding.buttonStartActivity.setOnClickListener {+ startActivity(Intent(this, this.javaClass))+ }+ viewBinding.buttonStartActivityFromApplicationContext.setOnClickListener {+ application.startActivity(+ Intent(this, this.javaClass).setFlags(FLAG_ACTIVITY_NEW_TASK)+ )+ }++ activityEmbeddingController = ActivityEmbeddingController.getInstance(this)+ initializeEmbeddedActivityInfoCallback()+ }++ private fun initializeEmbeddedActivityInfoCallback() {+ val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion+ if (extensionVersion < 6) {+ // EmbeddedActivityWindowInfo is only available on 6+.+ return+ }++ windowInfoView = viewBinding.windowIntoText+ lifecycleScope.launch(Dispatchers.Main) {+ // Collect EmbeddedActivityWindowInfo when STARTED and stop when STOPPED.+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {+ // After register, the flow will be triggered immediately if the activity is+ // embedded.+ // However, if the activity is changed to non-embedded state in background (after+ // STOPPED), the flow will not report the change (because it has been unregistered).+ // Reset before start listening.+ resetWindowInfoView()+ activityEmbeddingController.embeddedActivityWindowInfo(this@EmbeddedActivityBase)+ .collect { info ->+ if (info.isEmbedded) {+ windowInfoView.text = info.toString()+ } else {+ resetWindowInfoView()+ }+ }+ }+ }+ }++ private fun resetWindowInfoView() {+ windowInfoView.text = getString(R.string.embedded_window_info_unavailable)+ }+}
@@ -22,4 +22,6 @@
Untrusted Embedding ActivityActivity allows embedding in untrusted mode via opt-in.+ EmbeddedActivityWindowInfo not available+
\ No newline at end of file
@@ -0,0 +1,170 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.demo.area++import android.content.Context+import android.os.Bundle+import android.view.LayoutInflater+import androidx.appcompat.app.AppCompatActivity+import androidx.lifecycle.Lifecycle+import androidx.lifecycle.lifecycleScope+import androidx.lifecycle.repeatOnLifecycle+import androidx.window.area.WindowAreaCapability+import androidx.window.area.WindowAreaCapability.Operation.Companion.OPERATION_PRESENT_ON_AREA+import androidx.window.area.WindowAreaController+import androidx.window.area.WindowAreaInfo+import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING+import androidx.window.area.WindowAreaPresentationSessionCallback+import androidx.window.area.WindowAreaSessionPresenter+import androidx.window.core.ExperimentalWindowApi+import androidx.window.demo.R+import androidx.window.demo.common.infolog.InfoLogAdapter+import androidx.window.demo.databinding.ActivityRearDisplayPresentationBinding+import java.text.SimpleDateFormat+import java.util.Date+import java.util.Locale+import kotlinx.coroutines.Dispatchers+import kotlinx.coroutines.launch++/**+ * Demo Activity that showcases listening for the status of the [OPERATION_PRESENT_ON_AREA]+ * operation on a [WindowAreaInfo] of type [TYPE_REAR_FACING] as well as enabling/disabling a+ * presentation session on that window area. This Activity implements+ * [WindowAreaPresentationSessionCallback] for simplicity.+ *+ * This Activity overrides configuration changes for simplicity.+ */+@OptIn(ExperimentalWindowApi::class)+class RearDisplayPresentationActivity : AppCompatActivity(),+ WindowAreaPresentationSessionCallback {++ private var activePresentation: WindowAreaSessionPresenter? = null+ private var currentWindowAreaInfo: WindowAreaInfo? = null+ private lateinit var windowAreaController: WindowAreaController+ private val infoLogAdapter = InfoLogAdapter()++ private lateinit var binding: ActivityRearDisplayPresentationBinding++ override fun onCreate(savedInstanceState: Bundle?) {+ super.onCreate(savedInstanceState)++ windowAreaController = WindowAreaController.getOrCreate()++ binding = ActivityRearDisplayPresentationBinding.inflate(layoutInflater)+ setContentView(binding.root)+ binding.rearStatusRecyclerView.adapter = infoLogAdapter++ lifecycleScope.launch(Dispatchers.Main) {+ // The block passed to repeatOnLifecycle is executed when the lifecycle+ // is at least STARTED and is cancelled when the lifecycle is STOPPED.+ // It automatically restarts the block when the lifecycle is STARTED again.+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {+ // Safely collect from windowInfoRepo when the lifecycle is STARTED+ // and stops collection when the lifecycle is STOPPED+ windowAreaController.windowAreaInfos.collect { windowAreaInfos ->+ infoLogAdapter.appendAndNotify(+ getCurrentTimeString(), "number of areas: " + windowAreaInfos.size+ )+ windowAreaInfos.forEach { windowAreaInfo ->+ if (windowAreaInfo.type == TYPE_REAR_FACING) {+ currentWindowAreaInfo = windowAreaInfo+ val presentCapability = windowAreaInfo.getCapability(+ OPERATION_PRESENT_ON_AREA+ )+ infoLogAdapter.append(+ getCurrentTimeString(),+ presentCapability.status.toString() + " : " ++ windowAreaInfo.metrics.toString()+ )+ updateRearDisplayPresentationButton()+ }+ }+ infoLogAdapter.notifyDataSetChanged()+ }+ }+ }++ binding.rearDisplayPresentationButton.setOnClickListener {+ if (activePresentation != null) {+ activePresentation?.close()+ } else {+ currentWindowAreaInfo?.let {+ windowAreaController.presentContentOnWindowArea(+ it.token,+ this@RearDisplayPresentationActivity,+ { obj: Runnable -> obj.run() },+ this@RearDisplayPresentationActivity+ )+ }+ }+ }+ }++ override fun onSessionStarted(session: WindowAreaSessionPresenter) {+ infoLogAdapter.appendAndNotify(getCurrentTimeString(),+ "Presentation session has been started")++ activePresentation = session+ val concurrentContext: Context = session.context+ val contentView = LayoutInflater.from(concurrentContext).inflate(+ R.layout.concurrent_presentation, null)+ session.setContentView(contentView)+ activePresentation = session+ updateRearDisplayPresentationButton()+ }++ override fun onContainerVisibilityChanged(isVisible: Boolean) {+ infoLogAdapter.appendAndNotify(getCurrentTimeString(),+ "Presentation content is visible: $isVisible")+ }++ override fun onSessionEnded(t: Throwable?) {+ infoLogAdapter.appendAndNotify(getCurrentTimeString(),+ "Presentation session has been ended")+ activePresentation = null+ }++ private fun updateRearDisplayPresentationButton() {+ if (activePresentation != null) {+ binding.rearDisplayPresentationButton.isEnabled = true+ binding.rearDisplayPresentationButton.text = "Disable rear display presentation"+ return+ }+ when (currentWindowAreaInfo?.getCapability(OPERATION_PRESENT_ON_AREA)?.status) {+ WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {+ binding.rearDisplayPresentationButton.isEnabled = false+ binding.rearDisplayPresentationButton.text =+ "Rear display presentation mode is not supported on this device"+ }+ WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {+ binding.rearDisplayPresentationButton.isEnabled = false+ binding.rearDisplayPresentationButton.text =+ "Rear display presentation is not currently available"+ }+ WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {+ binding.rearDisplayPresentationButton.isEnabled = true+ binding.rearDisplayPresentationButton.text = "Enable rear display presentation mode"+ }+ }+ }++ private fun getCurrentTimeString(): String {+ val sdf = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())+ val currentDate = sdf.format(Date())+ return currentDate.toString()+ }+}
@@ -0,0 +1,22 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.demo.embedding++import androidx.appcompat.app.AppCompatActivity++/** Dialog style Activity. */+open class DialogActivity : AppCompatActivity()
@@ -0,0 +1,19 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.demo.embedding++class OverlayAssociatedActivityA : OverlayActivityBase()
@@ -0,0 +1,28 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.demo.embedding++import android.graphics.Color+import android.os.Bundle++class OverlayAssociatedActivityB : OverlayActivityBase() {+ override fun onCreate(savedInstanceState: Bundle?) {+ super.onCreate(savedInstanceState)++ viewBinding.rootOverlayActivityLayout.setBackgroundColor(Color.parseColor("#e8f5e9"))+ }+}
@@ -50,7 +50,7 @@
Rear Display ModeDemo of observing to WindowAreaStatus and enabling/disabling RearDisplay modeCurrent SplitAttributes:>- Current Animation Background Color:>+ Current Animation Background:>Test IMEClear LogsClose Test IME@@ -61,7 +61,8 @@
System IME SettingsSwitch default IMEInstall window-demos:demo-second-app to launch activities from a different UID.- Please enable PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED and ensure device supports splits.+ This device does not support Activity Embedding.+ Please enable PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED.Latest ConfigurationApplication DisplayListener#onDisplayChangedActivity DisplayListener#onDisplayChanged@@ -86,4 +87,10 @@
Choose the split typeChoose the layout directionPlaceholder Activity may show (again) because "Use a placeholder for A is checked". Clear the check box to expand Activity A to fill the task by either clicking "FINISH SECONDARY ACTIVITIES".+ Dual Display+ Demo of showing content on the rear and internal displays concurrently+ The device does not support overlay features!+ Overlay bounds:+ Width in ratio: + Height in ratio:
@@ -41,4 +41,14 @@
window:activityName="androidx.window.demo.embedding.SplitImeActivityMain"/>++ ++ + window:finishPrimaryWithSecondary="never"+ window:finishSecondaryWithPrimary="adjacent">+ + window:primaryActivityName="androidx.window.demo.embedding.OverlayActivityA"+ window:secondaryActivityName="androidx.window.demo.embedding.SplitActivityDetail"/>+
\ No newline at end of file
@@ -11,6 +11,18 @@
package androidx.window.java.embedding {+ public final class ActivityEmbeddingControllerCallbackAdapter {+ ctor public ActivityEmbeddingControllerCallbackAdapter(androidx.window.embedding.ActivityEmbeddingController controller);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void addEmbeddedActivityWindowInfoListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer listener);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void removeEmbeddedActivityWindowInfoListener(androidx.core.util.Consumer listener);+ }++ public final class OverlayControllerCallbackAdapter {+ ctor public OverlayControllerCallbackAdapter(androidx.window.embedding.OverlayController controller);+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void addOverlayInfoListener(String overlayTag, java.util.concurrent.Executor executor, androidx.core.util.Consumer consumer);+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void removeOverlayInfoListener(androidx.core.util.Consumer consumer);+ }+ @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public final class SplitControllerCallbackAdapter { ctor public SplitControllerCallbackAdapter(androidx.window.embedding.SplitController controller); method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer> consumer);
@@ -11,6 +11,18 @@
package androidx.window.java.embedding {+ public final class ActivityEmbeddingControllerCallbackAdapter {+ ctor public ActivityEmbeddingControllerCallbackAdapter(androidx.window.embedding.ActivityEmbeddingController controller);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void addEmbeddedActivityWindowInfoListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer listener);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void removeEmbeddedActivityWindowInfoListener(androidx.core.util.Consumer listener);+ }++ public final class OverlayControllerCallbackAdapter {+ ctor public OverlayControllerCallbackAdapter(androidx.window.embedding.OverlayController controller);+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void addOverlayInfoListener(String overlayTag, java.util.concurrent.Executor executor, androidx.core.util.Consumer consumer);+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void removeOverlayInfoListener(androidx.core.util.Consumer consumer);+ }+ @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public final class SplitControllerCallbackAdapter { ctor public SplitControllerCallbackAdapter(androidx.window.embedding.SplitController controller); method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer> consumer);
@@ -0,0 +1,91 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.java.embedding++import android.app.Activity+import androidx.core.util.Consumer+import androidx.window.RequiresWindowSdkExtension+import androidx.window.WindowSdkExtensions+import androidx.window.embedding.ActivityEmbeddingController+import androidx.window.embedding.EmbeddedActivityWindowInfo+import androidx.window.java.core.CallbackToFlowAdapter+import java.util.concurrent.Executor++/**+ * An adapted interface for [ActivityEmbeddingController] that provides callback shaped APIs to+ * report the latest [EmbeddedActivityWindowInfo].+ *+ * It should only be used if [ActivityEmbeddingController.embeddedActivityWindowInfo] is not+ * available. For example, an app is written in Java and cannot use Flow APIs.+ *+ * @constructor creates a callback adapter of+ * [ActivityEmbeddingController.embeddedActivityWindowInfo] flow API.+ * @param controller an [ActivityEmbeddingController] that can be obtained by+ * [ActivityEmbeddingController.getInstance].+ */+class ActivityEmbeddingControllerCallbackAdapter(+ private val controller: ActivityEmbeddingController+) {+ private val callbackToFlowAdapter = CallbackToFlowAdapter()++ /**+ * Registers a listener for updates of [EmbeddedActivityWindowInfo] of [activity].+ *+ * The [listener] will immediately be invoked with the latest value upon registration if the+ * [activity] is currently embedded as [EmbeddedActivityWindowInfo.isEmbedded] is `true`.+ *+ * When the [activity] is embedded, the [listener] will be invoked when+ * [EmbeddedActivityWindowInfo] is changed.+ * When the [activity] is not embedded, the [listener] will not be triggered unless the+ * [activity] is becoming non-embedded from embedded.+ *+ * Note that this API is only supported on the device with+ * [WindowSdkExtensions.extensionVersion] equal to or larger than 6.+ * If [WindowSdkExtensions.extensionVersion] is less than 6, this [listener] will not be+ * invoked.+ *+ * @param activity the [Activity] that is interested in getting the embedded window info.+ * @param executor the [Executor] to dispatch the [EmbeddedActivityWindowInfo] change.+ * @param listener the [Consumer] that will be invoked on the [executor] when there is+ * an update to [EmbeddedActivityWindowInfo].+ */+ @RequiresWindowSdkExtension(6)+ fun addEmbeddedActivityWindowInfoListener(+ activity: Activity,+ executor: Executor,+ listener: Consumer+ ) {+ callbackToFlowAdapter.connect(+ executor,+ listener,+ controller.embeddedActivityWindowInfo(activity)+ )+ }++ /**+ * Unregisters a listener that was previously registered via+ * [addEmbeddedActivityWindowInfoListener].+ *+ * It's no-op if the [listener] has not been registered.+ *+ * @param listener the previously registered [Consumer] to unregister.+ */+ @RequiresWindowSdkExtension(6)+ fun removeEmbeddedActivityWindowInfoListener(listener: Consumer) {+ callbackToFlowAdapter.disconnect(listener)+ }+}
@@ -0,0 +1,78 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.java.embedding++import androidx.core.util.Consumer+import androidx.window.RequiresWindowSdkExtension+import androidx.window.WindowSdkExtensions+import androidx.window.embedding.ActivityStack+import androidx.window.embedding.OverlayController+import androidx.window.embedding.OverlayCreateParams+import androidx.window.embedding.OverlayInfo+import androidx.window.java.core.CallbackToFlowAdapter+import java.util.concurrent.Executor++/**+ * An adapted interface for [OverlayController] that provides callback shaped APIs to report+ * the latest [OverlayInfo].+ *+ * It should only be used if [OverlayController.overlayInfo] is not available. For example, an app+ * is written in Java and cannot use Flow APIs.+ *+ * @constructor creates a callback adapter of [OverlayController.overlayInfo] flow API.+ * @param controller an [OverlayController] that can be obtained by [OverlayController.getInstance].+ */+class OverlayControllerCallbackAdapter(private val controller: OverlayController) {++ private val callbackToFlowAdapter = CallbackToFlowAdapter()++ /**+ * Registers a listener for updates of [OverlayInfo] that [overlayTag] is associated with.+ *+ * If there is no active overlay [ActivityStack], the reported [OverlayInfo.activityStack] and+ * [OverlayInfo.currentOverlayAttributes] will be `null`.+ *+ * Note that launching an overlay [ActivityStack] only supports on the device with+ * [WindowSdkExtensions.extensionVersion] equal to or larger than 5.+ * If [WindowSdkExtensions.extensionVersion] is less than 5, this flow will always+ * report [OverlayInfo] without associated [OverlayInfo.activityStack].+ *+ * @param overlayTag the overlay [ActivityStack]'s tag which is set through+ * [OverlayCreateParams]+ * @param executor the [Executor] to dispatch the [OverlayInfo] change+ * @param consumer the [Consumer] that will be invoked on the [executor] when there is+ * an update to [OverlayInfo].+ */+ @RequiresWindowSdkExtension(5)+ fun addOverlayInfoListener(+ overlayTag: String,+ executor: Executor,+ consumer: Consumer+ ) {+ callbackToFlowAdapter.connect(executor, consumer, controller.overlayInfo(overlayTag))+ }++ /**+ * Unregisters a listener that was previously registered via [addOverlayInfoListener].+ *+ * @param consumer the previously registered [Consumer] to unregister.+ */+ @RequiresWindowSdkExtension(5)+ fun removeOverlayInfoListener(consumer: Consumer) {+ callbackToFlowAdapter.disconnect(consumer)+ }+}
@@ -1,4 +1,14 @@
// Signature format: 4.0+package androidx.window.testing {++ public final class WindowSdkExtensionsRule implements org.junit.rules.TestRule {+ ctor public WindowSdkExtensionsRule();+ method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);+ method public void overrideExtensionVersion(@IntRange(from=0L) int version);+ }++}+ package androidx.window.testing.embedding { public final class ActivityEmbeddingRule implements org.junit.rules.TestRule {
@@ -1,4 +1,14 @@
// Signature format: 4.0+package androidx.window.testing {++ public final class WindowSdkExtensionsRule implements org.junit.rules.TestRule {+ ctor public WindowSdkExtensionsRule();+ method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);+ method public void overrideExtensionVersion(@IntRange(from=0L) int version);+ }++}+ package androidx.window.testing.embedding { public final class ActivityEmbeddingRule implements org.junit.rules.TestRule {
@@ -0,0 +1,37 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.testing++import androidx.annotation.IntRange+import androidx.window.WindowSdkExtensions++/**+ * A fake [WindowSdkExtensions] implementation that can override [extensionVersion],+ * which is intended to be used during unit tests.+ */+internal class FakeWindowSdkExtensions : WindowSdkExtensions() {++ override val extensionVersion: Int+ get() = _extensionVersion++ private var _extensionVersion: Int = 0++ internal fun overrideExtensionVersion(@IntRange(from = 0) version: Int) {+ require(version >= 0) { "The override version must equal to or greater than 0." }+ _extensionVersion = version+ }+}
@@ -0,0 +1,67 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.testing++import androidx.annotation.IntRange+import androidx.window.WindowSdkExtensions+import androidx.window.WindowSdkExtensionsDecorator+import org.junit.rules.TestRule+import org.junit.runner.Description+import org.junit.runners.model.Statement++/**+ * [TestRule] for overriding [WindowSdkExtensions] properties in unit tests.+ *+ * The [TestRule] is designed to only be used in unit tests. Users should use the actual+ * [WindowSdkExtensions] properties for instrumentation tests. Overriding the device's+ * extensions version to a higher version may lead to unexpected test failures or even app+ * crash.+ */+class WindowSdkExtensionsRule : TestRule {++ private val fakeWindowSdkExtensions = FakeWindowSdkExtensions()++ override fun apply(+ @Suppress("InvalidNullabilityOverride") // JUnit missing annotations+ base: Statement,+ @Suppress("InvalidNullabilityOverride") // JUnit missing annotations+ description: Description+ ): Statement {+ return object : Statement() {+ override fun evaluate() {+ WindowSdkExtensions.overrideDecorator(object : WindowSdkExtensionsDecorator {+ override fun decorate(windowSdkExtensions: WindowSdkExtensions):+ WindowSdkExtensions = fakeWindowSdkExtensions+ })+ try {+ base.evaluate()+ } finally {+ WindowSdkExtensions.reset()+ }+ }+ }+ }++ /**+ * Overrides the [WindowSdkExtensions.extensionVersion] for testing.+ *+ * @param version The extension version to override.+ */+ fun overrideExtensionVersion(@IntRange(from = 0) version: Int) {+ fakeWindowSdkExtensions.overrideExtensionVersion(version)+ }+}
@@ -0,0 +1,41 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.testing++import androidx.window.WindowSdkExtensions+import org.junit.Assert.assertEquals+import org.junit.Rule+import org.junit.Test++/** Test class to verify [WindowSdkExtensionsRule] behaviors. */+class WindowSdkExtensionsRuleTest {++ @JvmField+ @Rule+ val rule = WindowSdkExtensionsRule()++ /** Verifies the [WindowSdkExtensionsRule] behavior. */+ @Test+ fun testWindowSdkExtensionsRule() {+ assertEquals("The WindowSdkExtensions.extensionVersion is 0 in unit test", 0,+ WindowSdkExtensions.getInstance().extensionVersion)++ rule.overrideExtensionVersion(3)++ assertEquals(3, WindowSdkExtensions.getInstance().extensionVersion)+ }+}
@@ -52,7 +52,7 @@
public class SplitAttributesCalculatorParamsTestingJavaTest { private static final Rect TEST_BOUNDS = new Rect(0, 0, 2000, 2000); private static final WindowMetrics TEST_METRICS = new WindowMetrics(TEST_BOUNDS,- WindowInsetsCompat.CONSUMED);+ WindowInsetsCompat.CONSUMED, 1f /* density */); private static final SplitAttributes DEFAULT_SPLIT_ATTRIBUTES = new SplitAttributes.Builder().build(); private static final SplitAttributes TABLETOP_HINGE_ATTRIBUTES = new SplitAttributes.Builder()
@@ -16,14 +16,12 @@
package androidx.window.testing.embedding-import androidx.window.core.ExperimentalWindowApi import androidx.window.embedding.SplitAttributes import org.junit.Assert.assertEquals import org.junit.Test import org.mockito.kotlin.mock /** Test class to verify [TestSplitInfo] */-@OptIn(ExperimentalWindowApi::class) class SplitInfoTestingTest { /** Verifies the default value of [TestSplitInfo]. */
@@ -1,72 +0,0 @@
-/*- * Copyright 2023 The Android Open Source Project- *- * Licensed under the Apache License, Version 2.0 (the "License");- * you may not use this file except in compliance with the License.- * You may obtain a copy of the License at- *- * http://www.apache.org/licenses/LICENSE-2.0- *- * Unless required by applicable law or agreed to in writing, software- * distributed under the License is distributed on an "AS IS" BASIS,- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.- * See the License for the specific language governing permissions and- * limitations under the License.- */--package androidx.window.testing.embedding--import android.app.Activity-import android.os.Binder-import android.os.IBinder-import androidx.window.core.ExperimentalWindowApi-import androidx.window.embedding.ActivityStack-import androidx.window.embedding.SplitAttributes-import androidx.window.embedding.SplitInfo--/**- * A convenience method to get a test [SplitInfo] with default values provided. With the default- * values it returns an empty [ActivityStack] for the primary and secondary stacks. The default- * [SplitAttributes] are for splitting equally and matching the locale layout.- *- * Note: This method should be used for testing local logic as opposed to end to end verification.- * End to end verification requires a device that supports Activity Embedding.- *- * @param primaryActivity the [Activity] for the primary container.- * @param secondaryActivity the [Activity] for the secondary container.- * @param splitAttributes the [SplitAttributes].- */-@ExperimentalWindowApi-fun TestSplitInfo(- primaryActivity: Activity,- secondaryActivity: Activity,- splitAttributes: SplitAttributes = SplitAttributes(),- token: IBinder = Binder()-): SplitInfo {- val primaryActivityStack = TestActivityStack(primaryActivity, false)- val secondaryActivityStack = TestActivityStack(secondaryActivity, false)- return SplitInfo(primaryActivityStack, secondaryActivityStack, splitAttributes, token)-}--/**- * A convenience method to get a test [ActivityStack] with default values provided. With the default- * values, there will be a single [Activity] in the stack and it will be considered not empty.- *- * Note: This method should be used for testing local logic as opposed to end to end verification.- * End to end verification requires a device that supports Activity Embedding.- *- * @param testActivity an [Activity] that should be considered in the stack- * @param isEmpty states if the stack is empty or not. In practice an [ActivityStack] with a single- * [Activity] but [isEmpty] set to `false` means there is an [Activity] from outside the process- * in the stack.- */-@ExperimentalWindowApi-fun TestActivityStack(- testActivity: Activity,- isEmpty: Boolean = true,-): ActivityStack {- return ActivityStack(- listOf(testActivity),- isEmpty,- )-}
@@ -13,6 +13,8 @@
field public static final String PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED = "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED"; field public static final String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE"; field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";+ field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE";+ field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE"; } public abstract class WindowSdkExtensions {@@ -107,8 +109,10 @@
@SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSessionPresenter extends androidx.window.area.WindowAreaSession { method public android.content.Context getContext();+ method public android.view.Window? getWindow(); method public void setContentView(android.view.View view); property public abstract android.content.Context context;+ property public abstract android.view.Window? window; } }@@ -123,9 +127,13 @@
package androidx.window.embedding { public final class ActivityEmbeddingController {- method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public kotlinx.coroutines.flow.Flow embeddedActivityWindowInfo(android.app.Activity activity);+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void finishActivityStacks(java.util.Set activityStacks);+ method public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity); method public static androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);+ method @androidx.window.RequiresWindowSdkExtension(version=3) public void invalidateVisibleActivityStacks(); method public boolean isActivityEmbedded(android.app.Activity activity);+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void setEmbeddingConfiguration(androidx.window.embedding.EmbeddingConfiguration embeddingConfiguration); field public static final androidx.window.embedding.ActivityEmbeddingController.Companion Companion; }@@ -133,6 +141,11 @@
method public androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context); }+ public final class ActivityEmbeddingOptions {+ method @androidx.window.RequiresWindowSdkExtension(version=5) public static android.os.Bundle setLaunchingActivityStack(android.os.Bundle, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public static android.os.Bundle setOverlayCreateParams(android.os.Bundle, android.app.Activity activity, androidx.window.embedding.OverlayCreateParams overlayCreateParams);+ }+ public final class ActivityFilter { ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction); method public android.content.ComponentName getComponentName();@@ -163,6 +176,84 @@
property public final boolean isEmpty; }+ public abstract class DividerAttributes {+ method public final int getColor();+ method public final int getWidthDp();+ property public final int color;+ property public final int widthDp;+ field public static final androidx.window.embedding.DividerAttributes.Companion Companion;+ field public static final androidx.window.embedding.DividerAttributes NO_DIVIDER;+ field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff+ }++ public static final class DividerAttributes.Companion {+ }++ public abstract static class DividerAttributes.DragRange {+ field public static final androidx.window.embedding.DividerAttributes.DragRange.Companion Companion;+ field public static final androidx.window.embedding.DividerAttributes.DragRange DRAG_RANGE_SYSTEM_DEFAULT;+ }++ public static final class DividerAttributes.DragRange.Companion {+ }++ public static final class DividerAttributes.DragRange.SplitRatioDragRange extends androidx.window.embedding.DividerAttributes.DragRange {+ ctor public DividerAttributes.DragRange.SplitRatioDragRange(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float minRatio, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float maxRatio);+ method public float getMaxRatio();+ method public float getMinRatio();+ property public final float maxRatio;+ property public final float minRatio;+ }++ public static final class DividerAttributes.DraggableDividerAttributes extends androidx.window.embedding.DividerAttributes {+ method public androidx.window.embedding.DividerAttributes.DragRange getDragRange();+ property public final androidx.window.embedding.DividerAttributes.DragRange dragRange;+ }++ @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.DraggableDividerAttributes.Builder {+ ctor public DividerAttributes.DraggableDividerAttributes.Builder();+ ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.DraggableDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.DraggableDividerAttributes original);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes build();+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setColor(@ColorInt int color);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setDragRange(androidx.window.embedding.DividerAttributes.DragRange dragRange);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);+ }++ public static final class DividerAttributes.FixedDividerAttributes extends androidx.window.embedding.DividerAttributes {+ }++ @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.FixedDividerAttributes.Builder {+ ctor public DividerAttributes.FixedDividerAttributes.Builder();+ ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.FixedDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.FixedDividerAttributes original);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes build();+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setColor(@ColorInt int color);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);+ }++ public final class EmbeddedActivityWindowInfo {+ method public android.graphics.Rect getBoundsInParentHost();+ method public android.graphics.Rect getParentHostBounds();+ method public boolean isEmbedded();+ property public final android.graphics.Rect boundsInParentHost;+ property public final boolean isEmbedded;+ property public final android.graphics.Rect parentHostBounds;+ }++ public abstract class EmbeddingAnimationBackground {+ method public static final androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);+ field public static final androidx.window.embedding.EmbeddingAnimationBackground.Companion Companion;+ field public static final androidx.window.embedding.EmbeddingAnimationBackground DEFAULT;+ }++ public static final class EmbeddingAnimationBackground.ColorBackground extends androidx.window.embedding.EmbeddingAnimationBackground {+ method public int getColor();+ property public final int color;+ }++ public static final class EmbeddingAnimationBackground.Companion {+ method public androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);+ }+ public final class EmbeddingAspectRatio { method public static androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio); field public static final androidx.window.embedding.EmbeddingAspectRatio ALWAYS_ALLOW;@@ -174,11 +265,149 @@
method public androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio); }+ public final class EmbeddingBounds {+ ctor public EmbeddingBounds(androidx.window.embedding.EmbeddingBounds.Alignment alignment, androidx.window.embedding.EmbeddingBounds.Dimension width, androidx.window.embedding.EmbeddingBounds.Dimension height);+ method public androidx.window.embedding.EmbeddingBounds.Alignment getAlignment();+ method public androidx.window.embedding.EmbeddingBounds.Dimension getHeight();+ method public androidx.window.embedding.EmbeddingBounds.Dimension getWidth();+ property public final androidx.window.embedding.EmbeddingBounds.Alignment alignment;+ property public final androidx.window.embedding.EmbeddingBounds.Dimension height;+ property public final androidx.window.embedding.EmbeddingBounds.Dimension width;+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_EXPANDED;+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_BOTTOM;+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_LEFT;+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_RIGHT;+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_TOP;+ field public static final androidx.window.embedding.EmbeddingBounds.Companion Companion;+ }++ public static final class EmbeddingBounds.Alignment {+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_BOTTOM;+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_LEFT;+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_RIGHT;+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_TOP;+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment.Companion Companion;+ }++ public static final class EmbeddingBounds.Alignment.Companion {+ }++ public static final class EmbeddingBounds.Companion {+ }++ public abstract static class EmbeddingBounds.Dimension {+ method public static final androidx.window.embedding.EmbeddingBounds.Dimension pixel(@IntRange(from=1L) @Px int value);+ method public static final androidx.window.embedding.EmbeddingBounds.Dimension ratio(@FloatRange(from=0.0, fromInclusive=false, to=1.0, toInclusive=false) float ratio);+ field public static final androidx.window.embedding.EmbeddingBounds.Dimension.Companion Companion;+ field public static final androidx.window.embedding.EmbeddingBounds.Dimension DIMENSION_EXPANDED;+ field public static final androidx.window.embedding.EmbeddingBounds.Dimension DIMENSION_HINGE;+ }++ public static final class EmbeddingBounds.Dimension.Companion {+ method public androidx.window.embedding.EmbeddingBounds.Dimension pixel(@IntRange(from=1L) @Px int value);+ method public androidx.window.embedding.EmbeddingBounds.Dimension ratio(@FloatRange(from=0.0, fromInclusive=false, to=1.0, toInclusive=false) float ratio);+ }++ public final class EmbeddingConfiguration {+ ctor public EmbeddingConfiguration();+ ctor public EmbeddingConfiguration(optional @androidx.window.RequiresWindowSdkExtension(version=5) androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior);+ method public androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior getDimAreaBehavior();+ property public final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior;+ }++ public static final class EmbeddingConfiguration.Builder {+ ctor public EmbeddingConfiguration.Builder();+ method public androidx.window.embedding.EmbeddingConfiguration build();+ method public androidx.window.embedding.EmbeddingConfiguration.Builder setDimAreaBehavior(androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior area);+ }++ public static final class EmbeddingConfiguration.DimAreaBehavior {+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion Companion;+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_ACTIVITY_STACK;+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_TASK;+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior UNDEFINED;+ }++ public static final class EmbeddingConfiguration.DimAreaBehavior.Companion {+ }+ public abstract class EmbeddingRule { method public final String? getTag(); property public final String? tag; }+ public final class OverlayAttributes {+ ctor public OverlayAttributes();+ ctor public OverlayAttributes(optional androidx.window.embedding.EmbeddingBounds bounds);+ method public androidx.window.embedding.EmbeddingBounds getBounds();+ property public final androidx.window.embedding.EmbeddingBounds bounds;+ }++ public static final class OverlayAttributes.Builder {+ ctor public OverlayAttributes.Builder();+ method public androidx.window.embedding.OverlayAttributes build();+ method public androidx.window.embedding.OverlayAttributes.Builder setBounds(androidx.window.embedding.EmbeddingBounds bounds);+ }++ public final class OverlayAttributesCalculatorParams {+ method public androidx.window.embedding.OverlayAttributes getDefaultOverlayAttributes();+ method public String getOverlayTag();+ method public android.content.res.Configuration getParentConfiguration();+ method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();+ method public androidx.window.layout.WindowMetrics getParentWindowMetrics();+ property public final androidx.window.embedding.OverlayAttributes defaultOverlayAttributes;+ property public final String overlayTag;+ property public final android.content.res.Configuration parentConfiguration;+ property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;+ property public final androidx.window.layout.WindowMetrics parentWindowMetrics;+ }++ public final class OverlayController {+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void clearOverlayAttributesCalculator();+ method public static androidx.window.embedding.OverlayController getInstance(android.content.Context context);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public kotlinx.coroutines.flow.Flow overlayInfo(String overlayTag);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void setOverlayAttributesCalculator(kotlin.jvm.functions.Function1 super androidx.window.embedding.OverlayAttributesCalculatorParams,androidx.window.embedding.OverlayAttributes> calculator);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void updateOverlayAttributes(String overlayTag, androidx.window.embedding.OverlayAttributes overlayAttributes);+ field public static final androidx.window.embedding.OverlayController.Companion Companion;+ }++ public static final class OverlayController.Companion {+ method public androidx.window.embedding.OverlayController getInstance(android.content.Context context);+ }++ public final class OverlayCreateParams {+ ctor public OverlayCreateParams();+ ctor public OverlayCreateParams(optional String tag);+ ctor public OverlayCreateParams(optional String tag, optional androidx.window.embedding.OverlayAttributes overlayAttributes);+ method public static String generateOverlayTag();+ method public androidx.window.embedding.OverlayAttributes getOverlayAttributes();+ method public String getTag();+ property public final androidx.window.embedding.OverlayAttributes overlayAttributes;+ property public final String tag;+ field public static final androidx.window.embedding.OverlayCreateParams.Companion Companion;+ }++ public static final class OverlayCreateParams.Builder {+ ctor public OverlayCreateParams.Builder();+ method public androidx.window.embedding.OverlayCreateParams build();+ method public androidx.window.embedding.OverlayCreateParams.Builder setOverlayAttributes(androidx.window.embedding.OverlayAttributes attrs);+ method public androidx.window.embedding.OverlayCreateParams.Builder setTag(String tag);+ }++ public static final class OverlayCreateParams.Companion {+ method public String generateOverlayTag();+ }++ public final class OverlayInfo {+ method public operator boolean contains(android.app.Activity activity);+ method public androidx.window.embedding.ActivityStack? getActivityStack();+ method public androidx.window.embedding.OverlayAttributes? getCurrentOverlayAttributes();+ method public String getOverlayTag();+ property public final androidx.window.embedding.ActivityStack? activityStack;+ property public final androidx.window.embedding.OverlayAttributes? currentOverlayAttributes;+ property public final String overlayTag;+ }+ public final class RuleController { method public void addRule(androidx.window.embedding.EmbeddingRule rule); method public void clearRules();@@ -196,8 +425,17 @@
} public final class SplitAttributes {+ ctor public SplitAttributes();+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType);+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground);+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.DividerAttributes dividerAttributes);+ method public androidx.window.embedding.EmbeddingAnimationBackground getAnimationBackground();+ method public androidx.window.embedding.DividerAttributes getDividerAttributes(); method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection(); method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();+ property public final androidx.window.embedding.EmbeddingAnimationBackground animationBackground;+ property public final androidx.window.embedding.DividerAttributes dividerAttributes; property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection; property public final androidx.window.embedding.SplitAttributes.SplitType splitType; field public static final androidx.window.embedding.SplitAttributes.Companion Companion;@@ -206,6 +444,8 @@
public static final class SplitAttributes.Builder { ctor public SplitAttributes.Builder(); method public androidx.window.embedding.SplitAttributes build();+ method @androidx.window.RequiresWindowSdkExtension(version=5) public androidx.window.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.embedding.EmbeddingAnimationBackground background);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.embedding.DividerAttributes dividerAttributes); method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection); method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type); }@@ -256,10 +496,11 @@
method @androidx.window.RequiresWindowSdkExtension(version=2) public void clearSplitAttributesCalculator(); method public static androidx.window.embedding.SplitController getInstance(android.content.Context context); method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();- method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();+ method @androidx.window.RequiresWindowSdkExtension(version=5) public boolean pinTopActivityStack(int taskId, androidx.window.embedding.SplitPinRule splitPinRule); method @androidx.window.RequiresWindowSdkExtension(version=2) public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1 super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator); method public kotlinx.coroutines.flow.Flow> splitInfoList(android.app.Activity activity);- method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void unpinTopActivityStack(int taskId);+ method @androidx.window.RequiresWindowSdkExtension(version=3) public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes); property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus; field public static final androidx.window.embedding.SplitController.Companion Companion; }@@ -326,6 +567,24 @@
method public androidx.window.embedding.SplitPairRule.Builder setTag(String? tag); }+ public final class SplitPinRule extends androidx.window.embedding.SplitRule {+ method public boolean isSticky();+ property public final boolean isSticky;+ }++ public static final class SplitPinRule.Builder {+ ctor public SplitPinRule.Builder();+ method public androidx.window.embedding.SplitPinRule build();+ method public androidx.window.embedding.SplitPinRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);+ method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);+ method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);+ method public androidx.window.embedding.SplitPinRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);+ method public androidx.window.embedding.SplitPinRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);+ method public androidx.window.embedding.SplitPinRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);+ method public androidx.window.embedding.SplitPinRule.Builder setSticky(boolean isSticky);+ method public androidx.window.embedding.SplitPinRule.Builder setTag(String? tag);+ }+ public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule { method public java.util.Set getFilters(); method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithPlaceholder();@@ -431,10 +690,20 @@
public static final class FoldingFeature.State.Companion { }+ public final class SupportedPosture {+ field public static final androidx.window.layout.SupportedPosture.Companion Companion;+ field public static final androidx.window.layout.SupportedPosture TABLETOP;+ }++ public static final class SupportedPosture.Companion {+ }+ public interface WindowInfoTracker { method public static androidx.window.layout.WindowInfoTracker getOrCreate(android.content.Context context);+ method public default java.util.List getSupportedPostures(); method public kotlinx.coroutines.flow.Flow windowLayoutInfo(android.app.Activity activity); method public default kotlinx.coroutines.flow.Flow windowLayoutInfo(@UiContext android.content.Context context);+ property @androidx.window.RequiresWindowSdkExtension(version=6) public default java.util.List supportedPostures; field public static final androidx.window.layout.WindowInfoTracker.Companion Companion; }@@ -449,8 +718,10 @@
public final class WindowMetrics { method public android.graphics.Rect getBounds();+ method public float getDensity(); method @SuppressCompatibility @RequiresApi(android.os.Build.VERSION_CODES.R) @androidx.window.core.ExperimentalWindowApi public androidx.core.view.WindowInsetsCompat getWindowInsets(); property public final android.graphics.Rect bounds;+ property public final float density; } public interface WindowMetricsCalculator {
@@ -13,6 +13,8 @@
field public static final String PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED = "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED"; field public static final String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE"; field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";+ field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE";+ field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE"; } public abstract class WindowSdkExtensions {@@ -107,8 +109,10 @@
@SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSessionPresenter extends androidx.window.area.WindowAreaSession { method public android.content.Context getContext();+ method public android.view.Window? getWindow(); method public void setContentView(android.view.View view); property public abstract android.content.Context context;+ property public abstract android.view.Window? window; } }@@ -123,9 +127,13 @@
package androidx.window.embedding { public final class ActivityEmbeddingController {- method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public kotlinx.coroutines.flow.Flow embeddedActivityWindowInfo(android.app.Activity activity);+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void finishActivityStacks(java.util.Set activityStacks);+ method public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity); method public static androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);+ method @androidx.window.RequiresWindowSdkExtension(version=3) public void invalidateVisibleActivityStacks(); method public boolean isActivityEmbedded(android.app.Activity activity);+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void setEmbeddingConfiguration(androidx.window.embedding.EmbeddingConfiguration embeddingConfiguration); field public static final androidx.window.embedding.ActivityEmbeddingController.Companion Companion; }@@ -133,6 +141,11 @@
method public androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context); }+ public final class ActivityEmbeddingOptions {+ method @androidx.window.RequiresWindowSdkExtension(version=5) public static android.os.Bundle setLaunchingActivityStack(android.os.Bundle, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public static android.os.Bundle setOverlayCreateParams(android.os.Bundle, android.app.Activity activity, androidx.window.embedding.OverlayCreateParams overlayCreateParams);+ }+ public final class ActivityFilter { ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction); method public android.content.ComponentName getComponentName();@@ -163,6 +176,84 @@
property public final boolean isEmpty; }+ public abstract class DividerAttributes {+ method public final int getColor();+ method public final int getWidthDp();+ property public final int color;+ property public final int widthDp;+ field public static final androidx.window.embedding.DividerAttributes.Companion Companion;+ field public static final androidx.window.embedding.DividerAttributes NO_DIVIDER;+ field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff+ }++ public static final class DividerAttributes.Companion {+ }++ public abstract static class DividerAttributes.DragRange {+ field public static final androidx.window.embedding.DividerAttributes.DragRange.Companion Companion;+ field public static final androidx.window.embedding.DividerAttributes.DragRange DRAG_RANGE_SYSTEM_DEFAULT;+ }++ public static final class DividerAttributes.DragRange.Companion {+ }++ public static final class DividerAttributes.DragRange.SplitRatioDragRange extends androidx.window.embedding.DividerAttributes.DragRange {+ ctor public DividerAttributes.DragRange.SplitRatioDragRange(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float minRatio, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float maxRatio);+ method public float getMaxRatio();+ method public float getMinRatio();+ property public final float maxRatio;+ property public final float minRatio;+ }++ public static final class DividerAttributes.DraggableDividerAttributes extends androidx.window.embedding.DividerAttributes {+ method public androidx.window.embedding.DividerAttributes.DragRange getDragRange();+ property public final androidx.window.embedding.DividerAttributes.DragRange dragRange;+ }++ @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.DraggableDividerAttributes.Builder {+ ctor public DividerAttributes.DraggableDividerAttributes.Builder();+ ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.DraggableDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.DraggableDividerAttributes original);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes build();+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setColor(@ColorInt int color);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setDragRange(androidx.window.embedding.DividerAttributes.DragRange dragRange);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);+ }++ public static final class DividerAttributes.FixedDividerAttributes extends androidx.window.embedding.DividerAttributes {+ }++ @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.FixedDividerAttributes.Builder {+ ctor public DividerAttributes.FixedDividerAttributes.Builder();+ ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.FixedDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.FixedDividerAttributes original);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes build();+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setColor(@ColorInt int color);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);+ }++ public final class EmbeddedActivityWindowInfo {+ method public android.graphics.Rect getBoundsInParentHost();+ method public android.graphics.Rect getParentHostBounds();+ method public boolean isEmbedded();+ property public final android.graphics.Rect boundsInParentHost;+ property public final boolean isEmbedded;+ property public final android.graphics.Rect parentHostBounds;+ }++ public abstract class EmbeddingAnimationBackground {+ method public static final androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);+ field public static final androidx.window.embedding.EmbeddingAnimationBackground.Companion Companion;+ field public static final androidx.window.embedding.EmbeddingAnimationBackground DEFAULT;+ }++ public static final class EmbeddingAnimationBackground.ColorBackground extends androidx.window.embedding.EmbeddingAnimationBackground {+ method public int getColor();+ property public final int color;+ }++ public static final class EmbeddingAnimationBackground.Companion {+ method public androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);+ }+ public final class EmbeddingAspectRatio { method public static androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio); field public static final androidx.window.embedding.EmbeddingAspectRatio ALWAYS_ALLOW;@@ -174,11 +265,149 @@
method public androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio); }+ public final class EmbeddingBounds {+ ctor public EmbeddingBounds(androidx.window.embedding.EmbeddingBounds.Alignment alignment, androidx.window.embedding.EmbeddingBounds.Dimension width, androidx.window.embedding.EmbeddingBounds.Dimension height);+ method public androidx.window.embedding.EmbeddingBounds.Alignment getAlignment();+ method public androidx.window.embedding.EmbeddingBounds.Dimension getHeight();+ method public androidx.window.embedding.EmbeddingBounds.Dimension getWidth();+ property public final androidx.window.embedding.EmbeddingBounds.Alignment alignment;+ property public final androidx.window.embedding.EmbeddingBounds.Dimension height;+ property public final androidx.window.embedding.EmbeddingBounds.Dimension width;+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_EXPANDED;+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_BOTTOM;+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_LEFT;+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_RIGHT;+ field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_TOP;+ field public static final androidx.window.embedding.EmbeddingBounds.Companion Companion;+ }++ public static final class EmbeddingBounds.Alignment {+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_BOTTOM;+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_LEFT;+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_RIGHT;+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_TOP;+ field public static final androidx.window.embedding.EmbeddingBounds.Alignment.Companion Companion;+ }++ public static final class EmbeddingBounds.Alignment.Companion {+ }++ public static final class EmbeddingBounds.Companion {+ }++ public abstract static class EmbeddingBounds.Dimension {+ method public static final androidx.window.embedding.EmbeddingBounds.Dimension pixel(@IntRange(from=1L) @Px int value);+ method public static final androidx.window.embedding.EmbeddingBounds.Dimension ratio(@FloatRange(from=0.0, fromInclusive=false, to=1.0, toInclusive=false) float ratio);+ field public static final androidx.window.embedding.EmbeddingBounds.Dimension.Companion Companion;+ field public static final androidx.window.embedding.EmbeddingBounds.Dimension DIMENSION_EXPANDED;+ field public static final androidx.window.embedding.EmbeddingBounds.Dimension DIMENSION_HINGE;+ }++ public static final class EmbeddingBounds.Dimension.Companion {+ method public androidx.window.embedding.EmbeddingBounds.Dimension pixel(@IntRange(from=1L) @Px int value);+ method public androidx.window.embedding.EmbeddingBounds.Dimension ratio(@FloatRange(from=0.0, fromInclusive=false, to=1.0, toInclusive=false) float ratio);+ }++ public final class EmbeddingConfiguration {+ ctor public EmbeddingConfiguration();+ ctor public EmbeddingConfiguration(optional @androidx.window.RequiresWindowSdkExtension(version=5) androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior);+ method public androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior getDimAreaBehavior();+ property public final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior;+ }++ public static final class EmbeddingConfiguration.Builder {+ ctor public EmbeddingConfiguration.Builder();+ method public androidx.window.embedding.EmbeddingConfiguration build();+ method public androidx.window.embedding.EmbeddingConfiguration.Builder setDimAreaBehavior(androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior area);+ }++ public static final class EmbeddingConfiguration.DimAreaBehavior {+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion Companion;+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_ACTIVITY_STACK;+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_TASK;+ field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior UNDEFINED;+ }++ public static final class EmbeddingConfiguration.DimAreaBehavior.Companion {+ }+ public abstract class EmbeddingRule { method public final String? getTag(); property public final String? tag; }+ public final class OverlayAttributes {+ ctor public OverlayAttributes();+ ctor public OverlayAttributes(optional androidx.window.embedding.EmbeddingBounds bounds);+ method public androidx.window.embedding.EmbeddingBounds getBounds();+ property public final androidx.window.embedding.EmbeddingBounds bounds;+ }++ public static final class OverlayAttributes.Builder {+ ctor public OverlayAttributes.Builder();+ method public androidx.window.embedding.OverlayAttributes build();+ method public androidx.window.embedding.OverlayAttributes.Builder setBounds(androidx.window.embedding.EmbeddingBounds bounds);+ }++ public final class OverlayAttributesCalculatorParams {+ method public androidx.window.embedding.OverlayAttributes getDefaultOverlayAttributes();+ method public String getOverlayTag();+ method public android.content.res.Configuration getParentConfiguration();+ method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();+ method public androidx.window.layout.WindowMetrics getParentWindowMetrics();+ property public final androidx.window.embedding.OverlayAttributes defaultOverlayAttributes;+ property public final String overlayTag;+ property public final android.content.res.Configuration parentConfiguration;+ property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;+ property public final androidx.window.layout.WindowMetrics parentWindowMetrics;+ }++ public final class OverlayController {+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void clearOverlayAttributesCalculator();+ method public static androidx.window.embedding.OverlayController getInstance(android.content.Context context);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public kotlinx.coroutines.flow.Flow overlayInfo(String overlayTag);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void setOverlayAttributesCalculator(kotlin.jvm.functions.Function1 super androidx.window.embedding.OverlayAttributesCalculatorParams,androidx.window.embedding.OverlayAttributes> calculator);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public void updateOverlayAttributes(String overlayTag, androidx.window.embedding.OverlayAttributes overlayAttributes);+ field public static final androidx.window.embedding.OverlayController.Companion Companion;+ }++ public static final class OverlayController.Companion {+ method public androidx.window.embedding.OverlayController getInstance(android.content.Context context);+ }++ public final class OverlayCreateParams {+ ctor public OverlayCreateParams();+ ctor public OverlayCreateParams(optional String tag);+ ctor public OverlayCreateParams(optional String tag, optional androidx.window.embedding.OverlayAttributes overlayAttributes);+ method public static String generateOverlayTag();+ method public androidx.window.embedding.OverlayAttributes getOverlayAttributes();+ method public String getTag();+ property public final androidx.window.embedding.OverlayAttributes overlayAttributes;+ property public final String tag;+ field public static final androidx.window.embedding.OverlayCreateParams.Companion Companion;+ }++ public static final class OverlayCreateParams.Builder {+ ctor public OverlayCreateParams.Builder();+ method public androidx.window.embedding.OverlayCreateParams build();+ method public androidx.window.embedding.OverlayCreateParams.Builder setOverlayAttributes(androidx.window.embedding.OverlayAttributes attrs);+ method public androidx.window.embedding.OverlayCreateParams.Builder setTag(String tag);+ }++ public static final class OverlayCreateParams.Companion {+ method public String generateOverlayTag();+ }++ public final class OverlayInfo {+ method public operator boolean contains(android.app.Activity activity);+ method public androidx.window.embedding.ActivityStack? getActivityStack();+ method public androidx.window.embedding.OverlayAttributes? getCurrentOverlayAttributes();+ method public String getOverlayTag();+ property public final androidx.window.embedding.ActivityStack? activityStack;+ property public final androidx.window.embedding.OverlayAttributes? currentOverlayAttributes;+ property public final String overlayTag;+ }+ public final class RuleController { method public void addRule(androidx.window.embedding.EmbeddingRule rule); method public void clearRules();@@ -196,8 +425,17 @@
} public final class SplitAttributes {+ ctor public SplitAttributes();+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType);+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground);+ ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.DividerAttributes dividerAttributes);+ method public androidx.window.embedding.EmbeddingAnimationBackground getAnimationBackground();+ method public androidx.window.embedding.DividerAttributes getDividerAttributes(); method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection(); method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();+ property public final androidx.window.embedding.EmbeddingAnimationBackground animationBackground;+ property public final androidx.window.embedding.DividerAttributes dividerAttributes; property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection; property public final androidx.window.embedding.SplitAttributes.SplitType splitType; field public static final androidx.window.embedding.SplitAttributes.Companion Companion;@@ -206,6 +444,8 @@
public static final class SplitAttributes.Builder { ctor public SplitAttributes.Builder(); method public androidx.window.embedding.SplitAttributes build();+ method @androidx.window.RequiresWindowSdkExtension(version=5) public androidx.window.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.embedding.EmbeddingAnimationBackground background);+ method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.embedding.DividerAttributes dividerAttributes); method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection); method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type); }@@ -256,10 +496,11 @@
method @androidx.window.RequiresWindowSdkExtension(version=2) public void clearSplitAttributesCalculator(); method public static androidx.window.embedding.SplitController getInstance(android.content.Context context); method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();- method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();+ method @androidx.window.RequiresWindowSdkExtension(version=5) public boolean pinTopActivityStack(int taskId, androidx.window.embedding.SplitPinRule splitPinRule); method @androidx.window.RequiresWindowSdkExtension(version=2) public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1 super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator); method public kotlinx.coroutines.flow.Flow> splitInfoList(android.app.Activity activity);- method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);+ method @androidx.window.RequiresWindowSdkExtension(version=5) public void unpinTopActivityStack(int taskId);+ method @androidx.window.RequiresWindowSdkExtension(version=3) public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes); property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus; field public static final androidx.window.embedding.SplitController.Companion Companion; }@@ -326,6 +567,24 @@
method public androidx.window.embedding.SplitPairRule.Builder setTag(String? tag); }+ public final class SplitPinRule extends androidx.window.embedding.SplitRule {+ method public boolean isSticky();+ property public final boolean isSticky;+ }++ public static final class SplitPinRule.Builder {+ ctor public SplitPinRule.Builder();+ method public androidx.window.embedding.SplitPinRule build();+ method public androidx.window.embedding.SplitPinRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);+ method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);+ method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);+ method public androidx.window.embedding.SplitPinRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);+ method public androidx.window.embedding.SplitPinRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);+ method public androidx.window.embedding.SplitPinRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);+ method public androidx.window.embedding.SplitPinRule.Builder setSticky(boolean isSticky);+ method public androidx.window.embedding.SplitPinRule.Builder setTag(String? tag);+ }+ public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule { method public java.util.Set getFilters(); method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithPlaceholder();@@ -431,10 +690,20 @@
public static final class FoldingFeature.State.Companion { }+ public final class SupportedPosture {+ field public static final androidx.window.layout.SupportedPosture.Companion Companion;+ field public static final androidx.window.layout.SupportedPosture TABLETOP;+ }++ public static final class SupportedPosture.Companion {+ }+ public interface WindowInfoTracker { method public static androidx.window.layout.WindowInfoTracker getOrCreate(android.content.Context context);+ method public default java.util.List getSupportedPostures(); method public kotlinx.coroutines.flow.Flow windowLayoutInfo(android.app.Activity activity); method public default kotlinx.coroutines.flow.Flow windowLayoutInfo(@UiContext android.content.Context context);+ property @androidx.window.RequiresWindowSdkExtension(version=6) public default java.util.List supportedPostures; field public static final androidx.window.layout.WindowInfoTracker.Companion Companion; }@@ -449,8 +718,10 @@
public final class WindowMetrics { method public android.graphics.Rect getBounds();+ method public float getDensity(); method @SuppressCompatibility @RequiresApi(android.os.Build.VERSION_CODES.R) @androidx.window.core.ExperimentalWindowApi public androidx.core.view.WindowInsetsCompat getWindowInsets(); property public final android.graphics.Rect bounds;+ property public final float density; } public interface WindowMetricsCalculator {
@@ -57,7 +57,7 @@
implementation("androidx.core:core:1.8.0") def extensions_core_version = "androidx.window.extensions.core:core:1.0.0"- def extensions_version = "androidx.window.extensions:extensions:1.2.0"+ def extensions_version = project(":window:extensions:extensions") // A compile only dependency on extnensions.core so that other libraries do not expose it // transitively. compileOnly(extensions_core_version)@@ -82,7 +82,6 @@
testImplementation(libs.kotlinCoroutinesTest) testImplementation(extensions_version) testImplementation(compileOnly(project(":window:sidecar:sidecar")))- testImplementation(compileOnly(extensions_version)) androidTestImplementation(libs.testCore) androidTestImplementation(libs.kotlinTestJunit)
@@ -0,0 +1,37 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.samples.embedding++import android.app.Activity+import androidx.annotation.Sampled+import androidx.window.embedding.ActivityEmbeddingController+import androidx.window.embedding.SplitController++@Sampled+suspend fun expandPrimaryContainer() {+ SplitController.getInstance(primaryActivity).splitInfoList(primaryActivity)+ .collect { splitInfoList ->+ // Find all associated secondary ActivityStacks+ val associatedSecondaryActivityStacks = splitInfoList+ .mapTo(mutableSetOf()) { splitInfo -> splitInfo.secondaryActivityStack }+ // Finish them all.+ ActivityEmbeddingController.getInstance(primaryActivity)+ .finishActivityStacks(associatedSecondaryActivityStacks)+ }+}++val primaryActivity = Activity()
@@ -0,0 +1,66 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.samples.embedding++import android.app.Activity+import androidx.annotation.Sampled+import androidx.core.app.ActivityOptionsCompat+import androidx.window.embedding.ActivityStack+import androidx.window.embedding.OverlayController+import androidx.window.embedding.SplitController+import androidx.window.embedding.setLaunchingActivityStack++@Sampled+suspend fun launchingOnPrimaryActivityStack() {+ var primaryActivityStack: ActivityStack? = null++ SplitController.getInstance(primaryActivity).splitInfoList(primaryActivity)+ .collect { splitInfoList ->+ primaryActivityStack = splitInfoList.last().primaryActivityStack+ }++ primaryActivity.startActivity(+ INTENT,+ ActivityOptionsCompat.makeBasic().toBundle()!!.setLaunchingActivityStack(+ primaryActivity,+ primaryActivityStack!!+ )+ )+}++@Sampled+suspend fun launchingOnOverlayActivityStack() {+ var overlayActivityStack: ActivityStack? = null++ OverlayController.getInstance(context).overlayInfo(TAG_OVERLAY).collect { overlayInfo ->+ overlayActivityStack = overlayInfo.activityStack+ }++ // The use case is to launch an Activity to an existing overlay ActivityStack from the overlain+ // Activity. If activityStack is not specified, the activity is launched to the top of the+ // host task behind the overlay ActivityStack.+ overlainActivity.startActivity(+ INTENT,+ ActivityOptionsCompat.makeBasic().toBundle()!!.setLaunchingActivityStack(+ overlainActivity,+ overlayActivityStack!!+ )+ )+}++const val TAG_OVERLAY = "overlay"+val overlainActivity = Activity()
@@ -0,0 +1,79 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.samples.embedding++import android.app.Activity+import android.content.Intent+import android.graphics.Rect+import androidx.annotation.Sampled+import androidx.core.app.ActivityOptionsCompat+import androidx.window.embedding.EmbeddingBounds+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.ratio+import androidx.window.embedding.OverlayAttributes+import androidx.window.embedding.OverlayController+import androidx.window.embedding.OverlayCreateParams+import androidx.window.embedding.setOverlayCreateParams++@Sampled+fun launchOverlayActivityStackSample() {+ // Creates an overlay container on the right+ val params = OverlayCreateParams(+ overlayAttributes = OverlayAttributes(+ EmbeddingBounds(+ alignment = EmbeddingBounds.Alignment.ALIGN_RIGHT,+ width = ratio(0.5f),+ height = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,+ )+ )+ )++ val optionsWithOverlayParams = ActivityOptionsCompat.makeBasic().toBundle()+ ?.setOverlayCreateParams(launchingActivity, params)++ // Start INTENT to the overlay container specified by params.+ launchingActivity.startActivity(INTENT, optionsWithOverlayParams)+}++@Sampled+fun overlayAttributesCalculatorSample() {+ // A sample to show overlay on the bottom if the device is portrait, and on the right when+ // the device is landscape.+ OverlayController.getInstance(launchingActivity).setOverlayAttributesCalculator { params ->+ val taskBounds = params.parentWindowMetrics.bounds+ return@setOverlayAttributesCalculator OverlayAttributes(+ if (taskBounds.isPortrait()) {+ EmbeddingBounds(+ alignment = EmbeddingBounds.Alignment.ALIGN_BOTTOM,+ width = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,+ height = ratio(0.5f),+ )+ } else {+ EmbeddingBounds(+ alignment = EmbeddingBounds.Alignment.ALIGN_RIGHT,+ width = ratio(0.5f),+ height = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,+ )+ }+ )+ }+}++private fun Rect.isPortrait(): Boolean = height() >= width()++val launchingActivity: Activity = Activity()++val INTENT = Intent()
@@ -17,7 +17,9 @@
package androidx.window.samples.embedding import android.app.Application+import android.graphics.Color import androidx.annotation.Sampled+import androidx.window.embedding.EmbeddingAnimationBackground import androidx.window.embedding.SplitAttributes import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND@@ -58,6 +60,10 @@
SplitAttributes.LayoutDirection.LOCALE } )+ // Optionally set the animation background to use when switching between+ // vertical and horizontal+ .setAnimationBackground(EmbeddingAnimationBackground.createColorBackground(+ Color.GRAY)) .build() } return@setSplitAttributesCalculator if (@@ -67,6 +73,10 @@
SplitAttributes.Builder() .setSplitType(SPLIT_TYPE_EQUAL) .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)+ // Optionally set the animation background to use when switching between+ // vertical and horizontal+ .setAnimationBackground(EmbeddingAnimationBackground.createColorBackground(+ Color.GRAY)) .build() } else { // Expand containers if the device is in portrait or the width is less than 600 dp.@@ -88,12 +98,18 @@
return@setSplitAttributesCalculator if (parentConfiguration.screenWidthDp >= 600) { builder .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)- // Set the color to use when switching between vertical and horizontal+ // Optionally set the animation background to use when switching between+ // vertical and horizontal+ .setAnimationBackground(EmbeddingAnimationBackground.createColorBackground(+ Color.GRAY)) .build() } else if (parentConfiguration.screenHeightDp >= 600) { builder .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)- // Set the color to use when switching between vertical and horizontal+ // Optionally set the animation background to use when switching between+ // vertical and horizontal+ .setAnimationBackground(EmbeddingAnimationBackground.createColorBackground(+ Color.GRAY)) .build() } else { // Fallback to expand the secondary container
@@ -46,6 +46,15 @@
WindowAreaComponentFullImplementation::class.java, 3)) }+ @Test+ fun isWindowAreaComponentValid_apiLevel1() {+ assertFalse(+ WindowAreaComponentValidator.isWindowAreaComponentValid(+ WindowAreaComponentApiV2Implementation::class.java, apiLevel = 1+ )+ )+ }+ /** * Test that validator returns correct results for API Level 2 [WindowAreaComponent] * implementation.@@ -54,10 +63,15 @@
fun isWindowAreaComponentValid_apiLevel2() { assertTrue( WindowAreaComponentValidator.isWindowAreaComponentValid(- WindowAreaComponentApiV2Implementation::class.java, 2))+ WindowAreaComponentApiV2Implementation::class.java, 2+ )+ )+ assertFalse( WindowAreaComponentValidator.isWindowAreaComponentValid(- IncompleteWindowAreaComponentApiV2Implementation::class.java, 3))+ IncompleteWindowAreaComponentApiV2Implementation::class.java, 3+ )+ ) } /**@@ -85,7 +99,9 @@
} /**- * Test that validator returns true if the [ExtensionWindowAreaStatus] is valid+ * Test that validator returns true if the [ExtensionWindowAreaStatus] is valid and expected to+ * be found on that vendorApiLevel. Verifies that true is returned if the vendorApiLevel is a+ * version before [ExtensionWindowAreaStatus] was introduced. */ @Test fun isExtensionWindowAreaStatusValid_trueIfValid() {@@ -98,15 +114,13 @@
} /**- * Test that validator returns false if the [ExtensionWindowAreaStatus] is incomplete+ * Test that validator returns false if the [ExtensionWindowAreaStatus] is incomplete and+ * expected to be on the device. */ @Test fun isExtensionWindowAreaStatusValid_falseIfIncomplete() { assertFalse( WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(- IncompleteExtensionWindowAreaStatus::class.java, 2))- assertFalse(- WindowAreaComponentValidator.isExtensionWindowAreaStatusValid( IncompleteExtensionWindowAreaStatus::class.java, 3)) }
@@ -34,81 +34,89 @@
@RunWith(AndroidJUnit4::class) public class WindowMetricsTest { @Test- public fun testGetBounds() {+ fun testGetBounds() { val bounds = Rect(1, 2, 3, 4)- val windowMetrics = WindowMetrics(bounds)+ val windowMetrics = WindowMetrics(bounds, density = 1f) assertEquals(bounds, windowMetrics.bounds) } @Test- public fun testEquals_sameBounds() {+ fun testEquals_sameBounds() { val bounds = Rect(1, 2, 3, 4)- val windowMetrics0 = WindowMetrics(bounds)- val windowMetrics1 = WindowMetrics(bounds)+ val windowMetrics0 = WindowMetrics(bounds, density = 1f)+ val windowMetrics1 = WindowMetrics(bounds, density = 1f) assertEquals(windowMetrics0, windowMetrics1) } @Test- public fun testEquals_differentBounds() {+ fun testEquals_differentBounds() { val bounds0 = Rect(1, 2, 3, 4)- val windowMetrics0 = WindowMetrics(bounds0)+ val windowMetrics0 = WindowMetrics(bounds0, density = 1f) val bounds1 = Rect(6, 7, 8, 9)- val windowMetrics1 = WindowMetrics(bounds1)+ val windowMetrics1 = WindowMetrics(bounds1, density = 1f) assertNotEquals(windowMetrics0, windowMetrics1) } @Test- public fun testHashCode_matchesIfEqual() {+ fun testEquals_differentDensities() { val bounds = Rect(1, 2, 3, 4)- val windowMetrics0 = WindowMetrics(bounds)- val windowMetrics1 = WindowMetrics(bounds)+ val windowMetrics0 = WindowMetrics(bounds, density = 0f)+ val windowMetrics1 = WindowMetrics(bounds, density = 1f)+ assertNotEquals(windowMetrics0, windowMetrics1)+ }++ @Test+ fun testHashCode_matchesIfEqual() {+ val bounds = Rect(1, 2, 3, 4)+ val windowMetrics0 = WindowMetrics(bounds, density = 1f)+ val windowMetrics1 = WindowMetrics(bounds, density = 1f) assertEquals(windowMetrics0.hashCode().toLong(), windowMetrics1.hashCode().toLong()) } @RequiresApi(Build.VERSION_CODES.R) @Test- public fun testSameWindowInsets_emptyInsets() {+ fun testSameWindowInsets_emptyInsets() { assumePlatformROrAbove() val bounds = Bounds(1, 2, 3, 4) val windowInsetsCompat = WindowInsetsCompat.Builder().build()- val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat)- val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat)+ val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)+ val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */) assertEquals(windowMetricsA, windowMetricsB) } @RequiresApi(Build.VERSION_CODES.R) @Test- public fun testSameWindowInsets_nonEmptyInsets() {+ fun testSameWindowInsets_nonEmptyInsets() { assumePlatformROrAbove() val bounds = Bounds(1, 2, 3, 4) val insets = Insets.of(6, 7, 8, 9) val builder = WindowInsetsCompat.Builder()- for (type in WindowMetricsCalculatorCompat.insetsTypeMasks) {+ for (type in WindowMetricsCalculatorCompat().insetsTypeMasks) { builder.setInsets(type, insets) } val windowInsetsCompat = builder.build()- val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat)- val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat)+ val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)+ val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */) assertEquals(windowMetricsA, windowMetricsB) } @RequiresApi(Build.VERSION_CODES.R) @Test- public fun testDiffWindowInsets() {+ fun testDiffWindowInsets() { assumePlatformROrAbove() val bounds = Bounds(1, 2, 3, 4) val insetsA = Insets.of(1, 2, 3, 4) val insetsB = Insets.of(6, 7, 8, 9) val builderA = WindowInsetsCompat.Builder() val builderB = WindowInsetsCompat.Builder()- for (type in WindowMetricsCalculatorCompat.insetsTypeMasks) {+ for (type in WindowMetricsCalculatorCompat().insetsTypeMasks) { builderA.setInsets(type, insetsA) builderB.setInsets(type, insetsB) } val windowInsetsCompatA = builderA.build() val windowInsetsCompatB = builderB.build()- val windowMetricsA = WindowMetrics(bounds, windowInsetsCompatA)- val windowMetricsB = WindowMetrics(bounds, windowInsetsCompatB)+ val windowMetricsA = WindowMetrics(bounds, windowInsetsCompatA, 1f /* density */)+ val windowMetricsB = WindowMetrics(bounds, windowInsetsCompatB, 1f /* density */) assertNotEquals(windowMetricsA, windowMetricsB) }
@@ -0,0 +1,66 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.layout.util++import android.os.Build+import android.view.WindowManager+import androidx.annotation.RequiresApi+import androidx.test.ext.junit.rules.ActivityScenarioRule+import androidx.test.ext.junit.runners.AndroidJUnit4+import androidx.test.filters.LargeTest+import androidx.window.TestActivity+import androidx.window.WindowTestUtils.Companion.assumePlatformROrAbove+import org.junit.Assert.assertEquals+import org.junit.Rule+import org.junit.Test+import org.junit.runner.RunWith++/**+ * Tests for [BoundsHelper].+ */+@LargeTest+@RunWith(AndroidJUnit4::class)+class BoundsHelperTest {++ @get:Rule+ var activityScenarioRule: ActivityScenarioRule =+ ActivityScenarioRule(TestActivity::class.java)++ @RequiresApi(Build.VERSION_CODES.R)+ @Test+ fun testCurrentWindowBounds_postR() {+ assumePlatformROrAbove()+ activityScenarioRule.scenario.onActivity { activity ->+ val currentBounds = BoundsHelper.getInstance().currentWindowBounds(activity)+ val expectedBounds = activity.getSystemService(WindowManager::class.java)+ .currentWindowMetrics.bounds+ assertEquals(expectedBounds, currentBounds)+ }+ }++ @RequiresApi(Build.VERSION_CODES.R)+ @Test+ fun testMaximumWindowBounds_postR() {+ assumePlatformROrAbove()+ activityScenarioRule.scenario.onActivity { activity ->+ val currentBounds = BoundsHelper.getInstance().maximumWindowBounds(activity)+ val expectedBounds = activity.getSystemService(WindowManager::class.java)+ .maximumWindowMetrics.bounds+ assertEquals(expectedBounds, currentBounds)+ }+ }+}
@@ -0,0 +1,107 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.layout.util++import android.graphics.Rect+import android.os.Build+import android.util.DisplayMetrics+import android.view.WindowInsets+import android.view.WindowManager+import android.view.WindowMetrics as AndroidWindowMetrics+import androidx.annotation.RequiresApi+import androidx.test.ext.junit.rules.ActivityScenarioRule+import androidx.test.ext.junit.runners.AndroidJUnit4+import androidx.test.filters.LargeTest+import androidx.window.TestActivity+import androidx.window.WindowTestUtils.Companion.assumePlatformBeforeU+import androidx.window.WindowTestUtils.Companion.assumePlatformROrAbove+import androidx.window.WindowTestUtils.Companion.assumePlatformUOrAbove+import androidx.window.WindowTestUtils.Companion.runActionsAcrossActivityLifecycle+import org.junit.Assert.assertEquals+import org.junit.Rule+import org.junit.Test+import org.junit.runner.RunWith++/** Tests for the [DensityCompatHelper] class. */+@LargeTest+@RunWith(AndroidJUnit4::class)+class DensityCompatHelperTest {++ @get:Rule+ var activityScenarioRule: ActivityScenarioRule =+ ActivityScenarioRule(TestActivity::class.java)++ @Test+ fun testDensityFromContext_beforeU() {+ assumePlatformBeforeU()+ runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->+ val helper = DensityCompatHelper.getInstance()+ assertEquals(activity.resources.displayMetrics.density, helper.density(activity))+ }+ }++ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)+ @Test+ fun testDensityFromContext_UOrAbove() {+ assumePlatformUOrAbove()+ runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->+ val wm = activity.getSystemService(WindowManager::class.java)+ val helper = DensityCompatHelper.getInstance()+ assertEquals(wm.currentWindowMetrics.density, helper.density(activity))+ }+ }++ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)+ @Test+ fun testDensityFromConfiguration_beforeU() {+ assumePlatformBeforeU()+ assumePlatformROrAbove()+ runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->+ val helper = DensityCompatHelper.getInstance()++ @Suppress("DEPRECATION")+ val fakeWindowMetrics = AndroidWindowMetrics(+ Rect(0, 0, 0, 0),+ WindowInsets.Builder().build(),+ )++ val density = helper.density(activity.resources.configuration, fakeWindowMetrics)+ val expectedDensity = activity.resources.configuration.densityDpi.toFloat() /+ DisplayMetrics.DENSITY_DEFAULT+ assertEquals(expectedDensity, density)+ }+ }++ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)+ @Test+ fun testDensityFromWindowMetrics_UOrAbove() {+ assumePlatformUOrAbove()+ runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->+ val helper = DensityCompatHelper.getInstance()++ val fakeDensity = 123.456f+ val fakeWindowMetrics = AndroidWindowMetrics(+ Rect(0, 0, 0, 0),+ WindowInsets.Builder().build(),+ fakeDensity+ )++ val density = helper.density(activity.resources.configuration, fakeWindowMetrics)+ assertEquals(fakeDensity, density)+ }+ }+}
@@ -119,7 +119,6 @@
* * ``` */- // TODO(b/274924641): Make property public const val PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED = "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED"@@ -182,4 +181,97 @@
*/ const val PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES"++ /**+ * Application-level+ * [PackageManager][android.content.pm.PackageManager.Property] tag that+ * (when set to false) informs the system the app has opted out of the+ * user-facing aspect ratio compatibility override.+ *+ * The compatibility override enables device users to set the app's aspect+ * ratio or force the app to fill the display regardless of the aspect+ * ratio or orientation specified in the app manifest.+ *+ * The aspect ratio compatibility override is exposed to users in device+ * settings. A menu in device settings lists all apps that have not opted out of+ * the compatibility override. Users select apps from the menu and set the+ * app aspect ratio on a per-app basis. Typically, the menu is available+ * only on large screen devices.+ *+ * When users apply the aspect ratio override, the minimum aspect ratio+ * specified in the app manifest is overridden. If users choose a+ * full-screen aspect ratio, the orientation of the activity is forced to+ * [SCREEN_ORIENTATION_USER][android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER];+ * see [PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE] to+ * disable the full-screen option only.+ *+ * The user override is intended to improve the app experience on devices+ * that have the ignore orientation request display setting enabled by OEMs+ * (enables compatibility mode for fixed orientation on Android 12 (API+ * level 31) or higher; see+ * [Large screen compatibility mode](https://developer.android.com/guide/topics/large-screens/large-screen-compatibility-mode)+ * for more details).+ *+ * To opt out of the user aspect ratio compatibility override, add this property+ * to your app manifest and set the value to `false`. Your app will be excluded+ * from the list of apps in device settings, and users will not be able to+ * override the app's aspect ratio.+ *+ * Not setting this property at all, or setting this property to `true` has+ * no effect.+ *+ * **Syntax:**+ * ```+ * + * + * android:name="android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE"+ * android:value="false" />+ * + * ```+ */+ const val PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE =+ "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE";++ /**+ * Application-level+ * [PackageManager][android.content.pm.PackageManager.Property] tag that+ * (when set to false) informs the system the app has opted out of the+ * full-screen option of the user aspect ratio compatibility override settings.+ * (For background information about the user aspect ratio compatibility override, see+ * [PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE].)+ *+ * When users apply the full-screen compatibility override, the orientation+ * of the activity is forced to+ * [SCREEN_ORIENTATION_USER][android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER].+ *+ * The user override aims to improve the app experience on devices that have+ * the ignore orientation request display setting enabled by OEMs (enables+ * compatibility mode for fixed orientation on Android 12 (API level 31) or+ * higher; see+ * [Large screen compatibility mode](https://developer.android.com/guide/topics/large-screens/large-screen-compatibility-mode)+ * for more details).+ *+ * To opt out of the full-screen option of the user aspect ratio compatibility+ * override, add this property to your app manifest and set the value to+ * `false`. Your app will have full-screen option removed from the list of user+ * aspect ratio override options in device settings, and users will not be able+ * to apply full-screen override to your app.+ *+ * **Note:** If [PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE] is+ * `false`, this property has no effect.+ *+ * Not setting this property at all, or setting this property to `true`+ * has no effect.+ *+ * **Syntax:**+ * ```+ * + * + * android:name="android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE"+ * android:value="false" />+ * + * ```+ */+ const val PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE =+ "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE"; }
@@ -17,6 +17,7 @@
package androidx.window import androidx.annotation.IntRange+import androidx.annotation.RestrictTo import androidx.window.core.ExtensionsUtil /**@@ -31,7 +32,7 @@
* * @sample androidx.window.samples.checkWindowSdkExtensionsVersion */-abstract class WindowSdkExtensions internal constructor() {+abstract class WindowSdkExtensions @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor() { /** * Reports the device's extension version@@ -55,6 +56,23 @@
} }+ /**+ * Checks the [extensionVersion] and throws [UnsupportedOperationException] if the version is+ * not in the [range].+ *+ * This is useful to provide compatibility for APIs updated in 2+ but deprecated in latest+ * version.+ *+ * @param range the required extension range of the targeting API.+ * @throws UnsupportedOperationException if the required [range] is not satisfied.+ */+ internal fun requireExtensionVersion(range: kotlin.ranges.IntRange) {+ if (extensionVersion !in range) {+ throw UnsupportedOperationException("This API requires extension version " ++ "$range, but the device is on $extensionVersion")+ }+ }+ companion object { /** Returns a [WindowSdkExtensions] instance. */ @JvmStatic@@ -64,17 +82,20 @@
private var decorator: WindowSdkExtensionsDecorator = EmptyDecoratorWindowSdk- internal fun overrideDecorator(overridingDecorator: WindowSdkExtensionsDecorator) {+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ fun overrideDecorator(overridingDecorator: WindowSdkExtensionsDecorator) { decorator = overridingDecorator }- internal fun reset() {+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ fun reset() { decorator = EmptyDecoratorWindowSdk } } }-internal interface WindowSdkExtensionsDecorator {+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+interface WindowSdkExtensionsDecorator { /** Returns a [WindowSdkExtensions] instance. */ fun decorate(windowSdkExtensions: WindowSdkExtensions): WindowSdkExtensions }
@@ -18,6 +18,7 @@
import android.content.Context import android.view.View+import android.view.Window import androidx.window.core.ExperimentalWindowApi /**@@ -35,6 +36,15 @@
val context: Context /**+ * Returns the [Window] associated with the active presentation window area or null if there is+ * no [Window] currently active. This could occur if the presenter has already been dismissed,+ * and there is no expectation that the [Window] would become non-null at a later point.+ * This API can be used to directly access parts of the [Window] API that are not available+ * through the [Context] provided.+ */+ val window: Window?++ /** * Sets a [View] to show on a window area. After setting the view the system can turn on the * corresponding display and start showing content. */
@@ -22,26 +22,26 @@
* Data class holding metrics about a specific device. */ internal class DeviceMetrics(- val manufacturer: String,+ val brand: String, val model: String, val rearDisplayMetrics: DisplayMetrics ) { override fun equals(other: Any?): Boolean { return other is DeviceMetrics &&- manufacturer == other.manufacturer &&+ brand == other.brand && model == other.model && rearDisplayMetrics.equals(other.rearDisplayMetrics) } override fun hashCode(): Int {- var result = manufacturer.hashCode()+ var result = brand.hashCode() result = 31 * result + model.hashCode() result = 31 * result + rearDisplayMetrics.hashCode() return result } override fun toString(): String {- return "DeviceMetrics{ Manufacturer: $manufacturer, model: $model, " ++ return "DeviceMetrics{ Brand: $brand, model: $model, " + "Rear display metrics: $rearDisplayMetrics }" } }
@@ -0,0 +1,53 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.area.utils++import android.os.Build+import android.util.DisplayMetrics+import androidx.window.area.WindowAreaController+import androidx.window.extensions.area.WindowAreaComponent++/**+ * Object to provide util methods for non-standard behaviors around device metrics that are needed+ * or provided through the [WindowAreaController] API's that may exist on certain devices or certain+ * vendor API levels. This currently includes the rear display metrics for devices that support+ * WindowArea features, but do not currently support the [WindowAreaComponent.getRearDisplayMetrics]+ * method. This object can expand to include any differences that have to be taken into account that+ * vary from the standard behavior.+ */+internal object DeviceMetricsCompatUtils {++ private val deviceMetricsList = listOf(DeviceMetrics("google", "pixel fold",+ DisplayMetrics().apply {+ widthPixels = 1080+ heightPixels = 2092+ density = 2.625f+ densityDpi = DisplayMetrics.DENSITY_420+ }+ ))++ fun hasDeviceMetrics(): Boolean {+ return getDeviceMetrics() != null+ }++ fun getDeviceMetrics(): DeviceMetrics? {+ return deviceMetricsList.firstOrNull { deviceMetrics ->+ deviceMetrics.brand.equals(Build.BRAND, ignoreCase = true) &&+ deviceMetrics.model.equals(Build.MODEL, ignoreCase = true)+ }+ }+}
@@ -1,49 +0,0 @@
-/*- * Copyright 2023 The Android Open Source Project- *- * Licensed under the Apache License, Version 2.0 (the "License");- * you may not use this file except in compliance with the License.- * You may obtain a copy of the License at- *- * http://www.apache.org/licenses/LICENSE-2.0- *- * Unless required by applicable law or agreed to in writing, software- * distributed under the License is distributed on an "AS IS" BASIS,- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.- * See the License for the specific language governing permissions and- * limitations under the License.- */--package androidx.window.area.utils--import android.util.DisplayMetrics-import java.util.Locale--/**- * Utility object to provide information about specific devices that may not be available- * through the extensions API at a certain vendor API level- */-internal object DeviceUtils {-- private val deviceList = listOf(DeviceMetrics("google", "pixel fold",- DisplayMetrics().apply {- widthPixels = 1080- heightPixels = 2092- density = 2.625f- densityDpi = 420 }- ))-- internal fun hasDeviceMetrics(manufacturer: String, model: String): Boolean {- return deviceList.any {- it.manufacturer == manufacturer.lowercase(Locale.US) &&- it.model == model.lowercase(Locale.US)- }- }-- internal fun getRearDisplayMetrics(manufacturer: String, model: String): DisplayMetrics? {- return deviceList.firstOrNull {- it.manufacturer == manufacturer.lowercase(Locale.US) &&- it.model == model.lowercase(Locale.US)- }?.rearDisplayMetrics- }-}
@@ -0,0 +1,41 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.area.utils++import android.annotation.SuppressLint+import android.view.Window+import androidx.window.extensions.area.ExtensionWindowAreaPresentation+import java.lang.reflect.Method++internal object PresentationWindowCompatUtils {++ // We perform our own extensions vendor API level check at the call-site+ @SuppressLint("BanUncheckedReflection")+ fun getWindowBeforeVendorApiLevel4(+ extensionPresentation: ExtensionWindowAreaPresentation+ ): Window? {+ val getWindowMethod = getWindowMethod(extensionPresentation)+ return if (getWindowMethod == null) null else+ (getWindowMethod.invoke(extensionPresentation) as Window?)+ }++ private fun getWindowMethod(extensionPresentation: ExtensionWindowAreaPresentation): Method? {+ return extensionPresentation.javaClass.methods.firstOrNull { method: Method? ->+ method?.name == "getWindow" && method.returnType == android.view.Window::class.java+ }+ }+}
@@ -28,10 +28,10 @@
* not contain any behavior or calculations. */ internal class Bounds(- public val left: Int,- public val top: Int,- public val right: Int,- public val bottom: Int+ val left: Int,+ val top: Int,+ val right: Int,+ val bottom: Int, ) { constructor(rect: Rect) : this(rect.left, rect.top, rect.right, rect.bottom)@@ -98,4 +98,8 @@
result = 31 * result + bottom return result }++ companion object {+ val EMPTY_BOUNDS = Bounds(0, 0, 0, 0)+ } }
@@ -17,11 +17,14 @@
package androidx.window.embedding import android.app.Activity-import android.app.ActivityOptions import android.content.Context-import android.os.IBinder+import android.os.Bundle+import androidx.core.util.Consumer import androidx.window.RequiresWindowSdkExtension-import androidx.window.core.ExperimentalWindowApi+import androidx.window.WindowSdkExtensions+import kotlinx.coroutines.channels.awaitClose+import kotlinx.coroutines.flow.Flow+import kotlinx.coroutines.flow.callbackFlow /** * The controller that allows checking the current [Activity] embedding status.@@ -33,7 +36,6 @@
* * @param activity the [Activity] to check. */- // TODO(b/204399167) Migrate to a Flow fun isActivityEmbedded(activity: Activity): Boolean = backend.isActivityEmbedded(activity)@@ -42,28 +44,141 @@
* embedding container and associated with a [SplitInfo]. Returns `null` if there is no such * [ActivityStack]. *+ * Started from [WindowSdkExtensions.extensionVersion] 5, this method can also obtain+ * standalone [ActivityStack], which is not associated with any [SplitInfo]. For example,+ * an [ActivityStack] launched with [ActivityRule.alwaysExpand], or an overlay [ActivityStack]+ * launched by [setLaunchingActivityStack] with [OverlayCreateParams].+ * * @param activity The [Activity] to check. * @return the [ActivityStack] that this [activity] is part of, or `null` if there is no such * [ActivityStack]. */- @ExperimentalWindowApi fun getActivityStack(activity: Activity): ActivityStack? = backend.getActivityStack(activity) /**- * Sets the launching [ActivityStack] to the given [android.app.ActivityOptions].+ * Sets the launching [ActivityStack] to the given [Bundle]. *- * @param options The [android.app.ActivityOptions] to be updated.- * @param token The token of the [ActivityStack] to be set.+ * Apps can launch an [Activity] into the [ActivityStack] by [Activity.startActivity].+ *+ * @param options the [Bundle] to be updated.+ * @param activityStack the [ActivityStack] to be set.+ */+ @RequiresWindowSdkExtension(5)+ internal fun setLaunchingActivityStack(+ options: Bundle,+ activityStack: ActivityStack+ ): Bundle = backend.setLaunchingActivityStack(options, activityStack)++ /**+ * Finishes a set of [activityStacks][ActivityStack] from the lowest to the highest z-order+ * regardless of the order of `activityStack` passed in the input parameter.+ *+ * If a remaining activityStack from a split participates in other splits with+ * other activityStacks, the remaining activityStack might split with other activityStacks.+ * For example, if activityStack A splits with activityStack B and C, and activityStack C covers+ * activityStack B, finishing activityStack C might make the split of activityStack A and B+ * show.+ *+ * If all split-associated activityStacks are finished, the remaining activityStack will+ * be expanded to fill the parent task container. This is useful to expand the primary+ * container as the sample linked below shows.+ *+ * **Note** that it's caller's responsibility to check whether this API is supported by checking+ * [WindowSdkExtensions.extensionVersion] is greater than or equal to 5. If not, an alternative+ * approach to finishing all containers above a particular activity can be to launch it again+ * with flag [android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP].+ *+ * @param activityStacks The set of [ActivityStack] to be finished.+ * @throws UnsupportedOperationException if extension version is less than 5.+ * @sample androidx.window.samples.embedding.expandPrimaryContainer+ */+ @RequiresWindowSdkExtension(5)+ fun finishActivityStacks(activityStacks: Set) {+ backend.finishActivityStacks(activityStacks)+ }++ /**+ * Sets the [EmbeddingConfiguration] of the Activity Embedding environment that defines how the+ * embedded Activities behaves.+ *+ * The [EmbeddingConfiguration] can be supported only if the vendor API level of the target+ * device is equals or higher than required API level. Otherwise, it would be no-op when setting+ * the [EmbeddingConfiguration] on a target device that has lower API level.+ *+ * In addition, the existing configuration in the library won't be overwritten if the properties+ * of the given [embeddingConfiguration] are undefined. Only the configuration properties that+ * are explicitly set will be updated.+ *+ * **Note** that it is recommended to be configured in the [androidx.startup.Initializer] or+ * [android.app.Application.onCreate], so that the [EmbeddingConfiguration] is applied early+ * in the application startup, before any activities complete initialization. The+ * [EmbeddingConfiguration] updates afterward may or may not apply to already running+ * activities.+ *+ * @param embeddingConfiguration The [EmbeddingConfiguration]+ */+ @RequiresWindowSdkExtension(5)+ fun setEmbeddingConfiguration(embeddingConfiguration: EmbeddingConfiguration) {+ backend.setEmbeddingConfiguration(embeddingConfiguration)+ }++ /**+ * Triggers calculator functions set through [SplitController.setSplitAttributesCalculator] and+ * [OverlayController.setOverlayAttributesCalculator] to update attributes for visible+ * [activityStacks][ActivityStack].+ *+ * This method can be used when the application wants to update the embedding presentation based+ * on the application state.+ *+ * This method is not needed for changes that are driven by window and device state changes or+ * new activity starts, because those will invoke the calculator functions+ * automatically.+ *+ * Visible [activityStacks][ActivityStack] are usually the last element of [SplitInfo]+ * list which was received from the callback registered in [SplitController.splitInfoList] and+ * an active overlay [ActivityStack] if exists.+ *+ * The call will be no-op if there is no visible [activityStacks][ActivityStack] or there's no+ * calculator set.+ *+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]+ * is less than 3.+ * @see androidx.window.embedding.OverlayController.setOverlayAttributesCalculator+ * @see androidx.window.embedding.SplitController.setSplitAttributesCalculator */ @RequiresWindowSdkExtension(3)- internal fun setLaunchingActivityStack(- options: ActivityOptions,- token: IBinder- ): ActivityOptions {- return backend.setLaunchingActivityStack(options, token)+ fun invalidateVisibleActivityStacks() {+ backend.invalidateVisibleActivityStacks() }+ /**+ * A [Flow] of [EmbeddedActivityWindowInfo] that reports the change to the embedded window+ * related info of the [activity].+ *+ * The [Flow] will immediately be invoked with the latest value upon registration if the+ * [activity] is currently embedded as [EmbeddedActivityWindowInfo.isEmbedded] is `true`.+ *+ * When the [activity] is embedded, the [Flow] will be invoked when [EmbeddedActivityWindowInfo]+ * is changed.+ * When the [activity] is not embedded, the [Flow] will not be triggered unless the [activity]+ * is becoming non-embedded from embedded.+ *+ * Note that this API is only supported on the device with+ * [WindowSdkExtensions.extensionVersion] equal to or larger than 6.+ * If [WindowSdkExtensions.extensionVersion] is less than 6, this [Flow] will not be invoked.+ *+ * @param activity the [Activity] that is interested in getting the embedded window info.+ * @return a [Flow] of [EmbeddedActivityWindowInfo] of the [activity].+ */+ @RequiresWindowSdkExtension(6)+ fun embeddedActivityWindowInfo(activity: Activity): Flow =+ callbackFlow {+ val callback = Consumer { info: EmbeddedActivityWindowInfo -> trySend(info) }+ backend.addEmbeddedActivityWindowInfoCallbackForActivity(activity, callback)+ awaitClose { backend.removeEmbeddedActivityWindowInfoCallbackForActivity(callback) }+ }+ companion object { /** * Obtains an instance of [ActivityEmbeddingController].
@@ -0,0 +1,107 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+@file:JvmName("ActivityEmbeddingOptions")++package androidx.window.embedding++import android.app.Activity+import android.app.ActivityOptions+import android.content.Context+import android.os.Bundle+import androidx.window.RequiresWindowSdkExtension+import androidx.window.WindowSdkExtensions++/**+ * Sets the target launching [ActivityStack] to the given [Bundle].+ *+ * The [Bundle] then could be used to launch an [Activity] to the top of the [ActivityStack] through+ * [Activity.startActivity]. If there's a bundle used for customizing how the [Activity] should be+ * started by [ActivityOptions.toBundle] or [androidx.core.app.ActivityOptionsCompat.toBundle],+ * it's suggested to use the bundle to call this method.+ *+ * It is suggested to use a visible [ActivityStack] reported by [SplitController.splitInfoList]+ * or [OverlayController.overlayInfo], or the launching activity will be launched on the default+ * target if the [activityStack] no longer exists in the host task. The default target could be+ * the top of the visible split's secondary [ActivityStack], or the top of the host task.+ *+ * Below samples are use cases to specify the launching [ActivityStack].+ *+ * @sample androidx.window.samples.embedding.launchingOnPrimaryActivityStack+ * @sample androidx.window.samples.embedding.launchingOnOverlayActivityStack+ *+ * @param context The [android.content.Context] that is going to be used for launching+ * activity with this [Bundle], which is usually be the [android.app.Activity] of the app that+ * hosts the task.+ * @param activityStack The target [ActivityStack] for launching.+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less than 5.+ */+@RequiresWindowSdkExtension(5)+fun Bundle.setLaunchingActivityStack(+ context: Context,+ activityStack: ActivityStack+): Bundle = ActivityEmbeddingController.getInstance(context)+ .setLaunchingActivityStack(this, activityStack)++/**+ * Puts [OverlayCreateParams] to [Bundle] to create a singleton-per-task overlay [ActivityStack].+ *+ * The [Bundle] then could be used to launch an [Activity] to the [ActivityStack] through+ * [Activity.startActivity]. If there's a bundle used for customizing how the [Activity] should be+ * started by [ActivityOptions.toBundle] or [androidx.core.app.ActivityOptionsCompat.toBundle],+ * it's suggested to use the bundle to call this method.+ *+ * Below sample shows how to launch an overlay [ActivityStack].+ *+ * If there's an existing overlay [ActivityStack] shown, the existing overlay container may be+ * dismissed or updated based on [OverlayCreateParams.tag] and [activity] because of following+ * constraints:+ * 1. A task can hold only one overlay container at most.+ * 2. An overlay [ActivityStack] tag is unique per process.+ *+ * Belows are possible scenarios:+ *+ * 1. If there's an overlay container with the same `tag` as [OverlayCreateParams.tag] in the same+ * task as [activity], the overlay container's [OverlayAttributes]+ * will be updated to [OverlayCreateParams.overlayAttributes], and the activity will be launched+ * on the top of the overlay [ActivityStack].+ *+ * 2. If there's an overlay container with different `tag` from [OverlayCreateParams.tag] in the+ * same task as [activity], the existing overlay container will be dismissed, and a new overlay+ * container will be launched with the new [OverlayCreateParams].+ *+ * 3. If there's an overlay container with the same `tag` as [OverlayCreateParams.tag] in a+ * different task from [activity], the existing overlay container in the other task will be+ * dismissed, and a new overlay container will be launched in the task of [activity].+ *+ * Note that the second and the third scenarios may happen at the same time if+ * [activity]'s task holds an overlay container and [OverlayCreateParams.tag] matches an overlay+ * container in a different task.+ *+ * @sample androidx.window.samples.embedding.launchOverlayActivityStackSample+ *+ * @param activity The [Activity] that is going to be used for launching activity with this+ * [ActivityOptions], which is usually be the [Activity] of the app that hosts the task.+ * @param overlayCreateParams The parameter container to create an overlay [ActivityStack]+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less than 6.+ */+@RequiresWindowSdkExtension(6)+fun Bundle.setOverlayCreateParams(+ activity: Activity,+ overlayCreateParams: OverlayCreateParams+): Bundle = OverlayController.getInstance(activity).setOverlayCreateParams(+ this,+ overlayCreateParams+)
@@ -0,0 +1,252 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++import android.os.Bundle+import androidx.window.RequiresWindowSdkExtension+import androidx.window.WindowSdkExtensions+import androidx.window.embedding.EmbeddingBounds.Dimension+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_EXPANDED+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_HINGE+import androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_ACTIVITY_STACK_TOKEN+import androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG+import androidx.window.extensions.embedding.ActivityStack.Token++/**+ * The implementation of ActivityEmbeddingOptions in WM Jetpack which uses constants defined in+ * [androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties] and this object.+ */+internal object ActivityEmbeddingOptionsImpl {++ /**+ * Key of [EmbeddingBounds].+ *+ * Type: [Bundle]+ *+ * Properties:+ *+ * |Key|Type|Property|+ * |---|----|----|+ * |[KEY_EMBEDDING_BOUNDS_ALIGNMENT]|[Int]|[EmbeddingBounds.alignment]|+ * |[KEY_EMBEDDING_BOUNDS_WIDTH]|[Bundle]|[EmbeddingBounds.width]|+ * |[KEY_EMBEDDING_BOUNDS_HEIGHT]|[Bundle]|[EmbeddingBounds.height]|+ */+ private const val KEY_EMBEDDING_BOUNDS = "androidx.window.embedding.EmbeddingBounds"++ /**+ * Key of [EmbeddingBounds.alignment].+ *+ * Type: [Int]+ *+ * Valid values are:+ * - 0: [EmbeddingBounds.Alignment.ALIGN_TOP]+ * - 1: [EmbeddingBounds.Alignment.ALIGN_LEFT]+ * - 2: [EmbeddingBounds.Alignment.ALIGN_BOTTOM]+ * - 3: [EmbeddingBounds.Alignment.ALIGN_RIGHT]+ */+ private const val KEY_EMBEDDING_BOUNDS_ALIGNMENT =+ "androidx.window.embedding.EmbeddingBounds.alignment"++ /**+ * Key of [EmbeddingBounds.width].+ *+ * Type: [Bundle] with [putDimension]+ *+ * Properties:+ *+ * |Key|Type|Property|+ * |---|----|----|+ * |[KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE]|[String]| The dimension type |+ * |[KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE]|[Int], [Float]| The dimension value |+ */+ private const val KEY_EMBEDDING_BOUNDS_WIDTH = "androidx.window.embedding.EmbeddingBounds.width"++ /**+ * Key of [EmbeddingBounds.width].+ *+ * Type: [Bundle] with [putDimension]+ *+ * Properties:+ *+ * |Key|Type|Property|+ * |---|----|----|+ * |[KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE]|[String]| The dimension type |+ * |[KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE]|[Int], [Float]| The dimension value |+ */+ private const val KEY_EMBEDDING_BOUNDS_HEIGHT =+ "androidx.window.embedding.EmbeddingBounds.height"++ /**+ * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates+ * the [Dimension] is [DIMENSION_EXPANDED].+ */+ private const val DIMENSION_TYPE_EXPANDED = "expanded"++ /**+ * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates+ * the [Dimension] is [DIMENSION_HINGE].+ */+ private const val DIMENSION_TYPE_HINGE = "hinge"++ /**+ * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates+ * the [Dimension] is from [Dimension.ratio]. If this type is used,+ * [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE] should also be specified with a [Float] value.+ */+ private const val DIMENSION_TYPE_RATIO = "ratio"++ /**+ * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates+ * the [Dimension] is from [Dimension.pixel]. If this type is used,+ * [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE] should also be specified with a [Int] value.+ */+ private const val DIMENSION_TYPE_PIXEL = "pixel"++ /**+ * Key of [EmbeddingBounds.Dimension] type.+ *+ * Type: [String]+ *+ * Valid values are:+ * - [DIMENSION_TYPE_EXPANDED]: [DIMENSION_EXPANDED]+ * - [DIMENSION_TYPE_HINGE]: [DIMENSION_HINGE]+ * - [DIMENSION_TYPE_RATIO]: [Dimension.ratio]+ * - [DIMENSION_TYPE_PIXEL]: [Dimension.pixel]+ */+ private const val KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE =+ "androidx.window.embedding.EmbeddingBounds.dimension_type"++ /**+ * Key of [EmbeddingBounds.Dimension] value.+ *+ * Type: [Float] or [Int]+ *+ * The value passed in [Dimension.pixel] or [Dimension.ratio]:+ * - Accept [Float] if [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE] is [DIMENSION_TYPE_RATIO]+ * - Accept [Int] if [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE] is [DIMENSION_TYPE_PIXEL]+ */+ private const val KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE =+ "androidx.window.embedding.EmbeddingBounds.dimension_value"++ /**+ * Puts [OverlayCreateParams] information to [android.app.ActivityOptions] bundle to launch+ * the overlay container+ *+ * @param overlayCreateParams The [OverlayCreateParams] to launch the overlay container+ */+ @RequiresWindowSdkExtension(6)+ internal fun setOverlayCreateParams(+ options: Bundle,+ overlayCreateParams: OverlayCreateParams,+ ) {+ WindowSdkExtensions.getInstance().requireExtensionVersion(6)++ options.putString(KEY_OVERLAY_TAG, overlayCreateParams.tag)+ options.putEmbeddingBounds(overlayCreateParams.overlayAttributes.bounds)+ }++ /**+ * Puts [EmbeddingBounds] information into a bundle for tracking.+ */+ private fun Bundle.putEmbeddingBounds(embeddingBounds: EmbeddingBounds) {+ putBundle(+ KEY_EMBEDDING_BOUNDS,+ Bundle().apply {+ putInt(KEY_EMBEDDING_BOUNDS_ALIGNMENT, embeddingBounds.alignment.value)+ putDimension(KEY_EMBEDDING_BOUNDS_WIDTH, embeddingBounds.width)+ putDimension(KEY_EMBEDDING_BOUNDS_HEIGHT, embeddingBounds.height)+ }+ )+ }++ internal fun Bundle.getOverlayAttributes(): OverlayAttributes? {+ val embeddingBounds = getEmbeddingBounds() ?: return null+ return OverlayAttributes(embeddingBounds)+ }++ private fun Bundle.getEmbeddingBounds(): EmbeddingBounds? {+ val embeddingBoundsBundle = getBundle(KEY_EMBEDDING_BOUNDS) ?: return null+ return EmbeddingBounds(+ EmbeddingBounds.Alignment(embeddingBoundsBundle.getInt(KEY_EMBEDDING_BOUNDS_ALIGNMENT)),+ embeddingBoundsBundle.getDimension(KEY_EMBEDDING_BOUNDS_WIDTH),+ embeddingBoundsBundle.getDimension(KEY_EMBEDDING_BOUNDS_HEIGHT)+ )+ }++ /**+ * Retrieves [EmbeddingBounds.Dimension] value from bundle with given [key].+ *+ * See [putDimension] for the data structure of [EmbeddingBounds.Dimension] as bundle+ */+ private fun Bundle.getDimension(key: String): Dimension {+ val dimensionBundle = getBundle(key)!!+ return when (val type = dimensionBundle.getString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE)) {+ DIMENSION_TYPE_EXPANDED -> DIMENSION_EXPANDED+ DIMENSION_TYPE_HINGE -> DIMENSION_HINGE+ DIMENSION_TYPE_RATIO ->+ Dimension.ratio(dimensionBundle.getFloat(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE))+ DIMENSION_TYPE_PIXEL ->+ Dimension.pixel(dimensionBundle.getInt(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE))+ else -> throw IllegalArgumentException("Illegal type $type")+ }+ }++ /**+ * Puts [EmbeddingBounds.Dimension] information into bundle with a given [key].+ *+ * [EmbeddingBounds.Dimension] is encoded as a [Bundle] with following data structure:+ * - [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE]: A `string` type. Must be one of:+ * - [DIMENSION_TYPE_EXPANDED]+ * - [DIMENSION_TYPE_HINGE]+ * - [DIMENSION_TYPE_RATIO]+ * - [DIMENSION_TYPE_PIXEL]+ * - [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE]: Only specified for [DIMENSION_TYPE_RATIO] and+ * [DIMENSION_TYPE_PIXEL]. [DIMENSION_TYPE_RATIO] requires a [Float], while+ * [DIMENSION_TYPE_PIXEL] requires a [Int].+ */+ private fun Bundle.putDimension(key: String, dimension: Dimension) {+ putBundle(+ key,+ Bundle().apply {+ when (dimension) {+ DIMENSION_EXPANDED -> {+ putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_EXPANDED)+ }++ DIMENSION_HINGE -> {+ putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_HINGE)+ }++ is Dimension.Ratio -> {+ putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_RATIO)+ putFloat(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE, dimension.value)+ }++ is Dimension.Pixel -> {+ putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_PIXEL)+ putInt(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE, dimension.value)+ }+ }+ }+ )+ }++ @RequiresWindowSdkExtension(5)+ internal fun setActivityStackToken(options: Bundle, activityStackToken: Token) {+ options.putBundle(KEY_ACTIVITY_STACK_TOKEN, activityStackToken.toBundle())+ }+}
@@ -17,13 +17,15 @@
import android.app.Activity import androidx.annotation.RestrictTo-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP+import androidx.window.RequiresWindowSdkExtension+import androidx.window.WindowSdkExtensions+import androidx.window.extensions.embedding.ActivityStack.Token /** * A container that holds a stack of activities, overlapping and bound to the same rectangle on the * screen. */-class ActivityStack @RestrictTo(LIBRARY_GROUP) constructor(+class ActivityStack internal constructor( /** * The [Activity] list in this application's process that belongs to this [ActivityStack]. *@@ -41,9 +43,32 @@
* `false`. */ val isEmpty: Boolean,+ /**+ * A token uniquely identifying this `ActivityStack`.+ */+ private val token: Token?, ) { /**+ * Creates ActivityStack ONLY for testing.+ *+ * @param activitiesInProcess the [Activity] list in this application's process that belongs to+ * this [ActivityStack].+ * @param isEmpty whether there is no [Activity] running in this [ActivityStack].+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ constructor(+ activitiesInProcess: List,+ isEmpty: Boolean+ ) : this(activitiesInProcess, isEmpty, token = null)++ @RequiresWindowSdkExtension(5)+ internal fun getToken(): Token = let {+ WindowSdkExtensions.getInstance().requireExtensionVersion(5)+ token!!+ }++ /** * Whether this [ActivityStack] contains the [activity]. */ operator fun contains(activity: Activity): Boolean {@@ -56,6 +81,7 @@
if (activitiesInProcess != other.activitiesInProcess) return false if (isEmpty != other.isEmpty) return false+ if (token != other.token) return false return true }@@ -63,6 +89,7 @@
override fun hashCode(): Int { var result = activitiesInProcess.hashCode() result = 31 * result + isEmpty.hashCode()+ result = 31 * result + token.hashCode() return result }@@ -70,5 +97,6 @@
"ActivityStack{" + "activitiesInProcess=$activitiesInProcess" + ", isEmpty=$isEmpty" ++ ", token=$token" + "}" }
@@ -0,0 +1,141 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++import android.app.Activity+import android.graphics.Rect+import android.util.ArrayMap+import androidx.annotation.GuardedBy+import androidx.annotation.VisibleForTesting+import androidx.core.util.Consumer as JetpackConsumer+import androidx.window.RequiresWindowSdkExtension+import androidx.window.WindowSdkExtensions+import androidx.window.extensions.core.util.function.Consumer+import androidx.window.extensions.embedding.ActivityEmbeddingComponent+import androidx.window.extensions.embedding.EmbeddedActivityWindowInfo as ExtensionsActivityWindowInfo+import java.util.concurrent.locks.ReentrantLock+import kotlin.concurrent.withLock++/** Manages and dispatches update of [EmbeddedActivityWindowInfo]. */+@RequiresWindowSdkExtension(6)+internal open class ActivityWindowInfoCallbackController(+ private val embeddingExtension: ActivityEmbeddingComponent,+) {+ private val globalLock = ReentrantLock()++ @GuardedBy("globalLock")+ private val extensionsCallback: Consumer++ @VisibleForTesting+ @GuardedBy("globalLock")+ internal var callbacks:+ MutableMap, CallbackWrapper> = ArrayMap()++ init {+ WindowSdkExtensions.getInstance().requireExtensionVersion(6)+ extensionsCallback = Consumer { info ->+ globalLock.withLock {+ for (callbackWrapper in callbacks.values) {+ callbackWrapper.accept(info)+ }+ }+ }+ }++ fun addCallback(+ activity: Activity,+ callback: JetpackConsumer+ ) {+ globalLock.withLock {+ if (callbacks.isEmpty()) {+ // Register when the first callback is added.+ embeddingExtension.setEmbeddedActivityWindowInfoCallback(+ Runnable::run,+ extensionsCallback+ )+ }++ val callbackWrapper = CallbackWrapper(activity, callback)+ callbacks[callback] = callbackWrapper+ embeddingExtension.getEmbeddedActivityWindowInfo(activity)?.apply {+ // Trigger with the latest info if the window exists.+ callbackWrapper.accept(this)+ }+ }+ }++ fun removeCallback(callback: JetpackConsumer) {+ globalLock.withLock {+ if (callbacks.remove(callback) == null) {+ // Early return if the callback is not registered.+ return+ }+ if (callbacks.isEmpty()) {+ // Unregister when the last callback is removed.+ embeddingExtension.clearEmbeddedActivityWindowInfoCallback()+ }+ }+ }++ /** Translates from Extensions info to Jetpack info. */+ @VisibleForTesting+ internal open fun translate(info: ExtensionsActivityWindowInfo): EmbeddedActivityWindowInfo {+ val parentHostBounds = Rect(info.taskBounds)+ val boundsInParentHost = Rect(info.activityStackBounds)+ // Converting to host container coordinate.+ boundsInParentHost.offset(-parentHostBounds.left, - parentHostBounds.top)+ return EmbeddedActivityWindowInfo(+ isEmbedded = info.isEmbedded,+ parentHostBounds = parentHostBounds,+ boundsInParentHost = boundsInParentHost+ )+ }++ @VisibleForTesting+ internal inner class CallbackWrapper(+ private val activity: Activity,+ val callback: JetpackConsumer+ ) {+ var lastReportedInfo: EmbeddedActivityWindowInfo? = null++ fun accept(extensionsActivityWindowInfo: ExtensionsActivityWindowInfo) {+ val updatedActivity = extensionsActivityWindowInfo.activity+ if (activity != updatedActivity) {+ return+ }++ val newInfo = translate(extensionsActivityWindowInfo)+ if (shouldReportInfo(newInfo)) {+ lastReportedInfo = newInfo+ callback.accept(newInfo)+ }+ }++ private fun shouldReportInfo(newInfo: EmbeddedActivityWindowInfo): Boolean =+ lastReportedInfo?.let {+ if (it.isEmbedded != newInfo.isEmbedded) {+ // Always report if the embedded status changes+ return true+ }+ if (!newInfo.isEmbedded) {+ // Do not report if the activity is not embedded+ return false+ }+ return it != newInfo+ } ?: newInfo.isEmbedded // Always report the first available info if it is embedded+ }+}
@@ -0,0 +1,340 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++import android.graphics.Color+import androidx.annotation.ColorInt+import androidx.annotation.FloatRange+import androidx.annotation.IntRange+import androidx.window.RequiresWindowSdkExtension++/**+ * The attributes of the divider layout and behavior.+ *+ * @property widthDp the width of the divider.+ * @property color the color of the divider.+ *+ * @see SplitAttributes.Builder.setDividerAttributes+ * @see FixedDividerAttributes+ * @see DraggableDividerAttributes+ * @see NO_DIVIDER+ */+abstract class DividerAttributes private constructor(+ @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) val widthDp: Int = WIDTH_SYSTEM_DEFAULT,+ @ColorInt val color: Int = Color.BLACK,+) {+ override fun toString(): String = DividerAttributes::class.java.simpleName + "{" ++ "width=$widthDp, " ++ "color=$color" ++ "}"++ /**+ * The attributes of a fixed divider. A fixed divider is a divider type that draws a static line+ * between the primary and secondary containers.+ *+ * @property widthDp the width of the divider.+ * @property color the color of the divider.+ *+ * @see SplitAttributes.Builder.setDividerAttributes+ */+ class FixedDividerAttributes @RequiresWindowSdkExtension(6) private constructor(+ @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int = WIDTH_SYSTEM_DEFAULT,+ @ColorInt color: Int = Color.BLACK+ ) : DividerAttributes(widthDp, color) {++ override fun equals(other: Any?): Boolean {+ if (this === other) return true+ if (other !is FixedDividerAttributes) return false+ return widthDp == other.widthDp && color == other.color+ }++ override fun hashCode(): Int = widthDp * 31 + color++ /**+ * The [FixedDividerAttributes] builder.+ *+ * @constructor creates a new [FixedDividerAttributes.Builder]+ */+ @RequiresWindowSdkExtension(6)+ class Builder() {+ @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong())+ private var widthDp = WIDTH_SYSTEM_DEFAULT++ @ColorInt+ private var color = Color.BLACK++ /**+ * The [FixedDividerAttributes] builder constructor initialized by an existing+ * [FixedDividerAttributes].+ *+ * @param original the original [FixedDividerAttributes] to initialize the [Builder].+ */+ @RequiresWindowSdkExtension(6)+ constructor(original: FixedDividerAttributes) : this() {+ widthDp = original.widthDp+ color = original.color+ }++ /**+ * Sets the divider width. It defaults to [WIDTH_SYSTEM_DEFAULT], which means the+ * system will choose a default value based on the display size and form factor.+ *+ * @throws IllegalArgumentException if the provided value is invalid.+ */+ @RequiresWindowSdkExtension(6)+ fun setWidthDp(@IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int): Builder =+ apply {+ validateWidth(widthDp)+ this.widthDp = widthDp+ }++ /**+ * Sets the color of the divider. If not set, the default color [Color.BLACK] is+ * used.+ *+ * @throws IllegalArgumentException if the provided value is invalid.+ */+ @RequiresWindowSdkExtension(6)+ fun setColor(@ColorInt color: Int): Builder =+ apply {+ validateColor(color)+ this.color = color+ }++ /** Builds a [FixedDividerAttributes] instance. */+ @RequiresWindowSdkExtension(6)+ fun build(): FixedDividerAttributes {+ return FixedDividerAttributes(widthDp = widthDp, color = color)+ }+ }+ }++ /**+ * The attributes of a draggable divider. A draggable divider draws a line between the primary+ * and secondary containers with a drag handle that the user can drag and resize the containers.+ *+ * @property widthDp the width of the divider.+ * @property color the color of the divider.+ * @property dragRange the range that a divider is allowed to be dragged. When the user+ * drags the divider beyond this range, the system will choose to either fully expand the+ * container or move the divider back into the range.+ *+ * @see SplitAttributes.Builder.setDividerAttributes+ */+ class DraggableDividerAttributes @RequiresWindowSdkExtension(6) private constructor(+ @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int = WIDTH_SYSTEM_DEFAULT,+ @ColorInt color: Int = Color.BLACK,+ val dragRange: DragRange = DragRange.DRAG_RANGE_SYSTEM_DEFAULT,+ ) : DividerAttributes(widthDp, color) {++ override fun equals(other: Any?): Boolean {+ if (this === other) return true+ if (other !is DraggableDividerAttributes) return false+ return widthDp == other.widthDp && color == other.color &&+ dragRange == other.dragRange+ }++ override fun hashCode(): Int = (widthDp * 31 + color) * 31 + dragRange.hashCode()++ override fun toString(): String = DividerAttributes::class.java.simpleName + "{" ++ "width=$widthDp, " ++ "color=$color, " ++ "primaryContainerDragRange=$dragRange" ++ "}"++ /**+ * The [DraggableDividerAttributes] builder.+ *+ * @constructor creates a new [DraggableDividerAttributes.Builder]+ */+ @RequiresWindowSdkExtension(6)+ class Builder() {+ @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong())+ private var widthDp = WIDTH_SYSTEM_DEFAULT++ @ColorInt+ private var color = Color.BLACK++ private var dragRange: DragRange = DragRange.DRAG_RANGE_SYSTEM_DEFAULT++ /**+ * The [DraggableDividerAttributes] builder constructor initialized by an existing+ * [DraggableDividerAttributes].+ *+ * @param original the original [DraggableDividerAttributes] to initialize the [Builder]+ */+ @RequiresWindowSdkExtension(6)+ constructor(original: DraggableDividerAttributes) : this() {+ widthDp = original.widthDp+ dragRange = original.dragRange+ color = original.color+ }++ /**+ * Sets the divider width. It defaults to [WIDTH_SYSTEM_DEFAULT], which means the+ * system will choose a default value based on the display size and form factor.+ *+ * @throws IllegalArgumentException if the provided value is invalid.+ */+ @RequiresWindowSdkExtension(6)+ fun setWidthDp(@IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int): Builder =+ apply {+ validateWidth(widthDp)+ this.widthDp = widthDp+ }++ /**+ * Sets the color of the divider. If not set, the default color [Color.BLACK] is+ * used.+ *+ * @throws IllegalArgumentException if the provided value is invalid.+ */+ @RequiresWindowSdkExtension(6)+ fun setColor(@ColorInt color: Int): Builder =+ apply {+ validateColor(color)+ this.color = color+ }++ /**+ * Sets the drag range of the divider in terms of the split ratio of the primary+ * container. It defaults to [DragRange.DRAG_RANGE_SYSTEM_DEFAULT], which means the+ * system will choose a default value based on the display size and form factor.+ *+ * When the user drags the divider beyond this range, the system will choose to either+ * fully expand the container or move the divider back into the range.+ *+ * @param dragRange the [DragRange] for the draggable divider.+ */+ @RequiresWindowSdkExtension(6)+ fun setDragRange(dragRange: DragRange): Builder =+ apply { this.dragRange = dragRange }++ /** Builds a [DividerAttributes] instance. */+ @RequiresWindowSdkExtension(6)+ fun build(): DraggableDividerAttributes =+ DraggableDividerAttributes(+ widthDp = widthDp,+ color = color,+ dragRange = dragRange,+ )+ }+ }++ /**+ * Describes the range that the user is allowed to drag the draggable divider.+ *+ * @see SplitRatioDragRange+ * @see DRAG_RANGE_SYSTEM_DEFAULT+ */+ abstract class DragRange private constructor() {+ /**+ * A drag range represented as an interval of the primary container's split ratios.+ *+ * @property minRatio the minimum split ratio of the primary container that the user is+ * allowed to drag to. When the divider is dragged beyond this ratio, the system will choose+ * to either fully expand the secondary container, or move the divider back to this ratio.+ * @property maxRatio the maximum split ratio of the primary container that the user is+ * allowed to drag to. When the divider is dragged beyond this ratio, the system will choose+ * to either fully expand the primary container, or move the divider back to this ratio.+ *+ * @constructor constructs a new [SplitRatioDragRange]+ * @throws IllegalArgumentException if the provided values are invalid.+ */+ class SplitRatioDragRange(+ @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)+ val minRatio: Float,+ @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)+ val maxRatio: Float,+ ) : DragRange() {+ init {+ if (minRatio <= 0.0 || minRatio >= 1.0) {+ throw IllegalArgumentException(+ "minRatio must be in the interval (0.0, 1.0)"+ )+ }+ if (maxRatio <= 0.0 || maxRatio >= 1.0) {+ throw IllegalArgumentException(+ "maxRatio must be in the interval (0.0, 1.0)"+ )+ }+ if (minRatio > maxRatio) {+ throw IllegalArgumentException(+ "minRatio must be less than or equal to maxRatio"+ )+ }+ }++ override fun toString(): String = "SplitRatioDragRange[$minRatio, $maxRatio]"++ override fun equals(other: Any?): Boolean {+ if (this === other) return true+ if (other !is SplitRatioDragRange) return false+ return minRatio == other.minRatio && maxRatio == other.maxRatio+ }++ override fun hashCode(): Int = minRatio.hashCode() * 31 + maxRatio.hashCode()+ }++ companion object {+ /**+ * A special value to indicate that the system will choose default values based on the+ * display size and form factor.+ *+ * @see DraggableDividerAttributes.dragRange+ */+ @JvmField+ val DRAG_RANGE_SYSTEM_DEFAULT = object : DragRange() {+ override fun toString(): String = "DRAG_RANGE_SYSTEM_DEFAULT"+ }+ }+ }++ companion object {+ /**+ * A special value to indicate that the system will choose a default value based on the+ * display size and form factor.+ *+ * @see DividerAttributes.widthDp+ */+ const val WIDTH_SYSTEM_DEFAULT: Int = -1++ /** Indicates that no divider is requested. */+ @JvmField+ val NO_DIVIDER = object : DividerAttributes() {+ override fun toString(): String = "NO_DIVIDER"+ }++ private fun validateWidth(widthDp: Int) = run {+ require(widthDp == WIDTH_SYSTEM_DEFAULT || widthDp >= 0) {+ "widthDp must be greater than or equal to 0 or WIDTH_SYSTEM_DEFAULT. Got: $widthDp"+ }+ }++ private fun validateColor(@ColorInt color: Int) = run {+ require(color.alpha() == 255) {+ "Divider color must be opaque. Got: ${Integer.toHexString(color)}"+ }+ }++ /**+ * Returns the alpha value of the color. This is the same as [Color.alpha] and is used to+ * avoid test-time dependency.+ */+ private fun Int.alpha() = this ushr 24+ }+}
@@ -0,0 +1,73 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++import android.graphics.Rect++/**+ * Describes the embedded window related info of an activity.+ *+ * When the activity is embedded, the [ActivityEmbeddingController.embeddedActivityWindowInfo] will+ * be invoked when any fields of [EmbeddedActivityWindowInfo] is changed.+ * When the activity is not embedded, the [ActivityEmbeddingController.embeddedActivityWindowInfo]+ * will not be triggered unless the activity is becoming non-embedded from embedded, in which case+ * [isEmbedded] will be `false`.+ *+ * @see ActivityEmbeddingController.embeddedActivityWindowInfo+ */+class EmbeddedActivityWindowInfo internal constructor(+ /**+ * Whether this activity is embedded and its presentation may be customized by the host+ * process of the task it is associated with.+ */+ val isEmbedded: Boolean,+ /**+ * The bounds of the host container in display coordinate space, which should be the Task bounds+ * for regular embedding use case, or if the activity is not embedded.+ */+ val parentHostBounds: Rect,+ /**+ * The relative bounds of the embedded [ActivityStack] in the host container coordinate space.+ * It has the same size as [parentHostBounds] if the activity is not embedded.+ */+ val boundsInParentHost: Rect,+) {+ override fun equals(other: Any?): Boolean {+ if (this === other) return true+ if (other !is EmbeddedActivityWindowInfo) return false++ if (isEmbedded != other.isEmbedded) return false+ if (parentHostBounds != other.parentHostBounds) return false+ if (boundsInParentHost != other.boundsInParentHost) return false++ return true+ }++ override fun hashCode(): Int {+ var result = isEmbedded.hashCode()+ result = 31 * result + parentHostBounds.hashCode()+ result = 31 * result + boundsInParentHost.hashCode()+ return result+ }++ override fun toString(): String =+ "EmbeddedActivityWindowInfo{" ++ "isEmbedded=$isEmbedded" ++ ", parentHostBounds=$parentHostBounds" ++ ", boundsInParentHost=$boundsInParentHost" ++ "}"+}
@@ -0,0 +1,113 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++import android.graphics.Color+import androidx.annotation.ColorInt+import androidx.annotation.IntRange++/**+ * Background to be used for window transition animations for embedding activities if the animation+ * requires a background.+ *+ * @see SplitAttributes.animationBackground+ */+abstract class EmbeddingAnimationBackground private constructor() {++ /**+ * An {@link EmbeddingAnimationBackground} to specify of using a developer-defined color as the+ * animation background.+ * Only opaque background is supported.+ *+ * @see EmbeddingAnimationBackground.createColorBackground+ */+ class ColorBackground internal constructor(+ /**+ * [ColorInt] to represent the color to use as the background color.+ */+ @IntRange(from = Color.BLACK.toLong(), to = Color.WHITE.toLong())+ @ColorInt+ val color: Int+ ) : EmbeddingAnimationBackground() {++ init {+ require(Color.alpha(color) == 255) {+ "Background color must be opaque"+ }+ }++ override fun toString() = "ColorBackground{color:${Integer.toHexString(color)}}"++ override fun equals(other: Any?): Boolean {+ if (other === this) return true+ if (other !is ColorBackground) return false+ return color == other.color+ }++ override fun hashCode() = color.hashCode()+ }++ /** @see EmbeddingAnimationBackground.DEFAULT */+ private class DefaultBackground : EmbeddingAnimationBackground() {++ override fun toString() = "DefaultBackground"+ }++ /**+ * Methods that create various [EmbeddingAnimationBackground].+ */+ companion object {++ /**+ * Creates a [ColorBackground] to represent the given [color].+ *+ * Only opaque color is supported.+ *+ * @param color [ColorInt] of an opaque color.+ * @return the [ColorBackground] representing the [color].+ * @throws IllegalArgumentException if the [color] is not opaque.+ *+ * @see [DEFAULT] for the default value, which means to use the+ * current theme window background color.+ */+ @JvmStatic+ fun createColorBackground(+ @IntRange(from = Color.BLACK.toLong(), to = Color.WHITE.toLong())+ @ColorInt+ color: Int+ ): ColorBackground = ColorBackground(color)++ /**+ * The special [EmbeddingAnimationBackground] to represent the default value,+ * which means to use the current theme window background color.+ */+ @JvmField+ val DEFAULT: EmbeddingAnimationBackground = DefaultBackground()++ /**+ * Returns an [EmbeddingAnimationBackground] with the given [color]+ */+ internal fun buildFromValue(@ColorInt color: Int): EmbeddingAnimationBackground {+ return if (Color.alpha(color) != 255) {+ // Treat any non-opaque color as the default.+ DEFAULT+ } else {+ createColorBackground(color)+ }+ }+ }+}
@@ -17,9 +17,8 @@
package androidx.window.embedding import android.app.Activity-import android.app.ActivityOptions import android.content.Context-import android.os.IBinder+import android.os.Bundle import androidx.annotation.RestrictTo import androidx.core.util.Consumer import androidx.window.RequiresWindowSdkExtension@@ -51,6 +50,12 @@
fun isActivityEmbedded(activity: Activity): Boolean+ @RequiresWindowSdkExtension(5)+ fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean++ @RequiresWindowSdkExtension(5)+ fun unpinTopActivityStack(taskId: Int)+ @RequiresWindowSdkExtension(2) fun setSplitAttributesCalculator( calculator: (SplitAttributesCalculatorParams) -> SplitAttributes@@ -61,15 +66,56 @@
fun getActivityStack(activity: Activity): ActivityStack?- @RequiresWindowSdkExtension(3)- fun setLaunchingActivityStack(options: ActivityOptions, token: IBinder): ActivityOptions+ @RequiresWindowSdkExtension(5)+ fun setLaunchingActivityStack(options: Bundle, activityStack: ActivityStack): Bundle++ @RequiresWindowSdkExtension(5)+ fun setOverlayCreateParams(options: Bundle, overlayCreateParams: OverlayCreateParams): Bundle++ @RequiresWindowSdkExtension(5)+ fun finishActivityStacks(activityStacks: Set)++ @RequiresWindowSdkExtension(5)+ fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration) @RequiresWindowSdkExtension(3)- fun invalidateTopVisibleSplitAttributes()+ fun invalidateVisibleActivityStacks() @RequiresWindowSdkExtension(3) fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes)+ @RequiresWindowSdkExtension(5)+ fun setOverlayAttributesCalculator(+ calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes+ )++ @RequiresWindowSdkExtension(5)+ fun clearOverlayAttributesCalculator()++ @RequiresWindowSdkExtension(5)+ fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes)++ @RequiresWindowSdkExtension(5)+ fun addOverlayInfoCallback(+ overlayTag: String,+ executor: Executor,+ overlayInfoCallback: Consumer,+ )++ @RequiresWindowSdkExtension(5)+ fun removeOverlayInfoCallback(overlayInfoCallback: Consumer)++ @RequiresWindowSdkExtension(6)+ fun addEmbeddedActivityWindowInfoCallbackForActivity(+ activity: Activity,+ callback: Consumer+ )++ @RequiresWindowSdkExtension(6)+ fun removeEmbeddedActivityWindowInfoCallbackForActivity(+ callback: Consumer+ )+ companion object { private var decorator: (EmbeddingBackend) -> EmbeddingBackend =
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingBounds.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingBounds.kt
new file mode 100644
index 0000000..7f9f97c
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingBounds.kt
@@ -0,0 +1,465 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++import android.graphics.Rect+import androidx.annotation.FloatRange+import androidx.annotation.IntRange+import androidx.annotation.Px+import androidx.annotation.VisibleForTesting+import androidx.window.core.Bounds+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_BOTTOM+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_LEFT+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_RIGHT+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_TOP+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_EXPANDED+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_HINGE+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.ratio+import androidx.window.layout.FoldingFeature+import androidx.window.layout.WindowLayoutInfo+import kotlin.math.min++/**+ * The bounds of a standalone [ActivityStack].+ *+ * It can be either described with `alignment`, `width` and `height` or predefined constant values.+ * Some important constants are:+ * - [BOUNDS_EXPANDED]: To indicate the bounds fills the parent window container.+ * - [BOUNDS_HINGE_TOP]: To indicate the bounds are at the top of the parent window container while+ * its bottom follows the hinge position. Refer to [BOUNDS_HINGE_LEFT], [BOUNDS_HINGE_BOTTOM] and+ * [BOUNDS_HINGE_RIGHT] for other bounds that follows the hinge position.+ *+ * @property alignment The alignment of the bounds relative to parent window container.+ * @property width The width of the bounds.+ * @property height The height of the bounds.+ * @constructor creates an embedding bounds.+ */+class EmbeddingBounds(val alignment: Alignment, val width: Dimension, val height: Dimension) {+ override fun toString(): String {+ return "Bounds:{alignment=$alignment, width=$width, height=$height}"+ }++ override fun hashCode(): Int {+ var result = alignment.hashCode()+ result = result * 31 + width.hashCode()+ result = result * 31 + height.hashCode()+ return result+ }++ override fun equals(other: Any?): Boolean {+ if (this === other) return true+ if (other !is EmbeddingBounds) return false+ return alignment == other.alignment &&+ width == other.width &&+ height == other.height+ }++ /** Returns `true` if the [width] should fallback to half of parent task width. */+ internal fun shouldUseFallbackDimensionForWidth(windowLayoutInfo: WindowLayoutInfo): Boolean {+ if (width != DIMENSION_HINGE) {+ return false+ }+ return !windowLayoutInfo.isVertical() ||+ alignment in listOf(ALIGN_TOP, ALIGN_BOTTOM)+ }++ /** Returns `true` if the [height] should fallback to half of parent task height. */+ internal fun shouldUseFallbackDimensionForHeight(windowLayoutInfo: WindowLayoutInfo): Boolean {+ if (height != DIMENSION_HINGE) {+ return false+ }+ return !windowLayoutInfo.isHorizontal() ||+ alignment in listOf(ALIGN_LEFT, ALIGN_RIGHT)+ }++ private fun WindowLayoutInfo.isHorizontal(): Boolean {+ val foldingFeature = getOnlyFoldingFeatureOrNull() ?: return false+ return foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL+ }++ private fun WindowLayoutInfo.isVertical(): Boolean {+ val foldingFeature = getOnlyFoldingFeatureOrNull() ?: return false+ return foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL+ }++ /**+ * Returns [FoldingFeature] if it's the only `FoldingFeature` in [WindowLayoutInfo]. Returns+ * `null`, otherwise.+ */+ private fun WindowLayoutInfo.getOnlyFoldingFeatureOrNull(): FoldingFeature? {+ val foldingFeatures = displayFeatures.filterIsInstance()+ return if (foldingFeatures.size == 1) foldingFeatures[0] else null+ }++ /**+ * Calculates [width] in pixel with [parentContainerBounds] and [windowLayoutInfo].+ */+ @Px+ internal fun getWidthInPixel(+ parentContainerBounds: Bounds,+ windowLayoutInfo: WindowLayoutInfo+ ): Int {+ val taskWidth = parentContainerBounds.width+ val widthDimension = if (shouldUseFallbackDimensionForWidth(windowLayoutInfo)) {+ ratio(0.5f)+ } else {+ width+ }+ when (widthDimension) {+ is Dimension.Ratio -> return widthDimension * taskWidth+ is Dimension.Pixel -> return min(taskWidth, widthDimension.value)+ DIMENSION_HINGE -> {+ // Should be verified by #shouldUseFallbackDimensionForWidth+ val hingeBounds = windowLayoutInfo.getOnlyFoldingFeatureOrNull()!!.bounds+ return when (alignment) {+ ALIGN_LEFT -> {+ hingeBounds.left - parentContainerBounds.left+ }+ ALIGN_RIGHT -> {+ parentContainerBounds.right - hingeBounds.right+ }+ else -> {+ throw IllegalStateException("Unhandled condition to get height in pixel! " ++ "embeddingBounds=$this taskBounds=$parentContainerBounds " ++ "windowLayoutInfo=$windowLayoutInfo")+ }+ }+ }+ else -> throw IllegalArgumentException("Unhandled width dimension=$width")+ }+ }++ /**+ * Calculates [height] in pixel with [parentContainerBounds] and [windowLayoutInfo].+ */+ @Px+ internal fun getHeightInPixel(+ parentContainerBounds: Bounds,+ windowLayoutInfo: WindowLayoutInfo+ ): Int {+ val taskHeight = parentContainerBounds.height+ val heightDimension = if (shouldUseFallbackDimensionForHeight(windowLayoutInfo)) {+ ratio(0.5f)+ } else {+ height+ }+ when (heightDimension) {+ is Dimension.Ratio -> return heightDimension * taskHeight+ is Dimension.Pixel -> return min(taskHeight, heightDimension.value)+ DIMENSION_HINGE -> {+ // Should be verified by #shouldUseFallbackDimensionForWidth+ val hingeBounds = windowLayoutInfo.getOnlyFoldingFeatureOrNull()!!.bounds+ return when (alignment) {+ ALIGN_TOP -> {+ hingeBounds.top - parentContainerBounds.top+ }+ ALIGN_BOTTOM -> {+ parentContainerBounds.bottom - hingeBounds.bottom+ }+ else -> {+ throw IllegalStateException("Unhandled condition to get height in pixel! " ++ "embeddingBounds=$this taskBounds=$parentContainerBounds " ++ "windowLayoutInfo=$windowLayoutInfo")+ }+ }+ }+ else -> throw IllegalArgumentException("Unhandled width dimension=$width")+ }+ }++ /** The position of the bounds relative to parent window container. */+ class Alignment internal constructor(@IntRange(from = 0, to = 3) internal val value: Int) {++ init { require(value in 0..3) }++ override fun equals(other: Any?): Boolean {+ if (this === other) return true+ if (other !is Alignment) return false+ return value == other.value+ }++ override fun hashCode(): Int {+ return value+ }++ override fun toString(): String = when (value) {+ 0 -> "top"+ 1 -> "left"+ 2 -> "bottom"+ 3 -> "right"+ else -> "unknown position:$value"+ }++ companion object {++ /** Specifies that the bounds is at the top of the parent window container. */+ @JvmField+ val ALIGN_TOP = Alignment(0)++ /** Specifies that the bounds is at the left of the parent window container. */+ @JvmField+ val ALIGN_LEFT = Alignment(1)++ /** Specifies that the bounds is at the bottom of the parent window container. */+ @JvmField+ val ALIGN_BOTTOM = Alignment(2)++ /** Specifies that the bounds is at the right of the parent window container. */+ @JvmField+ val ALIGN_RIGHT = Alignment(3)+ }+ }++ /**+ * The dimension of the bounds, which can be represented as multiple formats:+ * - [DIMENSION_EXPANDED]: means the bounds' dimension fills parent window's dimension.+ * - in [pixel]: To specify the dimension value in pixel.+ * - in [ratio]: To specify the dimension that relative to the parent window container.+ * For example, if [width] has [ratio] value 0.6, it means the bounds' width is 0.6 to the+ * parent window container's width.+ */+ abstract class Dimension internal constructor(internal val description: String) {++ override fun equals(other: Any?): Boolean {+ if (this === other) return true+ if (other !is Dimension) return false+ return description == other.description+ }++ override fun hashCode(): Int = description.hashCode()++ override fun toString(): String = description++ /**+ * The [Dimension] represented in pixel format+ *+ * @param value The dimension length in pixel+ */+ internal class Pixel(+ @Px+ @IntRange(from = 1)+ internal val value: Int+ ) : Dimension("dimension in pixel:$value") {++ init { require(value >= 1) { "Pixel value must be a positive integer." } }++ internal operator fun compareTo(dimen: Int): Int = value - dimen+ }++ /**+ * The [Dimension] represented in ratio format, which means the proportion of the parent+ * window dimension.+ *+ * @param value The ratio in (0.0, 1.0)+ */+ internal class Ratio(+ @FloatRange(from = 0.0, fromInclusive = false, to = 1.0)+ internal val value: Float+ ) : Dimension("dimension in ratio:$value") {++ init { require(value > 0.0 && value <= 1.0) { "Ratio must be in range (0.0, 1.0]" } }++ internal operator fun times(dimen: Int): Int = (value * dimen).toInt()+ }++ companion object {++ /** Represents this dimension follows its parent window dimension. */+ @JvmField+ val DIMENSION_EXPANDED: Dimension = Ratio(1.0f)++ /**+ * Represents this dimension follows the hinge position if the current window and+ * device state satisfies, or fallbacks to a half of the parent task dimension,+ * otherwise.+ *+ * The [DIMENSION_HINGE] works only if:+ *+ * - The parent container is not in multi-window mode (e.g., split-screen mode or+ * picture-in-picture mode)+ * - The device has a hinge or separating fold reported by+ * [androidx.window.layout.FoldingFeature.isSeparating]+ * - The hinge or separating fold orientation matches [EmbeddingBounds.alignment]:+ * - The hinge or fold orientation is vertical, and the position is+ * [POSITION_LEFT] or [POSITION_RIGHT]+ * - The hinge or fold orientation is horizontal, and the position is+ * [POSITION_TOP] or [POSITION_BOTTOM]+ */+ @JvmField+ val DIMENSION_HINGE: Dimension = object : Dimension("hinge") {}++ /**+ * Creates the dimension in pixel.+ *+ * If the dimension length exceeds the parent window dimension, the overlay container+ * will resize to fit the parent task dimension.+ *+ * @param value The dimension length in pixel+ */+ @JvmStatic+ fun pixel(@Px @IntRange(from = 1) value: Int): Dimension = Pixel(value)++ /**+ * Creates the dimension which takes a proportion of the parent window dimension.+ *+ * @param ratio The proportion of the parent window dimension this dimension should take+ */+ @JvmStatic+ fun ratio(+ @FloatRange(from = 0.0, fromInclusive = false, to = 1.0, toInclusive = false)+ ratio: Float+ ): Dimension = Ratio(ratio)+ }+ }++ companion object {++ /** The bounds fills the parent window bounds */+ @JvmField+ val BOUNDS_EXPANDED = EmbeddingBounds(ALIGN_TOP, DIMENSION_EXPANDED, DIMENSION_EXPANDED)++ /**+ * The bounds located on the top of the parent window, and the bounds' bottom side matches+ * the hinge position.+ */+ @JvmField+ val BOUNDS_HINGE_TOP = EmbeddingBounds(+ ALIGN_TOP,+ width = DIMENSION_EXPANDED,+ height = DIMENSION_HINGE+ )++ /**+ * The bounds located on the left of the parent window, and the bounds' right side matches+ * the hinge position.+ */+ @JvmField+ val BOUNDS_HINGE_LEFT = EmbeddingBounds(+ ALIGN_LEFT,+ width = DIMENSION_HINGE,+ height = DIMENSION_EXPANDED+ )++ /**+ * The bounds located on the bottom of the parent window, and the bounds' top side matches+ * the hinge position.+ */+ @JvmField+ val BOUNDS_HINGE_BOTTOM = EmbeddingBounds(+ ALIGN_BOTTOM,+ width = DIMENSION_EXPANDED,+ height = DIMENSION_HINGE+ )++ /**+ * The bounds located on the right of the parent window, and the bounds' left side matches+ * the hinge position.+ */+ @JvmField+ val BOUNDS_HINGE_RIGHT = EmbeddingBounds(+ ALIGN_RIGHT,+ width = DIMENSION_HINGE,+ height = DIMENSION_EXPANDED+ )++ /** Translates [EmbeddingBounds] to pure [Rect] bounds with given [ParentContainerInfo]. */+ @VisibleForTesting+ internal fun translateEmbeddingBounds(+ embeddingBounds: EmbeddingBounds,+ parentContainerBounds: Bounds,+ windowLayoutInfo: WindowLayoutInfo,+ ): Bounds {+ if (embeddingBounds.width == DIMENSION_EXPANDED &&+ embeddingBounds.height == DIMENSION_EXPANDED) {+ // If width and height are expanded, set bounds to empty to follow the parent task+ // bounds.+ return Bounds.EMPTY_BOUNDS+ }+ // 1. Fallbacks dimensions to ratio(0.5) if they can't follow the hinge with the current+ // device and window state.+ val width = if (embeddingBounds.shouldUseFallbackDimensionForWidth(windowLayoutInfo)) {+ ratio(0.5f)+ } else {+ embeddingBounds.width+ }+ val height = if (+ embeddingBounds.shouldUseFallbackDimensionForHeight(windowLayoutInfo)+ ) {+ ratio(0.5f)+ } else {+ embeddingBounds.height+ }++ // 2. Computes dimensions to pixel values. If it just matches parent task bounds, returns+ // the empty bounds to declare the bounds follow the parent task bounds.+ val sanitizedBounds = EmbeddingBounds(embeddingBounds.alignment, width, height)+ val widthInPixel = sanitizedBounds.getWidthInPixel(+ parentContainerBounds,+ windowLayoutInfo+ )+ val heightInPixel = sanitizedBounds.getHeightInPixel(+ parentContainerBounds,+ windowLayoutInfo+ )+ val taskWidth = parentContainerBounds.width+ val taskHeight = parentContainerBounds.height++ if (widthInPixel == taskWidth && heightInPixel == taskHeight) {+ return Bounds.EMPTY_BOUNDS+ }++ // 3. Offset the bounds by position:+ // - For top or bottom position, the bounds should attach to the top or bottom of+ // the parent task bounds and centered by the middle of the width.+ // - For left or right position, the bounds should attach to the left or right of+ // the parent task bounds and centered by the middle of the height.+ return Bounds(0, 0, widthInPixel, heightInPixel).let { bounds ->+ when (embeddingBounds.alignment) {+ ALIGN_TOP ->+ bounds.offset(((taskWidth - widthInPixel) / 2), 0)+ ALIGN_LEFT ->+ bounds.offset(0, ((taskHeight - heightInPixel) / 2))+ ALIGN_BOTTOM ->+ bounds.offset(((taskWidth - widthInPixel) / 2), taskHeight - heightInPixel)+ ALIGN_RIGHT ->+ bounds.offset(taskWidth - widthInPixel, ((taskHeight - heightInPixel) / 2))+ else ->+ throw IllegalArgumentException(+ "Unknown alignment: ${embeddingBounds.alignment}"+ )+ }+ }+ }++ private fun Bounds.offset(dx: Int, dy: Int): Bounds = Bounds(+ left + dx,+ top + dy,+ right + dx,+ bottom + dy+ )++ /** Translates [EmbeddingBounds] to pure [Rect] bounds with given [ParentContainerInfo]. */+ internal fun translateEmbeddingBounds(+ embeddingBounds: EmbeddingBounds,+ parentContainerInfo: ParentContainerInfo,+ ): Bounds = translateEmbeddingBounds(+ embeddingBounds,+ parentContainerInfo.windowBounds,+ parentContainerInfo.windowLayoutInfo+ )+ }+}
@@ -0,0 +1,126 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++import androidx.annotation.IntRange+import androidx.window.RequiresWindowSdkExtension++/**+ * Configurations of Activity Embedding environment that defines how the+ * embedded Activities behave.+ *+ * @see ActivityEmbeddingController.setEmbeddingConfiguration+ * @property dimAreaBehavior The requested dim area behavior.+ * @constructor The [EmbeddingConfiguration] constructor. The properties are undefined+ * if not specified.+ */+class EmbeddingConfiguration @JvmOverloads constructor(+ @RequiresWindowSdkExtension(5)+ val dimAreaBehavior: DimAreaBehavior = DimAreaBehavior.UNDEFINED+) {+ /**+ * The area of dimming to apply.+ *+ * @see [android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND]+ */+ class DimAreaBehavior private constructor(@IntRange(from = 0, to = 2) internal val value: Int) {+ companion object {+ /**+ * The dim area is not defined.+ *+ * This is the default value while building a [EmbeddingConfiguration]. This would+ * also keep the existing dim area configuration of the current Activity Embedding+ * environment unchanged when [ActivityEmbeddingController.setEmbeddingConfiguration]+ * is called.+ *+ * @see ActivityEmbeddingController.setEmbeddingConfiguration+ */+ @JvmField+ val UNDEFINED = DimAreaBehavior(0)++ /**+ * The dim effect is applying on the [ActivityStack] of the Activity window when needed.+ * If the [ActivityStack] is split and displayed side-by-side with another+ * [ActivityStack], the dim effect is applying only on the [ActivityStack] of the+ * requested Activity.+ */+ @JvmField+ val ON_ACTIVITY_STACK = DimAreaBehavior(1)++ /**+ * The dimming effect is applying on the area of the whole Task when needed. If the+ * embedded transparent activity is split and displayed side-by-side with another+ * activity, the dim effect is applying on the Task, which across over the two+ * [ActivityStack]s.+ *+ * This is the default dim area configuration of the Activity Embedding environment,+ * before the [DimAreaBehavior] is explicitly set by+ * [ActivityEmbeddingController.setEmbeddingConfiguration].+ */+ @JvmField+ val ON_TASK = DimAreaBehavior(2)+ }++ override fun toString(): String {+ return "DimAreaBehavior=" + when (value) {+ 0 -> "UNDEFINED"+ 1 -> "ON_ACTIVITY_STACK"+ 2 -> "ON_TASK"+ else -> "UNKNOWN"+ }+ }+ }++ override fun equals(other: Any?): Boolean {+ if (this === other) return true+ if (other !is EmbeddingConfiguration) return false++ if (dimAreaBehavior != other.dimAreaBehavior) return false;+ return true+ }++ override fun hashCode(): Int {+ return dimAreaBehavior.hashCode()+ }++ override fun toString(): String =+ "EmbeddingConfiguration{$dimAreaBehavior}"++ /**+ * Builder for creating an instance of [EmbeddingConfiguration].+ */+ class Builder {+ private var mDimAreaBehavior = DimAreaBehavior.UNDEFINED++ /**+ * Sets the dim area behavior. By default, the [DimAreaBehavior.UNDEFINED] is used if not+ * set.+ *+ * @param area The dim area.+ * @return This [Builder]+ */+ @SuppressWarnings("MissingGetterMatchingBuilder")+ fun setDimAreaBehavior(area: DimAreaBehavior): Builder = apply { mDimAreaBehavior = area }++ /**+ * Builds a[EmbeddingConfiguration] instance.+ *+ * @return The new [EmbeddingConfiguration] instance.+ */+ fun build(): EmbeddingConfiguration = EmbeddingConfiguration(mDimAreaBehavior)+ }+}
@@ -0,0 +1,62 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++/**+ * The attributes to describe how an overlay container should look like.+ *+ * @property bounds The overlay container's [EmbeddingBounds], which defaults to+ * [EmbeddingBounds.BOUNDS_EXPANDED] if not specified.+ * @constructor creates an overlay attributes.+ */+class OverlayAttributes @JvmOverloads constructor(+ val bounds: EmbeddingBounds = EmbeddingBounds.BOUNDS_EXPANDED+) {++ override fun toString(): String =+ "${OverlayAttributes::class.java.simpleName}: {" ++ "bounds=$bounds" ++ "}"++ override fun equals(other: Any?): Boolean {+ if (this === other) return true+ if (other !is OverlayAttributes) return false+ return bounds == other.bounds+ }++ override fun hashCode(): Int = bounds.hashCode()++ /** The [OverlayAttributes] builder. */+ class Builder {++ private var bounds = EmbeddingBounds.BOUNDS_EXPANDED++ /**+ * Sets the overlay bounds, which defaults to [EmbeddingBounds.BOUNDS_EXPANDED] if not+ * specified.+ *+ * @param bounds The [EmbeddingBounds] of the overlay [ActivityStack].+ * @return The [OverlayAttributes] builder.+ */+ fun setBounds(bounds: EmbeddingBounds): Builder = apply {+ this.bounds = bounds+ }++ /** Builds [OverlayAttributes]. */+ fun build(): OverlayAttributes = OverlayAttributes(bounds)+ }+}
@@ -0,0 +1,53 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++import android.content.res.Configuration+import androidx.window.layout.WindowLayoutInfo+import androidx.window.layout.WindowMetrics++/**+ * The parameter container used to report the current device and window state in+ * [OverlayController.setOverlayAttributesCalculator] and references the corresponding overlay+ * [ActivityStack] by [overlayTag].+ */+class OverlayAttributesCalculatorParams internal constructor(+ /** The parent container's [WindowMetrics] */+ val parentWindowMetrics: WindowMetrics,+ /** The parent container's [Configuration] */+ val parentConfiguration: Configuration,+ /** The parent container's [WindowLayoutInfo] */+ val parentWindowLayoutInfo: WindowLayoutInfo,+ /**+ * The unique identifier of the overlay [ActivityStack] specified by [OverlayCreateParams.tag]+ */+ val overlayTag: String,+ /**+ * The overlay [ActivityStack]'s [OverlayAttributes] specified by [overlayTag], which is the+ * [OverlayAttributes] that is not calculated by calculator. It should be either initialized by+ * [OverlayCreateParams.overlayAttributes] or [OverlayController.updateOverlayAttributes].+ */+ val defaultOverlayAttributes: OverlayAttributes,+) {+ override fun toString(): String =+ "${OverlayAttributesCalculatorParams::class.java}:{" ++ "parentWindowMetrics=$parentWindowMetrics" ++ "parentConfiguration=$parentConfiguration" ++ "parentWindowLayoutInfo=$parentWindowLayoutInfo" ++ "overlayTag=$overlayTag" ++ "defaultOverlayAttributes=$defaultOverlayAttributes"+}
@@ -0,0 +1,151 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++import android.app.Activity+import android.content.Context+import android.os.Bundle+import androidx.annotation.VisibleForTesting+import androidx.core.util.Consumer+import androidx.window.RequiresWindowSdkExtension+import androidx.window.WindowSdkExtensions+import kotlinx.coroutines.channels.awaitClose+import kotlinx.coroutines.flow.Flow+import kotlinx.coroutines.flow.callbackFlow++/**+ * The controller to manage overlay [ActivityStack], which is launched by+ * the activityOptions that [setOverlayCreateParams].+ *+ * See linked sample below for how to launch an [android.app.Activity] into an overlay+ * [ActivityStack].+ *+ * Supported operations are:+ * - [setOverlayAttributesCalculator] to update overlay presentation with device or window state and+ * [OverlayCreateParams.tag].+ *+ * @sample androidx.window.samples.embedding.launchOverlayActivityStackSample+ */+class OverlayController @VisibleForTesting internal constructor(+ private val backend: EmbeddingBackend+) {++ @RequiresWindowSdkExtension(6)+ internal fun setOverlayCreateParams(+ options: Bundle,+ overlayCreateParams: OverlayCreateParams,+ ): Bundle = backend.setOverlayCreateParams(options, overlayCreateParams)++ /**+ * Sets an overlay calculator function to update overlay presentation with device or window+ * state and [OverlayCreateParams.tag].+ *+ * Overlay calculator function is triggered with following scenarios:+ * - An overlay [ActivityStack] is launched.+ * - The parent task configuration changes. i.e. orientation change, enter/exit multi-window+ * mode or resize apps in multi-window mode.+ * - Device folding state changes.+ * - Device is attached to an external display and the app is forwarded to that display.+ *+ * If there's no [calculator] set, the overlay presentation will be calculated with+ * the previous set [OverlayAttributes], either from [OverlayCreateParams] to initialize+ * the overlay container, or from the runtime API to update the overlay container's+ * [OverlayAttributes].+ *+ * See the sample linked below for how to use [OverlayAttributes] calculator+ *+ * @param calculator The overlay calculator function to compute [OverlayAttributes] by+ * [OverlayAttributesCalculatorParams]. It will replace the previously set if it exists.+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]+ * is less than 6.+ * @sample androidx.window.samples.embedding.overlayAttributesCalculatorSample+ */+ @RequiresWindowSdkExtension(6)+ fun setOverlayAttributesCalculator(+ calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes+ ) {+ backend.setOverlayAttributesCalculator(calculator)+ }++ /**+ * Clears the overlay calculator function previously set by [setOverlayAttributesCalculator].+ *+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]+ * is less than 6.+ */+ @RequiresWindowSdkExtension(6)+ fun clearOverlayAttributesCalculator() {+ backend.clearOverlayAttributesCalculator()+ }++ /**+ * Updates [OverlayAttributes] of the overlay [ActivityStack] specified by [overlayTag].+ * It's no op if there's no such overlay [ActivityStack] associated with [overlayTag].+ *+ * If an [OverlayAttributes] calculator function is specified, the updated [overlayAttributes]+ * will be passed by [OverlayAttributesCalculatorParams.defaultOverlayAttributes] when the+ * calculator function applies to the overlay [ActivityStack] specified by [overlayTag].+ *+ * In most cases it is suggested to use+ * [ActivityEmbeddingController.invalidateVisibleActivityStacks] if a calculator has been set+ * through [OverlayController.setOverlayAttributesCalculator].+ *+ * @param overlayTag The overlay [ActivityStack]'s tag+ * @param overlayAttributes The [OverlayAttributes] to update+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]+ * is less than 6.+ */+ @RequiresWindowSdkExtension(6)+ fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes) {+ backend.updateOverlayAttributes(overlayTag, overlayAttributes)+ }++ /**+ * A [Flow] of [OverlayInfo] that [overlayTag] is associated with.+ *+ * If there's an active overlay [ActivityStack] associated with [overlayTag], it will be+ * reported in [OverlayInfo.activityStack]. Otherwise, [OverlayInfo.activityStack] is `null`.+ *+ * Note that launching an overlay [ActivityStack] only supports on the device with+ * [WindowSdkExtensions.extensionVersion] equal to or larger than 6.+ * If [WindowSdkExtensions.extensionVersion] is less than 6, this flow will always+ * report [OverlayInfo] without associated [OverlayInfo.activityStack].+ *+ * @param overlayTag The overlay [ActivityStack]'s tag which is set through+ * [OverlayCreateParams]+ * @return a [Flow] of [OverlayInfo] this [overlayTag] is associated with+ */+ @RequiresWindowSdkExtension(6)+ fun overlayInfo(overlayTag: String): Flow = callbackFlow {+ val listener = Consumer { info: OverlayInfo -> trySend(info) }+ backend.addOverlayInfoCallback(overlayTag, Runnable::run, listener)+ awaitClose { backend.removeOverlayInfoCallback(listener) }+ }++ companion object {+ /**+ * Obtains an instance of [OverlayController].+ *+ * @param context the [Context] to initialize the controller with+ */+ @JvmStatic+ fun getInstance(context: Context): OverlayController {+ val backend = EmbeddingBackend.getInstance(context)+ return OverlayController(backend)+ }+ }+}
@@ -0,0 +1,296 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++import android.content.res.Configuration+import android.util.ArrayMap+import androidx.annotation.GuardedBy+import androidx.annotation.VisibleForTesting+import androidx.core.util.Consumer as JetpackConsumer+import androidx.window.RequiresWindowSdkExtension+import androidx.window.WindowSdkExtensions+import androidx.window.embedding.ActivityEmbeddingOptionsImpl.getOverlayAttributes+import androidx.window.extensions.core.util.function.Consumer+import androidx.window.extensions.embedding.ActivityEmbeddingComponent+import androidx.window.extensions.embedding.ActivityStack+import androidx.window.extensions.embedding.ActivityStackAttributes+import androidx.window.extensions.embedding.ParentContainerInfo+import androidx.window.layout.WindowLayoutInfo+import androidx.window.layout.WindowMetrics+import androidx.window.layout.WindowMetricsCalculator+import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter+import androidx.window.layout.util.DensityCompatHelper+import java.util.concurrent.Executor+import java.util.concurrent.locks.ReentrantLock+import kotlin.concurrent.withLock++/**+ * The core implementation of [OverlayController] APIs, which is implemented by [ActivityStack]+ * operations in WM Extensions.+ */+@Suppress("NewApi") // Suppress #translateWindowMetrics, which requires R.+@RequiresWindowSdkExtension(6)+internal open class OverlayControllerImpl(+ private val embeddingExtension: ActivityEmbeddingComponent,+ private val adapter: EmbeddingAdapter,+) {+ private val globalLock = ReentrantLock()++ @GuardedBy("globalLock")+ internal var overlayAttributesCalculator:+ ((OverlayAttributesCalculatorParams) -> OverlayAttributes)? = null+ get() = globalLock.withLock { field }+ set(value) { globalLock.withLock { field = value } }++ /**+ * Mapping between the overlay container tag and its default [OverlayAttributes].+ * It's to record the [OverlayAttributes] updated through [updateOverlayAttributes] and+ * report in [OverlayAttributesCalculatorParams].+ */+ @GuardedBy("globalLock")+ private val overlayTagToDefaultAttributesMap: MutableMap = ArrayMap()++ /**+ * Mapping between the overlay container tag and its current [OverlayAttributes] to provide+ * the [OverlayInfo] updates.+ */+ @GuardedBy("globalLock")+ private val overlayTagToCurrentAttributesMap = ArrayMap()++ @GuardedBy("globalLock")+ private val overlayTagToContainerMap = ArrayMap()++ /**+ * The mapping from [OverlayInfo] callback to [activityStacks][ActivityStack] callback.+ */+ @GuardedBy("globalLock")+ private val overlayInfoToActivityStackCallbackMap =+ ArrayMap, Consumer>>()++ init {+ WindowSdkExtensions.getInstance().requireExtensionVersion(6)++ embeddingExtension.setActivityStackAttributesCalculator { params ->+ globalLock.withLock {+ val parentContainerInfo = params.parentContainerInfo+ val density = DensityCompatHelper.getInstance()+ .density(parentContainerInfo.configuration, parentContainerInfo.windowMetrics)+ val windowMetrics = WindowMetricsCalculator.translateWindowMetrics(+ parentContainerInfo.windowMetrics, density+ )+ val overlayAttributes = calculateOverlayAttributes(+ params.activityStackTag,+ params.launchOptions.getOverlayAttributes(),+ WindowMetricsCalculator.translateWindowMetrics(+ params.parentContainerInfo.windowMetrics, density+ ),+ params.parentContainerInfo.configuration,+ ExtensionsWindowLayoutInfoAdapter.translate(+ windowMetrics,+ parentContainerInfo.windowLayoutInfo+ ),+ )+ return@setActivityStackAttributesCalculator overlayAttributes+ .toActivityStackAttributes(parentContainerInfo)+ }+ }++ embeddingExtension.registerActivityStackCallback(Runnable::run) { activityStacks ->+ globalLock.withLock {+ val lastOverlayTags = overlayTagToContainerMap.keys++ overlayTagToContainerMap.clear()+ overlayTagToContainerMap.putAll(+ activityStacks.getOverlayContainers().map { overlayContainer ->+ Pair(overlayContainer.tag!!, overlayContainer)+ }+ )++ cleanUpDismissedOverlayContainerRecords(lastOverlayTags)+ }+ }+ }++ /**+ * Clean up records associated with dismissed overlay [activityStacks][ActivityStack] when+ * there's a [ActivityStack] state update.+ *+ * The dismissed overlay [activityStacks][ActivityStack] are identified by comparing the+ * differences of [ActivityStack] state before and after update.+ *+ * @param lastOverlayTags Overlay containers' tag before applying [ActivityStack] state update.+ */+ @GuardedBy("globalLock")+ private fun cleanUpDismissedOverlayContainerRecords(lastOverlayTags: Set) {+ if (lastOverlayTags.isEmpty()) {+ // If there's no last overlay container, return.+ return+ }++ val dismissedOverlayTags = ArrayList()+ val currentOverlayTags = overlayTagToContainerMap.keys++ for (overlayTag in lastOverlayTags) {+ if (overlayTag !in currentOverlayTags &&+ // If an overlay activityStack is not in the current overlay container list, check+ // whether the activityStack does really not exist in WM Extensions in case+ // an overlay container is just launched, but th WM Jetpack hasn't received the+ // update yet.+ embeddingExtension.getActivityStackToken(overlayTag) == null+ ) {+ dismissedOverlayTags.add(overlayTag)+ }+ }++ for (overlayTag in dismissedOverlayTags) {+ overlayTagToDefaultAttributesMap.remove(overlayTag)+ overlayTagToCurrentAttributesMap.remove(overlayTag)+ }+ }++ /**+ * Calculates the [OverlayAttributes] to report to the [ActivityStackAttributes] calculator.+ *+ * The calculator then computes [ActivityStackAttributes] for rendering the overlay+ * [ActivityStack].+ *+ * @param tag The overlay [ActivityStack].+ * @param initialOverlayAttrs The [OverlayCreateParams.overlayAttributes] that used to launching+ * this overlay [ActivityStack]+ * @param windowMetrics The parent window container's [WindowMetrics]+ * @param configuration The parent window container's [Configuration]+ * @param windowLayoutInfo The parent window container's [WindowLayoutInfo]+ */+ @VisibleForTesting+ internal fun calculateOverlayAttributes(+ tag: String,+ initialOverlayAttrs: OverlayAttributes?,+ windowMetrics: WindowMetrics,+ configuration: Configuration,+ windowLayoutInfo: WindowLayoutInfo,+ ): OverlayAttributes {+ val defaultOverlayAttrs = getUpdatedOverlayAttributes(tag)+ ?: initialOverlayAttrs+ ?: throw IllegalArgumentException(+ "Can't retrieve overlay attributes from launch options"+ )+ val currentOverlayAttrs = overlayAttributesCalculator?.invoke(+ OverlayAttributesCalculatorParams(+ windowMetrics,+ configuration,+ windowLayoutInfo,+ tag,+ defaultOverlayAttrs,+ )+ ) ?: defaultOverlayAttrs++ overlayTagToCurrentAttributesMap[tag] = currentOverlayAttrs++ return currentOverlayAttrs+ }++ @VisibleForTesting+ internal open fun getUpdatedOverlayAttributes(overlayTag: String): OverlayAttributes? =+ overlayTagToDefaultAttributesMap[overlayTag]++ internal open fun updateOverlayAttributes(+ overlayTag: String,+ overlayAttributes: OverlayAttributes+ ) {+ globalLock.withLock {+ val activityStackToken = overlayTagToContainerMap[overlayTag]?.activityStackToken+ // Users may call this API before any callback coming. Try to ask platform if+ // this container exists.+ ?: embeddingExtension.getActivityStackToken(overlayTag)+ // Early return if there's no such ActivityStack associated with the tag.+ ?: return++ embeddingExtension.updateActivityStackAttributes(+ activityStackToken,+ overlayAttributes.toActivityStackAttributes(+ embeddingExtension.getParentContainerInfo(activityStackToken)!!+ )+ )++ // Update the tag-overlayAttributes map, which will be treated as the default+ // overlayAttributes in calculator.+ overlayTagToDefaultAttributesMap[overlayTag] = overlayAttributes+ overlayTagToCurrentAttributesMap[overlayTag] = overlayAttributes+ }+ }++ private fun OverlayAttributes.toActivityStackAttributes(+ parentContainerInfo: ParentContainerInfo+ ): ActivityStackAttributes = ActivityStackAttributes.Builder()+ .setRelativeBounds(+ EmbeddingBounds.translateEmbeddingBounds(+ bounds,+ adapter.translate(parentContainerInfo)+ ).toRect()+ ).setWindowAttributes(adapter.translateWindowAttributes())+ .build()++ private fun List.getOverlayContainers(): List =+ filter { activityStack -> activityStack.tag != null }.toList()++ open fun addOverlayInfoCallback(+ overlayTag: String,+ executor: Executor,+ overlayInfoCallback: JetpackConsumer,+ ) {+ globalLock.withLock {+ val callback = Consumer> { activityStacks ->+ val overlayInfoList = activityStacks.filter { activityStack ->+ activityStack.tag == overlayTag+ }+ if (overlayInfoList.size > 1) {+ throw IllegalStateException(+ "There must be at most one overlay ActivityStack with $overlayTag"+ )+ }+ val overlayInfo = if (overlayInfoList.isEmpty()) {+ OverlayInfo(+ overlayTag,+ currentOverlayAttributes = null,+ activityStack = null,+ )+ } else {+ overlayInfoList.first().toOverlayInfo()+ }+ overlayInfoCallback.accept(overlayInfo)+ }+ overlayInfoToActivityStackCallbackMap[overlayInfoCallback] = callback++ embeddingExtension.registerActivityStackCallback(executor, callback)+ }+ }++ private fun ActivityStack.toOverlayInfo(): OverlayInfo = OverlayInfo(+ tag!!,+ overlayTagToCurrentAttributesMap[tag!!],+ adapter.translate(this),+ )++ open fun removeOverlayInfoCallback(overlayInfoCallback: JetpackConsumer) {+ globalLock.withLock {+ val callback = overlayInfoToActivityStackCallbackMap.remove(overlayInfoCallback)+ if (callback != null) {+ embeddingExtension.unregisterActivityStackCallback(callback)+ }+ }+ }+}
@@ -0,0 +1,90 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++import java.util.UUID++/**+ * The parameter container to create an overlay [ActivityStack].+ *+ * If there's an shown overlay [ActivityStack] associated with the [tag], the existing+ * [ActivityStack] will be:+ * - Dismissed if the overlay [ActivityStack] is in different task from the launched one+ * - Updated with [OverlayAttributes] if the overlay [ActivityStack] is in the same task.+ *+ * See [android.os.Bundle.setOverlayCreateParams] for how to create an overlay [ActivityStack].+ *+ * @property tag The unique identifier of the overlay [ActivityStack], which will be generated+ * automatically if not specified.+ * @property overlayAttributes The attributes of the overlay [ActivityStack], which defaults to+ * the default value of [OverlayAttributes].+ * @constructor creates a parameter container to launch an overlay [ActivityStack].+ */+class OverlayCreateParams @JvmOverloads constructor(+ val tag: String = generateOverlayTag(),+ val overlayAttributes: OverlayAttributes = OverlayAttributes.Builder().build(),+) {+ override fun toString(): String =+ "${OverlayCreateParams::class.simpleName}:{ " ++ ", tag=$tag" ++ ", attrs=$overlayAttributes" ++ "}"++ /**+ * The [OverlayCreateParams] builder.+ */+ class Builder {+ private var tag: String? = null+ private var launchAttrs: OverlayAttributes? = null++ /**+ * Sets the overlay [ActivityStack]'s unique identifier. The builder will generate one+ * automatically if not specified.+ *+ * @param tag The unique identifier of the overlay [ActivityStack] to create.+ * @return The [OverlayCreateParams] builder.+ */+ fun setTag(tag: String): Builder = apply { this.tag = tag }++ /**+ * Sets the overlay [ActivityStack]'s attributes, which defaults to the default value of+ * [OverlayAttributes.Builder].+ *+ * @param attrs The [OverlayAttributes].+ * @return The [OverlayCreateParams] builder.+ */+ fun setOverlayAttributes(+ attrs: OverlayAttributes+ ): Builder = apply { launchAttrs = attrs }++ /** Builds the [OverlayCreateParams] */+ fun build(): OverlayCreateParams = OverlayCreateParams(+ tag ?: generateOverlayTag(),+ launchAttrs ?: OverlayAttributes.Builder().build()+ )+ }++ companion object {++ /**+ * A helper function to generate a random unique identifier.+ */+ @JvmStatic+ fun generateOverlayTag(): String =+ UUID.randomUUID().toString().substring(IntRange(0, 32))+ }+}
diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayInfo.kt b/window/window/src/main/java/androidx/window/embedding/OverlayInfo.kt
new file mode 100644
index 0000000..8f54e36
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/OverlayInfo.kt
@@ -0,0 +1,46 @@
+/*+ * Copyright (C) 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++import android.app.Activity++/**+ * Describes an overlay [ActivityStack] associated with [OverlayCreateParams.tag].+ */+class OverlayInfo internal constructor(+ /** The unique identifier associated with the overlay [ActivityStack]. */+ val overlayTag: String,+ /**+ * The [OverlayAttributes] of the overlay [ActivityStack] if it exists, or `null` if there's no+ * such a [ActivityStack]+ */+ val currentOverlayAttributes: OverlayAttributes?,+ /**+ * The overlay [ActivityStack] associated with [overlayTag], or `null` if there's no such a+ * [ActivityStack].+ */+ val activityStack: ActivityStack?+) {+ operator fun contains(activity: Activity): Boolean = activityStack?.contains(activity) ?: false++ override fun toString(): String =+ "${OverlayInfo::class.java.simpleName}: {" ++ "tag=$overlayTag" ++ ", currentOverlayAttrs=$currentOverlayAttributes" ++ ", activityStack=$activityStack" ++ "}"+}
@@ -0,0 +1,48 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */+package androidx.window.embedding++import android.content.res.Configuration+import androidx.core.view.WindowInsetsCompat+import androidx.window.core.Bounds+import androidx.window.layout.WindowLayoutInfo++/**+ * The parent container information directly passed from WM Extensions, which is created to+ * make test implementation easier.+ *+ * @property windowBounds The parent container's [Bounds].+ * @property windowLayoutInfo The parent container's [WindowLayoutInfo].+ * @property windowInsets The parent container's [WindowInsetsCompat].+ * @property configuration The parent container's [Configuration].+ * @property density The parent container's density in DP, which has the same unit as+ * [android.util.DisplayMetrics.density].+ */+internal data class ParentContainerInfo(+ /** The parent container's [Bounds]. */+ val windowBounds: Bounds,+ /** The parent container's [WindowLayoutInfo]. */+ val windowLayoutInfo: WindowLayoutInfo,+ /** The parent container's [WindowInsetsCompat] */+ val windowInsets: WindowInsetsCompat,+ /** The parent container's [Configuration]. */+ val configuration: Configuration,+ /**+ * The parent container's density in DP, which has the same unit as+ * [android.util.DisplayMetrics.density].+ */+ val density: Float,+)
@@ -19,8 +19,7 @@
import android.annotation.SuppressLint import androidx.annotation.FloatRange import androidx.annotation.IntRange-import androidx.annotation.RestrictTo-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP+import androidx.window.RequiresWindowSdkExtension import androidx.window.WindowSdkExtensions import androidx.window.core.SpecificationComputer.Companion.startSpecification import androidx.window.core.VerificationMode@@ -40,6 +39,9 @@
* - Animation background color — The color of the background during * animation of the split involving this `SplitAttributes` object if the * animation requires a background+ * - Divider attributes — Specifies whether a divider is needed between+ * the split containers and the properties of the divider, including the+ * color, the width, whether the divider is draggable, etc. * * Attributes can be configured by: * - Setting the default `SplitAttributes` using@@ -49,28 +51,33 @@
* `animationBackgroundColor` attributes in `` or * `` tags in an XML configuration file. The * attributes are parsed as [SplitType], [LayoutDirection], and- * [BackgroundColor], respectively. Note that [SplitType.HingeSplitType]+ * [EmbeddingAnimationBackground], respectively. Note that [SplitType.HingeSplitType] * is not supported XML format.- * - Set `SplitAttributes` calculation function by- * [SplitController.setSplitAttributesCalculator]- * to customize the `SplitAttributes` for a given device and window state.+ * - Using+ * [SplitAttributesCalculator.computeSplitAttributesForParams] to customize+ * the `SplitAttributes` for a given device and window state.+ *+ * @property splitType The split type attribute. Defaults to an equal split of the parent window for+ * the primary and secondary containers.+ * @property layoutDirection The layout direction of the parent window split. The default is based+ * on locale value.+ * @property animationBackground The animation background to use during the animation of the split+ * involving this `SplitAttributes` object if the animation requires a background. The default is to+ * use the current theme window background color.+ * @property dividerAttributes The [DividerAttributes] for this split. Defaults to+ * [DividerAttributes.NO_DIVIDER], which means no divider is requested. * * @see SplitAttributes.SplitType * @see SplitAttributes.LayoutDirection+ * @see EmbeddingAnimationBackground+ * @see EmbeddingAnimationBackground.createColorBackground+ * @see EmbeddingAnimationBackground.DEFAULT */-class SplitAttributes @RestrictTo(LIBRARY_GROUP) constructor(-- /**- * The split type attribute. Defaults to an equal split of the parent window- * for the primary and secondary containers.- */+class SplitAttributes @JvmOverloads constructor( val splitType: SplitType = SPLIT_TYPE_EQUAL,-- /**- * The layout direction attribute for the parent window split. The default- * is based on locale.- */ val layoutDirection: LayoutDirection = LOCALE,+ val animationBackground: EmbeddingAnimationBackground = EmbeddingAnimationBackground.DEFAULT,+ val dividerAttributes: DividerAttributes = DividerAttributes.NO_DIVIDER, ) { /**@@ -365,6 +372,8 @@
override fun hashCode(): Int { var result = splitType.hashCode() result = result * 31 + layoutDirection.hashCode()+ result = result * 31 + animationBackground.hashCode()+ result = result * 31 + dividerAttributes.hashCode() return result }@@ -380,7 +389,9 @@
if (this === other) return true if (other !is SplitAttributes) return false return splitType == other.splitType &&- layoutDirection == other.layoutDirection+ layoutDirection == other.layoutDirection &&+ animationBackground == other.animationBackground &&+ dividerAttributes == other.dividerAttributes } /**@@ -390,7 +401,9 @@
*/ override fun toString(): String = "${SplitAttributes::class.java.simpleName}:" +- "{splitType=$splitType, layoutDir=$layoutDirection }"+ "{splitType=$splitType, layoutDir=$layoutDirection, " ++ "animationBackground=$animationBackground, " ++ "dividerAttributes=$dividerAttributes }" /** * Builder for creating an instance of [SplitAttributes].@@ -400,10 +413,13 @@
* - The default layout direction is based on locale. * - The default animation background color is to use the current theme * window background color.+ * - The default divider attributes is not to use divider. */ class Builder { private var splitType = SPLIT_TYPE_EQUAL private var layoutDirection = LOCALE+ private var animationBackground = EmbeddingAnimationBackground.DEFAULT+ private var dividerAttributes: DividerAttributes = DividerAttributes.NO_DIVIDER /** * Sets the split type attribute.@@ -432,11 +448,39 @@
apply { this.layoutDirection = layoutDirection } /**+ * Sets the animation background to use during animation of the split involving this+ * `SplitAttributes` object if the animation requires a background.+ *+ * The default is [EmbeddingAnimationBackground.DEFAULT], which means to use the+ * current theme window background color.+ *+ * The [EmbeddingAnimationBackground] can be supported only if the vendor API level of+ * the target device is equals or higher than required API level. Otherwise, it would be+ * no-op when setting the [EmbeddingAnimationBackground] on a target device that has lower+ * API level.+ *+ * @param background The animation background.+ * @return This `Builder`.+ *+ * @see EmbeddingAnimationBackground.createColorBackground+ * @see EmbeddingAnimationBackground.DEFAULT+ */+ @RequiresWindowSdkExtension(5)+ fun setAnimationBackground(background: EmbeddingAnimationBackground): Builder =+ apply { animationBackground = background }++ /** Sets the [DividerAttributes]. */+ @RequiresWindowSdkExtension(6)+ fun setDividerAttributes(dividerAttributes: DividerAttributes): Builder =+ apply { this.dividerAttributes = dividerAttributes }++ /** * Builds a `SplitAttributes` instance with the attributes specified by- * [setSplitType] and [setLayoutDirection].+ * [setSplitType], [setLayoutDirection], and [setAnimationBackground]. * * @return The new `SplitAttributes` instance. */- fun build(): SplitAttributes = SplitAttributes(splitType, layoutDirection)+ fun build(): SplitAttributes = SplitAttributes(splitType, layoutDirection,+ animationBackground, dividerAttributes) } }
@@ -22,7 +22,6 @@
import androidx.window.RequiresWindowSdkExtension import androidx.window.WindowProperties import androidx.window.WindowSdkExtensions-import androidx.window.core.ExperimentalWindowApi import androidx.window.layout.WindowMetrics import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow@@ -86,6 +85,55 @@
get() = embeddingBackend.splitSupportStatus /**+ * Pins the top-most [ActivityStack] to keep the stack of the Activities to be always+ * positioned on top. The rest of the activities in the Task will be split with the pinned+ * [ActivityStack]. The pinned [ActivityStack] would also have isolated activity+ * navigation in which only the activities that are started from the pinned+ * [ActivityStack] can be added on top of the [ActivityStack].+ *+ * The pinned [ActivityStack] is unpinned whenever the pinned [ActivityStack] is expanded.+ * Use [SplitPinRule.Builder.setSticky] if the same [ActivityStack] should be pinned+ * again whenever the [ActivityStack] is on top and split with another [ActivityStack] again.+ *+ * The caller **must** make sure if [WindowSdkExtensions.extensionVersion] is greater than+ * or equal to 5.+ *+ * @param taskId The id of the Task that top [ActivityStack] should be pinned.+ * @param splitPinRule The SplitRule that specifies how the top [ActivityStack] should+ * be split with others.+ * @return Returns `true` if the top [ActivityStack] is successfully pinned.+ * Otherwise, `false`. Few examples are:+ * 1. There's no [ActivityStack].+ * 2. There is already an existing pinned [ActivityStack].+ * 3. There's no other [ActivityStack] to split with the top [ActivityStack].+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]+ * is less than 5.+ */+ @RequiresWindowSdkExtension(5)+ fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean {+ return embeddingBackend.pinTopActivityStack(taskId, splitPinRule)+ }++ /**+ * Unpins the pinned [ActivityStack]. The [ActivityStack] will still be the+ * top-most [ActivityStack] right after unpinned, and the [ActivityStack] could+ * be expanded or continue to be split with the next top [ActivityStack] if the current+ * state matches any of the existing [SplitPairRule]. It is a no-op call if the task+ * does not have a pinned [ActivityStack].+ *+ * The caller **must** make sure if [WindowSdkExtensions.extensionVersion] is greater than+ * or equal to 5.+ *+ * @param taskId The id of the Task that top [ActivityStack] should be unpinned.+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]+ * is less than 5.+ */+ @RequiresWindowSdkExtension(5)+ fun unpinTopActivityStack(taskId: Int) {+ embeddingBackend.unpinTopActivityStack(taskId)+ }++ /** * Sets or replaces the previously registered [SplitAttributes] calculator. * * **Note** that it's callers' responsibility to check if this API is supported by checking@@ -150,27 +198,6 @@
} /**- * Triggers a [SplitAttributes] update callback for the current topmost and visible split layout- * if there is one. This method can be used when a change to the split presentation originates- * from an application state change. Changes that are driven by parent window changes or new- * activity starts invoke the callback provided in [setSplitAttributesCalculator] automatically- * without the need to call this function.- *- * The top [SplitInfo] is usually the last element of [SplitInfo] list which was received from- * the callback registered in [splitInfoList].- *- * The call will be ignored if there is no visible split.- *- * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]- * is less than 3.- */- @ExperimentalWindowApi- @RequiresWindowSdkExtension(3)- fun invalidateTopVisibleSplitAttributes() {- embeddingBackend.invalidateTopVisibleSplitAttributes()- }-- /** * Updates the [SplitAttributes] of a split pair. This is an alternative to using * a split attributes calculator callback set in [setSplitAttributesCalculator], useful when * apps only need to update the splits in a few cases proactively but rely on the default split@@ -185,15 +212,15 @@
* - A new Activity being launched. * - A window or device state updates (e,g. due to screen rotation or folding state update). *- * In most cases it is suggested to use [invalidateTopVisibleSplitAttributes] if- * [SplitAttributes] calculator callback is used.+ * In most cases it is suggested to use+ * [ActivityEmbeddingController.invalidateTopVisibleActivityStacks] if a calculator has been set+ * through [setSplitAttributesCalculator]. * * @param splitInfo the split pair to update * @param splitAttributes the [SplitAttributes] to be applied * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] * is less than 3. */- @ExperimentalWindowApi @RequiresWindowSdkExtension(3) fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes) { embeddingBackend.updateSplitAttributes(splitInfo, splitAttributes)
@@ -19,10 +19,13 @@
import android.app.Activity import android.os.IBinder import androidx.annotation.RestrictTo-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP+import androidx.window.RequiresWindowSdkExtension+import androidx.window.WindowSdkExtensions+import androidx.window.extensions.embedding.SplitInfo.Token /** Describes a split pair of two containers with activities. */-class SplitInfo @RestrictTo(LIBRARY_GROUP) constructor(+@Suppress("Deprecation") // To compat with device with version 3 and 4.+class SplitInfo private constructor( /** * The [ActivityStack] representing the primary split container. */@@ -33,11 +36,79 @@
val secondaryActivityStack: ActivityStack, /** The [SplitAttributes] of this split pair. */ val splitAttributes: SplitAttributes,++ @Deprecated(+ message = "Use [token] instead",+ replaceWith = ReplaceWith(+ expression = "SplitInfo.token",+ imports = arrayOf("androidx.window.embedding.SplitInfo"),+ )+ )+ private val binder: IBinder?, /** * A token uniquely identifying this `SplitInfo`. */- internal val token: IBinder,+ private val token: Token?, ) {+ @RequiresWindowSdkExtension(5)+ internal constructor(+ primaryActivityStack: ActivityStack,+ secondaryActivityStack: ActivityStack,+ splitAttributes: SplitAttributes,+ token: Token,+ ) : this(primaryActivityStack, secondaryActivityStack, splitAttributes, binder = null, token)++ /**+ * Creates SplitInfo for [WindowSdkExtensions.extensionVersion] 3 and 4.+ */+ @RequiresWindowSdkExtension(3)+ internal constructor(+ primaryActivityStack: ActivityStack,+ secondaryActivityStack: ActivityStack,+ splitAttributes: SplitAttributes,+ binder: IBinder,+ ) : this(+ primaryActivityStack,+ secondaryActivityStack,+ splitAttributes,+ binder,+ token = null,+ ) {+ WindowSdkExtensions.getInstance().requireExtensionVersion(3..4)+ }++ /**+ * Creates SplitInfo ONLY for testing.+ *+ * @param primaryActivityStack the [ActivityStack] representing the primary split container.+ * @param secondaryActivityStack the [ActivityStack] representing the secondary split container.+ * @param splitAttributes the [SplitAttributes] of this split pair.+ */+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)+ constructor(+ primaryActivityStack: ActivityStack,+ secondaryActivityStack: ActivityStack,+ splitAttributes: SplitAttributes,+ ) : this(+ primaryActivityStack,+ secondaryActivityStack,+ splitAttributes,+ binder = null,+ token = null,+ )++ @RequiresWindowSdkExtension(3)+ internal fun getBinder(): IBinder = let {+ WindowSdkExtensions.getInstance().requireExtensionVersion(3..4)+ requireNotNull(binder)+ }++ @RequiresWindowSdkExtension(5)+ internal fun getToken(): Token = let {+ WindowSdkExtensions.getInstance().requireExtensionVersion(5)+ requireNotNull(token)+ }+ /** * Whether the [primaryActivityStack] or the [secondaryActivityStack] in this [SplitInfo] * contains the [activity].@@ -55,6 +126,7 @@
if (secondaryActivityStack != other.secondaryActivityStack) return false if (splitAttributes != other.splitAttributes) return false if (token != other.token) return false+ if (binder != other.binder) return false return true }@@ -64,6 +136,7 @@
result = 31 * result + secondaryActivityStack.hashCode() result = 31 * result + splitAttributes.hashCode() result = 31 * result + token.hashCode()+ result = 31 * result + binder.hashCode() return result }@@ -73,7 +146,12 @@
append("primaryActivityStack=$primaryActivityStack, ") append("secondaryActivityStack=$secondaryActivityStack, ") append("splitAttributes=$splitAttributes, ")- append("token=$token")+ if (token != null) {+ append("token=$token")+ }+ if (binder != null) {+ append("binder=$binder")+ } append("}") } }
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPinRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPinRule.kt
new file mode 100644
index 0000000..d4dc97f
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPinRule.kt
@@ -0,0 +1,234 @@
+/*+ * Copyright 2023 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.embedding++import androidx.annotation.IntRange+import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT+import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT+import androidx.window.embedding.SplitRule.Companion.SPLIT_MIN_DIMENSION_ALWAYS_ALLOW+import androidx.window.embedding.SplitRule.Companion.SPLIT_MIN_DIMENSION_DP_DEFAULT++/**+ * Split configuration rules for pinning an [ActivityStack]. Define how the pinned [ActivityStack]+ * should be displayed side-by-side with the other [ActivityStack].+ */+class SplitPinRule internal constructor(+ /**+ * A unique string to identify this [SplitPinRule].+ */+ tag: String? = null,+ /**+ * The default [SplitAttributes] to apply on the pin container and the paired container.+ */+ defaultSplitAttributes: SplitAttributes,+ /**+ * Whether this rule should be sticky. If the value is `false`, this rule be removed whenever+ * the pinned [ActivityStack] is unpinned. Set to `true` if the rule should be applied+ * whenever once again possible (e.g. the host Task bounds satisfies the size and aspect ratio+ * requirements).+ */+ val isSticky: Boolean,++ @IntRange(from = 0) minWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,+ @IntRange(from = 0) minHeightDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,+ @IntRange(from = 0) minSmallestWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,+ maxAspectRatioInPortrait: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT,+ maxAspectRatioInLandscape: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT+) : SplitRule(tag, minWidthDp, minHeightDp, minSmallestWidthDp, maxAspectRatioInPortrait,+ maxAspectRatioInLandscape, defaultSplitAttributes) {++ /**+ * Builder for [SplitPinRule].+ *+ */+ class Builder {+ private var tag: String? = null+ @IntRange(from = 0)+ private var minWidthDp = SPLIT_MIN_DIMENSION_DP_DEFAULT+ @IntRange(from = 0)+ private var minHeightDp = SPLIT_MIN_DIMENSION_DP_DEFAULT+ @IntRange(from = 0)+ private var minSmallestWidthDp = SPLIT_MIN_DIMENSION_DP_DEFAULT+ private var maxAspectRatioInPortrait = SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT+ private var maxAspectRatioInLandscape = SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT+ private var defaultSplitAttributes = SplitAttributes.Builder().build()+ private var isSticky: Boolean = false++ /**+ * Sets the smallest value of width of the parent window when the split should be used, in+ * DP.+ * When the window size is smaller than requested here, activities in the secondary+ * container will be stacked on top of the activities in the primary one, completely+ * overlapping them.+ *+ * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.+ * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.+ *+ * @param minWidthDp the smallest value of width of the parent window when the split should+ * be used, in DP.+ */+ fun setMinWidthDp(@IntRange(from = 0) minWidthDp: Int): Builder =+ apply { this.minWidthDp = minWidthDp }++ /**+ * Sets the smallest value of height of the parent task window when the split should be+ * used, in DP. When the window size is smaller than requested here, activities in the+ * secondary container will be stacked on top of the activities in the primary one,+ * completely overlapping them.+ *+ * It is useful if it's necessary to split the parent window horizontally for this+ * [SplitPinRule].+ *+ * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.+ * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.+ *+ * @param minHeightDp the smallest value of height of the parent task window when the split+ * should be used, in DP.+ *+ * @see SplitAttributes.LayoutDirection.TOP_TO_BOTTOM+ * @see SplitAttributes.LayoutDirection.BOTTOM_TO_TOP+ */+ fun setMinHeightDp(@IntRange(from = 0) minHeightDp: Int): Builder =+ apply { this.minHeightDp = minHeightDp }++ /**+ * Sets the smallest value of the smallest possible width of the parent window in any+ * rotation when the split should be used, in DP. When the window size is smaller than+ * requested here, activities in the secondary container will be stacked on top of the+ * activities in the primary one, completely overlapping them.+ *+ * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.+ * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.+ *+ * @param minSmallestWidthDp the smallest value of the smallest possible width of the parent+ * window in any rotation when the split should be used, in DP.+ */+ fun setMinSmallestWidthDp(@IntRange(from = 0) minSmallestWidthDp: Int): Builder =+ apply { this.minSmallestWidthDp = minSmallestWidthDp }++ /**+ * Sets the largest value of the aspect ratio, expressed as `height / width` in decimal+ * form, of the parent window bounds in portrait when the split should be used. When the+ * window aspect ratio is greater than requested here, activities in the secondary container+ * will be stacked on top of the activities in the primary one, completely overlapping them.+ *+ * This value is only used when the parent window is in portrait (height >= width).+ *+ * The default is [SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT] if the app doesn't set, which is+ * the recommend value to only allow split when the parent window is not too stretched in+ * portrait.+ *+ * @param aspectRatio the largest value of the aspect ratio, expressed as `height / width`+ * in decimal form, of the parent window bounds in portrait when the split should be used.+ *+ * @see EmbeddingAspectRatio.ratio+ * @see EmbeddingAspectRatio.ALWAYS_ALLOW+ * @see EmbeddingAspectRatio.ALWAYS_DISALLOW+ */+ fun setMaxAspectRatioInPortrait(aspectRatio: EmbeddingAspectRatio): Builder =+ apply { this.maxAspectRatioInPortrait = aspectRatio }++ /**+ * Sets the largest value of the aspect ratio, expressed as `width / height` in decimal+ * form, of the parent window bounds in landscape when the split should be used. When the+ * window aspect ratio is greater than requested here, activities in the secondary container+ * will be stacked on top of the activities in the primary one, completely overlapping them.+ *+ * This value is only used when the parent window is in landscape (width > height).+ *+ * The default is [SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT] if the app doesn't set, which+ * is the recommend value to always allow split when the parent window is in landscape.+ *+ * @param aspectRatio the largest value of the aspect ratio, expressed as `width / height`+ * in decimal form, of the parent window bounds in landscape when the split should be used.+ *+ * @see EmbeddingAspectRatio.ratio+ * @see EmbeddingAspectRatio.ALWAYS_ALLOW+ * @see EmbeddingAspectRatio.ALWAYS_DISALLOW+ */+ fun setMaxAspectRatioInLandscape(aspectRatio: EmbeddingAspectRatio): Builder =+ apply { this.maxAspectRatioInLandscape = aspectRatio }++ /**+ * Sets the default [SplitAttributes] to apply on the activity containers pair when the host+ * task bounds satisfy [minWidthDp], [minHeightDp], [minSmallestWidthDp],+ * [maxAspectRatioInPortrait] and [maxAspectRatioInLandscape] requirements.+ *+ * @param defaultSplitAttributes the default [SplitAttributes] to apply on the activity+ * containers pair when the host task bounds satisfy all the rule requirements.+ */+ fun setDefaultSplitAttributes(defaultSplitAttributes: SplitAttributes): Builder =+ apply { this.defaultSplitAttributes = defaultSplitAttributes }++ /**+ * Sets a unique string to identify this [SplitPinRule], which defaults to `null`.+ * The suggested usage is to set the tag to be able to differentiate between different rules+ * in the [SplitAttributesCalculatorParams.splitRuleTag].+ *+ * @param tag unique string to identify this [SplitPinRule].+ */+ fun setTag(tag: String?): Builder =+ apply { this.tag = tag }++ /**+ * Sets this rule to be sticky.+ * @see isSticky+ *+ * @param isSticky whether to be a sticky rule.+ */+ fun setSticky(isSticky: Boolean): Builder =+ apply { this.isSticky = isSticky }++ /**+ * Builds a [SplitPinRule] instance.+ *+ * @return The new [SplitPinRule] instance.+ */+ fun build() = SplitPinRule(+ tag,+ defaultSplitAttributes,+ isSticky,+ minWidthDp,+ minHeightDp,+ minSmallestWidthDp,+ maxAspectRatioInPortrait,+ maxAspectRatioInLandscape+ )+ }++ override fun equals(other: Any?): Boolean {+ if (this === other) return true+ if (other !is SplitPinRule) return false++ if (!super.equals(other)) return false+ if (isSticky != other.isSticky) return false+ return true+ }++ override fun hashCode(): Int {+ var result = super.hashCode()+ result = 31 * result + isSticky.hashCode()+ return result+ }++ override fun toString(): String =+ "${SplitPinRule::class.java.simpleName}{" ++ "tag=$tag" ++ ", defaultSplitAttributes=$defaultSplitAttributes" ++ ", isSticky=$isSticky" ++ "}"+}
@@ -133,6 +133,8 @@
* The default [SplitAttributes] to apply on the activity containers pair when the host task * bounds satisfy [minWidthDp], [minHeightDp], [minSmallestWidthDp], * [maxAspectRatioInPortrait] and [maxAspectRatioInLandscape] requirements.+ *+ * It is set to split the host parent task vertically and equally by default. */ val defaultSplitAttributes: SplitAttributes, ) : EmbeddingRule(tag) {
@@ -30,10 +30,13 @@
import androidx.window.reflection.ReflectionUtils.doesReturn import androidx.window.reflection.ReflectionUtils.isPublic import androidx.window.reflection.ReflectionUtils.validateReflection+import androidx.window.reflection.WindowExtensionsConstants.DISPLAY_FOLD_FEATURE_CLASS import androidx.window.reflection.WindowExtensionsConstants.FOLDING_FEATURE_CLASS import androidx.window.reflection.WindowExtensionsConstants.JAVA_CONSUMER+import androidx.window.reflection.WindowExtensionsConstants.SUPPORTED_WINDOW_FEATURES_CLASS import androidx.window.reflection.WindowExtensionsConstants.WINDOW_CONSUMER import androidx.window.reflection.WindowExtensionsConstants.WINDOW_LAYOUT_COMPONENT_CLASS+import java.lang.reflect.ParameterizedType /** * Reflection Guard for [WindowLayoutComponent].@@ -63,13 +66,12 @@
if (!isWindowLayoutComponentAccessible()) { return false }- // TODO(b/267831038): can fallback to VendorApiLevel1 when level2 is not match- // but level 1 is matched- return when (ExtensionsUtil.safeVendorApiLevel) {- 1 -> hasValidVendorApiLevel1()- in 2..Int.MAX_VALUE -> hasValidVendorApiLevel2()- // TODO(b/267956499): add hasValidVendorApiLevel3- else -> false+ val vendorApiLevel = ExtensionsUtil.safeVendorApiLevel+ return when {+ vendorApiLevel < 1 -> false+ vendorApiLevel == 1 -> hasValidVendorApiLevel1()+ vendorApiLevel < 5 -> hasValidVendorApiLevel2()+ else -> hasValidVendorApiLevel6() } }@@ -100,8 +102,17 @@
return hasValidVendorApiLevel1() && isMethodWindowLayoutInfoListenerWindowConsumerValid() }+ @VisibleForTesting+ internal fun hasValidVendorApiLevel6(): Boolean {+ return hasValidVendorApiLevel2() &&+ isDisplayFoldFeatureValid() &&+ isSupportedWindowFeaturesValid() &&+ isGetSupportedWindowFeaturesValid()+ }+ private fun isWindowLayoutProviderValid(): Boolean {- return validateReflection("WindowExtensions#getWindowLayoutComponent is not valid") {+ return validateReflection(+ "WindowExtensions#getWindowLayoutComponent is not valid") { val extensionsClass = safeWindowExtensionsProvider.windowExtensionsClass val getWindowLayoutComponentMethod = extensionsClass.getMethod("getWindowLayoutComponent")@@ -164,6 +175,63 @@
} }+ private fun isDisplayFoldFeatureValid(): Boolean {+ return validateReflection("DisplayFoldFeature is not valid") {+ val displayFoldFeatureClass = displayFoldFeatureClass++ val getTypeMethod = displayFoldFeatureClass.getMethod("getType")+ val hasPropertyMethod = displayFoldFeatureClass+ .getMethod("hasProperty", Int::class.java)+ val hasPropertiesMethod = displayFoldFeatureClass+ .getMethod("hasProperties", IntArray::class.java)++ getTypeMethod.isPublic &&+ getTypeMethod.doesReturn(Int::class.java) &&+ hasPropertyMethod.isPublic &&+ hasPropertyMethod.doesReturn(Boolean::class.java) &&+ hasPropertiesMethod.isPublic &&+ hasPropertiesMethod.doesReturn(Boolean::class.java)+ }+ }++ private fun isSupportedWindowFeaturesValid(): Boolean {+ return validateReflection("SupportedWindowFeatures is not valid") {+ val supportedWindowFeaturesClass = supportedWindowFeaturesClass++ val getDisplayFoldFeaturesMethod = supportedWindowFeaturesClass+ .getMethod("getDisplayFoldFeatures")+ val returnTypeGeneric =+ (getDisplayFoldFeaturesMethod.genericReturnType as ParameterizedType)+ .actualTypeArguments[0] as Class<*>++ getDisplayFoldFeaturesMethod.isPublic &&+ getDisplayFoldFeaturesMethod.doesReturn(List::class.java) &&+ returnTypeGeneric == displayFoldFeatureClass+ }+ }++ private fun isGetSupportedWindowFeaturesValid(): Boolean {+ return validateReflection(+ "WindowLayoutComponent#getSupportedWindowFeatures is not valid") {+ val windowLayoutComponent = windowLayoutComponentClass+ val getSupportedWindowFeaturesMethod = windowLayoutComponent+ .getMethod("getSupportedWindowFeatures")++ getSupportedWindowFeaturesMethod.isPublic &&+ getSupportedWindowFeaturesMethod.doesReturn(supportedWindowFeaturesClass)+ }+ }++ private val displayFoldFeatureClass: Class<*>+ get() {+ return loader.loadClass(DISPLAY_FOLD_FEATURE_CLASS)+ }++ private val supportedWindowFeaturesClass: Class<*>+ get() {+ return loader.loadClass(SUPPORTED_WINDOW_FEATURES_CLASS)+ }+ private val foldingFeatureClass: Class<*> get() { return loader.loadClass(FOLDING_FEATURE_CLASS)
diff --git a/window/window/src/main/java/androidx/window/layout/SupportedPosture.kt b/window/window/src/main/java/androidx/window/layout/SupportedPosture.kt
new file mode 100644
index 0000000..167566a
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/SupportedPosture.kt
@@ -0,0 +1,52 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.layout++/**+ * A class to represent a posture that the device supports.+ */+class SupportedPosture internal constructor(private val rawValue: Int) {++ override fun toString(): String {+ return when (this) {+ TABLETOP -> "TABLETOP"+ else -> "UNKNOWN"+ }+ }++ override fun equals(other: Any?): Boolean {+ if (this === other) return true+ if (other == null) return false+ if (other::class != SupportedPosture::class) return false++ other as SupportedPosture++ return rawValue == other.rawValue+ }++ override fun hashCode(): Int {+ return rawValue+ }++ companion object {+ /**+ * The posture where there is a single fold in the half-opened state.+ */+ @JvmField+ val TABLETOP: SupportedPosture = SupportedPosture(0)+ }+}
@@ -23,6 +23,7 @@
import androidx.annotation.RestrictTo import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP import androidx.annotation.UiContext+import androidx.window.RequiresWindowSdkExtension import androidx.window.WindowSdkExtensions import androidx.window.core.ConsumerAdapter import androidx.window.layout.adapter.WindowBackend@@ -97,6 +98,20 @@
*/ fun windowLayoutInfo(activity: Activity): Flow+ /**+ * Returns the [List] of [SupportedPosture] values. This value will not change during runtime.+ * These values are for determining if the device supports the given [SupportedPosture] but does+ * not mean the device is in the given [SupportedPosture]. Use [windowLayoutInfo] to determine+ * the current state of the [DisplayFeature]'s on the device.+ * @see windowLayoutInfo+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less than+ * 6.+ * @throws NotImplementedError if a derived test class does not override this method.+ */+ @RequiresWindowSdkExtension(version = 6)+ val supportedPostures: List+ get() { throw NotImplementedError("Method was not implemented.") }+ companion object { private val DEBUG = false@@ -133,7 +148,11 @@
@JvmStatic fun getOrCreate(context: Context): WindowInfoTracker { val backend = extensionBackend ?: SidecarWindowBackend.getInstance(context)- val repo = WindowInfoTrackerImpl(WindowMetricsCalculatorCompat, backend)+ val repo = WindowInfoTrackerImpl(+ WindowMetricsCalculatorCompat(),+ backend,+ WindowSdkExtensions.getInstance()+ ) return decorator.decorate(repo) }
@@ -15,42 +15,20 @@
*/ package androidx.window.layout-import android.annotation.SuppressLint import android.app.Activity import android.content.Context-import android.content.res.Configuration-import android.graphics.Point-import android.graphics.Rect import android.inputmethodservice.InputMethodService-import android.os.Build-import android.os.Build.VERSION_CODES-import android.util.Log-import android.view.Display-import android.view.DisplayCutout-import android.view.WindowManager-import androidx.annotation.RequiresApi import androidx.annotation.UiContext-import androidx.annotation.VisibleForTesting import androidx.core.view.WindowInsetsCompat-import androidx.window.core.Bounds-import androidx.window.layout.util.ActivityCompatHelperApi24.isInMultiWindowMode-import androidx.window.layout.util.ContextCompatHelper.unwrapUiContext-import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowBounds-import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowInsets-import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowMetrics-import androidx.window.layout.util.ContextCompatHelperApi30.maximumWindowBounds-import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetBottom-import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetLeft-import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetRight-import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetTop-import java.lang.reflect.InvocationTargetException+import androidx.window.layout.util.DensityCompatHelper+import androidx.window.layout.util.WindowMetricsCompatHelper /** * Helper class used to compute window metrics across Android versions. */-internal object WindowMetricsCalculatorCompat : WindowMetricsCalculator {-- private val TAG: String = WindowMetricsCalculatorCompat::class.java.simpleName+internal class WindowMetricsCalculatorCompat(+ private val densityCompatHelper: DensityCompatHelper = DensityCompatHelper.getInstance()+) : WindowMetricsCalculator { /** * Computes the current [WindowMetrics] for a given [Context]. The context can be either@@ -59,33 +37,8 @@
* @see WindowMetricsCalculator.computeCurrentWindowMetrics */ override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {- // TODO(b/259148796): Make WindowMetricsCalculatorCompat more testable- if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {- return currentWindowMetrics(context)- } else {- when (val unwrappedContext = unwrapUiContext(context)) {- is Activity -> {- return computeCurrentWindowMetrics(unwrappedContext)- }- is InputMethodService -> {- val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager-- // On older SDK levels, the app and IME could show up on different displays.- // However, there isn't a way for us to figure this out from the application- // layer. But, this should be good enough for now given the small likelihood of- // IMEs showing up on non-primary displays on these SDK levels.- @Suppress("DEPRECATION")- val displaySize = getRealSizeForDisplay(wm.defaultDisplay)-- // IME occupies the whole display bounds.- val imeBounds = Rect(0, 0, displaySize.x, displaySize.y)- return WindowMetrics(imeBounds)- }- else -> {- throw IllegalArgumentException("$context is not a UiContext")- }- }- }+ return WindowMetricsCompatHelper.getInstance()+ .currentWindowMetrics(context, densityCompatHelper) } /**@@ -93,24 +46,8 @@
* @see WindowMetricsCalculator.computeCurrentWindowMetrics */ override fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics {- val bounds = if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {- currentWindowBounds(activity)- } else if (Build.VERSION.SDK_INT >= VERSION_CODES.Q) {- computeWindowBoundsQ(activity)- } else if (Build.VERSION.SDK_INT >= VERSION_CODES.P) {- computeWindowBoundsP(activity)- } else if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {- computeWindowBoundsN(activity)- } else {- computeWindowBoundsIceCreamSandwich(activity)- }- // TODO (b/233899790): compute insets for other platform versions below R- val windowInsetsCompat = if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {- computeWindowInsetsCompat(activity)- } else {- WindowInsetsCompat.Builder().build()- }- return WindowMetrics(Bounds(bounds), windowInsetsCompat)+ return WindowMetricsCompatHelper.getInstance()+ .currentWindowMetrics(activity, densityCompatHelper) } /**@@ -118,7 +55,8 @@
* @see WindowMetricsCalculator.computeMaximumWindowMetrics */ override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {- return computeMaximumWindowMetrics(activity as Context)+ return WindowMetricsCompatHelper.getInstance()+ .maximumWindowMetrics(activity, densityCompatHelper) } /**@@ -126,305 +64,8 @@
* @See WindowMetricsCalculator.computeMaximumWindowMetrics */ override fun computeMaximumWindowMetrics(@UiContext context: Context): WindowMetrics {- // TODO(b/259148796): Make WindowMetricsCalculatorCompat more testable- val bounds = if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {- maximumWindowBounds(context)- } else {- val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager- // [WindowManager#getDefaultDisplay] is deprecated but we have this for- // compatibility with older versions, as we can't reliably get the display associated- // with a Context through public APIs either.- @Suppress("DEPRECATION")- val display = wm.defaultDisplay- val displaySize = getRealSizeForDisplay(display)- Rect(0, 0, displaySize.x, displaySize.y)- }- // TODO (b/233899790): compute insets for other platform versions below R- val windowInsetsCompat = if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {- computeWindowInsetsCompat(context)- } else {- WindowInsetsCompat.Builder().build()- }- return WindowMetrics(Bounds(bounds), windowInsetsCompat)- }-- /** Computes the window bounds for [Build.VERSION_CODES.Q]. */- @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")- @RequiresApi(VERSION_CODES.Q)- internal fun computeWindowBoundsQ(activity: Activity): Rect {- var bounds: Rect- val config = activity.resources.configuration- try {- val windowConfigField =- Configuration::class.java.getDeclaredField("windowConfiguration")- windowConfigField.isAccessible = true- val windowConfig = windowConfigField[config]- val getBoundsMethod = windowConfig.javaClass.getDeclaredMethod("getBounds")- bounds = Rect(getBoundsMethod.invoke(windowConfig) as Rect)- } catch (e: NoSuchFieldException) {- Log.w(TAG, e)- // If reflection fails for some reason default to the P implementation which still- // has the ability to account for display cutouts.- bounds = computeWindowBoundsP(activity)- } catch (e: NoSuchMethodException) {- Log.w(TAG, e)- bounds = computeWindowBoundsP(activity)- } catch (e: IllegalAccessException) {- Log.w(TAG, e)- bounds = computeWindowBoundsP(activity)- } catch (e: InvocationTargetException) {- Log.w(TAG, e)- bounds = computeWindowBoundsP(activity)- }- return bounds- }-- /**- * Computes the window bounds for [Build.VERSION_CODES.P].- *- *- * NOTE: This method may result in incorrect values if the [android.content.res.Resources]- * value stored at 'navigation_bar_height' does not match the true navigation bar inset on- * the window.- *- */- @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")- @RequiresApi(VERSION_CODES.P)- internal fun computeWindowBoundsP(activity: Activity): Rect {- val bounds = Rect()- val config = activity.resources.configuration- try {- val windowConfigField =- Configuration::class.java.getDeclaredField("windowConfiguration")- windowConfigField.isAccessible = true- val windowConfig = windowConfigField[config]-- // In multi-window mode we'll use the WindowConfiguration#mBounds property which- // should match the window size. Otherwise we'll use the mAppBounds property and- // will adjust it below.- if (isInMultiWindowMode(activity)) {- val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getBounds")- bounds.set((getAppBounds.invoke(windowConfig) as Rect))- } else {- val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getAppBounds")- bounds.set((getAppBounds.invoke(windowConfig) as Rect))- }- } catch (e: NoSuchFieldException) {- Log.w(TAG, e)- getRectSizeFromDisplay(activity, bounds)- } catch (e: NoSuchMethodException) {- Log.w(TAG, e)- getRectSizeFromDisplay(activity, bounds)- } catch (e: IllegalAccessException) {- Log.w(TAG, e)- getRectSizeFromDisplay(activity, bounds)- } catch (e: InvocationTargetException) {- Log.w(TAG, e)- getRectSizeFromDisplay(activity, bounds)- }- val platformWindowManager = activity.windowManager-- // [WindowManager#getDefaultDisplay] is deprecated but we have this for- // compatibility with older versions- @Suppress("DEPRECATION")- val currentDisplay = platformWindowManager.defaultDisplay- val realDisplaySize = Point()- @Suppress("DEPRECATION")- currentDisplay.getRealSize(realDisplaySize)-- if (!isInMultiWindowMode(activity)) {- // The activity is not in multi-window mode. Check if the addition of the- // navigation bar size to mAppBounds results in the real display size and if so- // assume the nav bar height should be added to the result.- val navigationBarHeight = getNavigationBarHeight(activity)- if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {- bounds.bottom += navigationBarHeight- } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {- bounds.right += navigationBarHeight- } else if (bounds.left == navigationBarHeight) {- bounds.left = 0- }- }- if ((bounds.width() < realDisplaySize.x || bounds.height() < realDisplaySize.y) &&- !isInMultiWindowMode(activity)- ) {- // If the corrected bounds are not the same as the display size and the activity is- // not in multi-window mode it is possible there are unreported cutouts inset-ing- // the window depending on the layoutInCutoutMode. Check for them here by getting- // the cutout from the display itself.- val displayCutout = getCutoutForDisplay(currentDisplay)- if (displayCutout != null) {- if (bounds.left == safeInsetLeft(displayCutout)) {- bounds.left = 0- }- if (realDisplaySize.x - bounds.right == safeInsetRight(displayCutout)) {- bounds.right += safeInsetRight(displayCutout)- }- if (bounds.top == safeInsetTop(displayCutout)) {- bounds.top = 0- }- if (realDisplaySize.y - bounds.bottom == safeInsetBottom(displayCutout)) {- bounds.bottom += safeInsetBottom(displayCutout)- }- }- }- return bounds- }-- private fun getRectSizeFromDisplay(activity: Activity, bounds: Rect) {- // [WindowManager#getDefaultDisplay] is deprecated but we have this for- // compatibility with older versions- @Suppress("DEPRECATION")- val defaultDisplay = activity.windowManager.defaultDisplay- // [Display#getRectSize] is deprecated but we have this for- // compatibility with older versions- @Suppress("DEPRECATION")- defaultDisplay.getRectSize(bounds)- }-- /**- * Computes the window bounds for platforms between [Build.VERSION_CODES.N]- * and [Build.VERSION_CODES.O_MR1], inclusive.- *- *- * NOTE: This method may result in incorrect values under the following conditions:- *- * * If the activity is in multi-window mode the origin of the returned bounds will- * always be anchored at (0, 0).- * * If the [android.content.res.Resources] value stored at 'navigation_bar_height' does- * not match the true navigation bar size the returned bounds will not take into account- * the navigation- * bar.- *- */- @RequiresApi(VERSION_CODES.N)- internal fun computeWindowBoundsN(activity: Activity): Rect {- val bounds = Rect()- // [WindowManager#getDefaultDisplay] is deprecated but we have this for- // compatibility with older versions- @Suppress("DEPRECATION")- val defaultDisplay = activity.windowManager.defaultDisplay- // [Display#getRectSize] is deprecated but we have this for- // compatibility with older versions- @Suppress("DEPRECATION")- defaultDisplay.getRectSize(bounds)- if (!isInMultiWindowMode(activity)) {- // The activity is not in multi-window mode. Check if the addition of the- // navigation bar size to Display#getSize() results in the real display size and- // if so return this value. If not, return the result of Display#getSize().- val realDisplaySize = getRealSizeForDisplay(defaultDisplay)- val navigationBarHeight = getNavigationBarHeight(activity)- if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {- bounds.bottom += navigationBarHeight- } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {- bounds.right += navigationBarHeight- }- }- return bounds- }-- /**- * Computes the window bounds for platforms between [Build.VERSION_CODES.JELLY_BEAN]- * and [Build.VERSION_CODES.M], inclusive.- *- *- * Given that multi-window mode isn't supported before N we simply return the real display- * size which should match the window size of a full-screen app.- */- internal fun computeWindowBoundsIceCreamSandwich(activity: Activity): Rect {- // [WindowManager#getDefaultDisplay] is deprecated but we have this for- // compatibility with older versions- @Suppress("DEPRECATION")- val defaultDisplay = activity.windowManager.defaultDisplay- val realDisplaySize = getRealSizeForDisplay(defaultDisplay)- val bounds = Rect()- if (realDisplaySize.x == 0 || realDisplaySize.y == 0) {- // [Display#getRectSize] is deprecated but we have this for- // compatibility with older versions- @Suppress("DEPRECATION")- defaultDisplay.getRectSize(bounds)- } else {- bounds.right = realDisplaySize.x- bounds.bottom = realDisplaySize.y- }- return bounds- }-- /**- * Returns the full (real) size of the display, in pixels, without subtracting any window- * decor or applying any compatibility scale factors.- *- *- * The size is adjusted based on the current rotation of the display.- *- * @return a point representing the real display size in pixels.- *- * @see Display.getRealSize- */- @VisibleForTesting- @Suppress("DEPRECATION")- internal fun getRealSizeForDisplay(display: Display): Point {- val size = Point()- display.getRealSize(size)- return size- }-- /**- * Returns the [android.content.res.Resources] value stored as 'navigation_bar_height'.- *- *- * Note: This is error-prone and is **not** the recommended way to determine the size- * of the overlapping region between the navigation bar and a given window. The best- * approach is to acquire the [android.view.WindowInsets].- */- private fun getNavigationBarHeight(context: Context): Int {- val resources = context.resources- val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")- return if (resourceId > 0) {- resources.getDimensionPixelSize(resourceId)- } else 0- }-- /**- * Returns the [DisplayCutout] for the given display. Note that display cutout returned- * here is for the display and the insets provided are in the display coordinate system.- *- * @return the display cutout for the given display.- */- @SuppressLint("BanUncheckedReflection")- @RequiresApi(VERSION_CODES.P)- private fun getCutoutForDisplay(display: Display): DisplayCutout? {- var displayCutout: DisplayCutout? = null- try {- val displayInfoClass = Class.forName("android.view.DisplayInfo")- val displayInfoConstructor = displayInfoClass.getConstructor()- displayInfoConstructor.isAccessible = true- val displayInfo = displayInfoConstructor.newInstance()- val getDisplayInfoMethod = display.javaClass.getDeclaredMethod(- "getDisplayInfo", displayInfo.javaClass- )- getDisplayInfoMethod.isAccessible = true- getDisplayInfoMethod.invoke(display, displayInfo)- val displayCutoutField = displayInfo.javaClass.getDeclaredField("displayCutout")- displayCutoutField.isAccessible = true- val cutout = displayCutoutField[displayInfo]- if (cutout is DisplayCutout) {- displayCutout = cutout- }- } catch (e: ClassNotFoundException) {- Log.w(TAG, e)- } catch (e: NoSuchMethodException) {- Log.w(TAG, e)- } catch (e: NoSuchFieldException) {- Log.w(TAG, e)- } catch (e: IllegalAccessException) {- Log.w(TAG, e)- } catch (e: InvocationTargetException) {- Log.w(TAG, e)- } catch (e: InstantiationException) {- Log.w(TAG, e)- }- return displayCutout+ return WindowMetricsCompatHelper.getInstance()+ .maximumWindowMetrics(context, densityCompatHelper) } /**@@ -440,18 +81,4 @@
WindowInsetsCompat.Type.tappableElement(), WindowInsetsCompat.Type.displayCutout() )-- /**- * Computes the current [WindowInsetsCompat] for a given [Context].- */- @RequiresApi(VERSION_CODES.R)- internal fun computeWindowInsetsCompat(@UiContext context: Context): WindowInsetsCompat {- val build = Build.VERSION.SDK_INT- val windowInsetsCompat = if (build >= VERSION_CODES.R) {- currentWindowInsets(context)- } else {- throw Exception("Incompatible SDK version")- }- return windowInsetsCompat- } }
@@ -20,6 +20,8 @@
import androidx.annotation.RestrictTo import androidx.annotation.UiContext import androidx.core.util.Consumer+import androidx.window.RequiresWindowSdkExtension+import androidx.window.layout.SupportedPosture import androidx.window.layout.WindowLayoutInfo import java.util.concurrent.Executor@@ -50,4 +52,11 @@
fun hasRegisteredListeners(): Boolean { return false }++ /**+ * Returns a [List] of [SupportedPosture] for the device.+ * @throws UnsupportedOperationException if the Window SDK version is less than 6.+ */+ @RequiresWindowSdkExtension(version = 6)+ val supportedPostures: List }
@@ -0,0 +1,31 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.layout.adapter.extensions++import androidx.window.RequiresWindowSdkExtension+import androidx.window.core.ConsumerAdapter+import androidx.window.extensions.layout.WindowLayoutComponent+import androidx.window.layout.SupportedPosture++@RequiresWindowSdkExtension(version = 6)+internal class ExtensionWindowBackendApi6(+ component: WindowLayoutComponent,+ adapter: ConsumerAdapter+) : ExtensionWindowBackendApi2(component, adapter) {+ override val supportedPostures: List+ get() = ExtensionsWindowLayoutInfoAdapter.translate(component.supportedWindowFeatures)+}
@@ -23,6 +23,7 @@
import androidx.annotation.VisibleForTesting import androidx.core.util.Consumer import androidx.window.core.Version+import androidx.window.layout.SupportedPosture import androidx.window.layout.WindowLayoutInfo import androidx.window.layout.adapter.WindowBackend import androidx.window.layout.adapter.sidecar.ExtensionInterfaceCompat.ExtensionCallbackInterface@@ -126,6 +127,9 @@
} }+ override val supportedPostures: List+ get() = throw UnsupportedOperationException("Must be called from extensions.")+ /** * Checks if there are no more registered callbacks left for the activity and inform * extension if needed.
diff --git a/window/window/src/main/java/androidx/window/layout/util/BoundsHelper.kt b/window/window/src/main/java/androidx/window/layout/util/BoundsHelper.kt
new file mode 100644
index 0000000..e743f4c
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/util/BoundsHelper.kt
@@ -0,0 +1,381 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.layout.util++import android.annotation.SuppressLint+import android.app.Activity+import android.content.Context+import android.content.res.Configuration+import android.graphics.Point+import android.graphics.Rect+import android.os.Build+import android.util.Log+import android.view.Display+import android.view.DisplayCutout+import android.view.WindowManager+import androidx.annotation.RequiresApi+import androidx.annotation.UiContext+import androidx.window.layout.util.ActivityCompatHelperApi24.isInMultiWindowMode+import androidx.window.layout.util.BoundsHelper.Companion.TAG+import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetBottom+import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetLeft+import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetRight+import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetTop+import androidx.window.layout.util.DisplayHelper.getRealSizeForDisplay+import java.lang.reflect.InvocationTargetException++/**+ * Provides compatibility behavior for calculating bounds of an activity.+ */+internal interface BoundsHelper {++ /**+ * Compute the current bounds for the given [Activity].+ */+ fun currentWindowBounds(activity: Activity): Rect++ fun maximumWindowBounds(@UiContext context: Context): Rect++ companion object {++ val TAG: String = BoundsHelper::class.java.simpleName++ fun getInstance(): BoundsHelper {+ return when {+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {+ BoundsHelperApi30Impl+ }+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {+ BoundsHelperApi29Impl+ }+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {+ BoundsHelperApi28Impl+ }+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {+ BoundsHelperApi24Impl+ }+ else -> {+ BoundsHelperApi16Impl+ }+ }+ }+ }+}++@RequiresApi(Build.VERSION_CODES.R)+private object BoundsHelperApi30Impl : BoundsHelper {+ override fun currentWindowBounds(activity: Activity): Rect {+ val wm = activity.getSystemService(WindowManager::class.java)+ return wm.currentWindowMetrics.bounds+ }++ override fun maximumWindowBounds(@UiContext context: Context): Rect {+ val wm = context.getSystemService(WindowManager::class.java)+ return wm.maximumWindowMetrics.bounds+ }+}++@RequiresApi(Build.VERSION_CODES.Q)+private object BoundsHelperApi29Impl : BoundsHelper {++ /**+ * Computes the window bounds for [Build.VERSION_CODES.Q].+ */+ @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")+ override fun currentWindowBounds(activity: Activity): Rect {+ var bounds: Rect+ val config = activity.resources.configuration+ try {+ val windowConfigField =+ Configuration::class.java.getDeclaredField("windowConfiguration")+ windowConfigField.isAccessible = true+ val windowConfig = windowConfigField[config]+ val getBoundsMethod = windowConfig.javaClass.getDeclaredMethod("getBounds")+ bounds = Rect(getBoundsMethod.invoke(windowConfig) as Rect)+ } catch (e: Exception) {+ when (e) {+ is NoSuchFieldException,+ is NoSuchMethodException,+ is IllegalAccessException,+ is InvocationTargetException -> {+ Log.w(TAG, e)+ // If reflection fails for some reason default to the P implementation which+ // still has the ability to account for display cutouts.+ bounds = BoundsHelperApi28Impl.currentWindowBounds(activity)+ }+ else -> throw e+ }+ }+ return bounds+ }++ override fun maximumWindowBounds(@UiContext context: Context): Rect {+ return BoundsHelperApi28Impl.maximumWindowBounds(context)+ }+}++@RequiresApi(Build.VERSION_CODES.P)+private object BoundsHelperApi28Impl : BoundsHelper {++ /**+ * Computes the window bounds for [Build.VERSION_CODES.P].+ *+ * NOTE: This method may result in incorrect values if the [android.content.res.Resources]+ * value stored at 'navigation_bar_height' does not match the true navigation bar inset on+ * the window.+ *+ */+ @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")+ override fun currentWindowBounds(activity: Activity): Rect {+ val bounds = Rect()+ val config = activity.resources.configuration+ try {+ val windowConfigField =+ Configuration::class.java.getDeclaredField("windowConfiguration")+ windowConfigField.isAccessible = true+ val windowConfig = windowConfigField[config]++ // In multi-window mode we'll use the WindowConfiguration#mBounds property which+ // should match the window size. Otherwise we'll use the mAppBounds property and+ // will adjust it below.+ if (isInMultiWindowMode(activity)) {+ val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getBounds")+ bounds.set((getAppBounds.invoke(windowConfig) as Rect))+ } else {+ val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getAppBounds")+ bounds.set((getAppBounds.invoke(windowConfig) as Rect))+ }+ } catch (e: Exception) {+ when (e) {+ is NoSuchFieldException,+ is NoSuchMethodException,+ is IllegalAccessException,+ is InvocationTargetException -> {+ Log.w(TAG, e)+ getRectSizeFromDisplay(activity, bounds)+ }+ else -> throw e+ }+ }++ val platformWindowManager = activity.windowManager++ // [WindowManager#getDefaultDisplay] is deprecated but we have this for+ // compatibility with older versions+ @Suppress("DEPRECATION")+ val currentDisplay = platformWindowManager.defaultDisplay+ val realDisplaySize = Point()+ @Suppress("DEPRECATION")+ currentDisplay.getRealSize(realDisplaySize)++ if (!isInMultiWindowMode(activity)) {+ // The activity is not in multi-window mode. Check if the addition of the+ // navigation bar size to mAppBounds results in the real display size and if so+ // assume the nav bar height should be added to the result.+ val navigationBarHeight = getNavigationBarHeight(activity)+ if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {+ bounds.bottom += navigationBarHeight+ } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {+ bounds.right += navigationBarHeight+ } else if (bounds.left == navigationBarHeight) {+ bounds.left = 0+ }+ }+ if ((bounds.width() < realDisplaySize.x || bounds.height() < realDisplaySize.y) &&+ !isInMultiWindowMode(activity)+ ) {+ // If the corrected bounds are not the same as the display size and the activity is+ // not in multi-window mode it is possible there are unreported cutouts inset-ing+ // the window depending on the layoutInCutoutMode. Check for them here by getting+ // the cutout from the display itself.+ val displayCutout = getCutoutForDisplay(currentDisplay)+ if (displayCutout != null) {+ if (bounds.left == safeInsetLeft(displayCutout)) {+ bounds.left = 0+ }+ if (realDisplaySize.x - bounds.right == safeInsetRight(displayCutout)) {+ bounds.right += safeInsetRight(displayCutout)+ }+ if (bounds.top == safeInsetTop(displayCutout)) {+ bounds.top = 0+ }+ if (realDisplaySize.y - bounds.bottom == safeInsetBottom(displayCutout)) {+ bounds.bottom += safeInsetBottom(displayCutout)+ }+ }+ }+ return bounds+ }++ override fun maximumWindowBounds(@UiContext context: Context): Rect {+ return BoundsHelperApi24Impl.maximumWindowBounds(context)+ }+}++@RequiresApi(Build.VERSION_CODES.N)+private object BoundsHelperApi24Impl : BoundsHelper {++ /**+ * Computes the window bounds for platforms between [Build.VERSION_CODES.N]+ * and [Build.VERSION_CODES.O_MR1], inclusive.+ *+ * NOTE: This method may result in incorrect values under the following conditions:+ *+ * * If the activity is in multi-window mode the origin of the returned bounds will+ * always be anchored at (0, 0).+ * * If the [android.content.res.Resources] value stored at 'navigation_bar_height' does+ * not match the true navigation bar size the returned bounds will not take into account+ * the navigation bar.+ *+ */+ override fun currentWindowBounds(activity: Activity): Rect {+ val bounds = Rect()+ // [WindowManager#getDefaultDisplay] is deprecated but we have this for+ // compatibility with older versions+ @Suppress("DEPRECATION")+ val defaultDisplay = activity.windowManager.defaultDisplay+ // [Display#getRectSize] is deprecated but we have this for+ // compatibility with older versions+ @Suppress("DEPRECATION")+ defaultDisplay.getRectSize(bounds)+ if (!isInMultiWindowMode(activity)) {+ // The activity is not in multi-window mode. Check if the addition of the+ // navigation bar size to Display#getSize() results in the real display size and+ // if so return this value. If not, return the result of Display#getSize().+ val realDisplaySize = getRealSizeForDisplay(defaultDisplay)+ val navigationBarHeight = getNavigationBarHeight(activity)+ if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {+ bounds.bottom += navigationBarHeight+ } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {+ bounds.right += navigationBarHeight+ }+ }+ return bounds+ }++ override fun maximumWindowBounds(@UiContext context: Context): Rect {+ return BoundsHelperApi16Impl.maximumWindowBounds(context)+ }+}++private object BoundsHelperApi16Impl : BoundsHelper {++ /**+ * Computes the window bounds for platforms between [Build.VERSION_CODES.JELLY_BEAN]+ * and [Build.VERSION_CODES.M], inclusive.+ *+ * Given that multi-window mode isn't supported before N we simply return the real display+ * size which should match the window size of a full-screen app.+ */+ override fun currentWindowBounds(activity: Activity): Rect {+ // [WindowManager#getDefaultDisplay] is deprecated but we have this for+ // compatibility with older versions+ @Suppress("DEPRECATION")+ val defaultDisplay = activity.windowManager.defaultDisplay+ val realDisplaySize = getRealSizeForDisplay(defaultDisplay)+ val bounds = Rect()+ if (realDisplaySize.x == 0 || realDisplaySize.y == 0) {+ // [Display#getRectSize] is deprecated but we have this for+ // compatibility with older versions+ @Suppress("DEPRECATION")+ defaultDisplay.getRectSize(bounds)+ } else {+ bounds.right = realDisplaySize.x+ bounds.bottom = realDisplaySize.y+ }+ return bounds+ }++ override fun maximumWindowBounds(@UiContext context: Context): Rect {+ val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager+ // [WindowManager#getDefaultDisplay] is deprecated but we have this for+ // compatibility with older versions, as we can't reliably get the display associated+ // with a Context through public APIs either.+ @Suppress("DEPRECATION")+ val display = wm.defaultDisplay+ val displaySize = getRealSizeForDisplay(display)+ return Rect(0, 0, displaySize.x, displaySize.y)+ }+}++/**+ * Returns the [android.content.res.Resources] value stored as 'navigation_bar_height'.+ *+ * Note: This is error-prone and is **not** the recommended way to determine the size+ * of the overlapping region between the navigation bar and a given window. The best+ * approach is to acquire the [android.view.WindowInsets].+ */+private fun getNavigationBarHeight(context: Context): Int {+ val resources = context.resources+ val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")+ return if (resourceId > 0) {+ resources.getDimensionPixelSize(resourceId)+ } else 0+}++private fun getRectSizeFromDisplay(activity: Activity, bounds: Rect) {+ // [WindowManager#getDefaultDisplay] is deprecated but we have this for+ // compatibility with older versions+ @Suppress("DEPRECATION")+ val defaultDisplay = activity.windowManager.defaultDisplay+ // [Display#getRectSize] is deprecated but we have this for+ // compatibility with older versions+ @Suppress("DEPRECATION")+ defaultDisplay.getRectSize(bounds)+}++/**+ * Returns the [DisplayCutout] for the given display. Note that display cutout returned+ * here is for the display and the insets provided are in the display coordinate system.+ *+ * @return the display cutout for the given display.+ */+@SuppressLint("BanUncheckedReflection")+@RequiresApi(Build.VERSION_CODES.P)+private fun getCutoutForDisplay(display: Display): DisplayCutout? {+ var displayCutout: DisplayCutout? = null+ try {+ val displayInfoClass = Class.forName("android.view.DisplayInfo")+ val displayInfoConstructor = displayInfoClass.getConstructor()+ displayInfoConstructor.isAccessible = true+ val displayInfo = displayInfoConstructor.newInstance()+ val getDisplayInfoMethod = display.javaClass.getDeclaredMethod("getDisplayInfo",+ displayInfo.javaClass+ )+ getDisplayInfoMethod.isAccessible = true+ getDisplayInfoMethod.invoke(display, displayInfo)+ val displayCutoutField = displayInfo.javaClass.getDeclaredField("displayCutout")+ displayCutoutField.isAccessible = true+ val cutout = displayCutoutField[displayInfo]+ if (cutout is DisplayCutout) {+ displayCutout = cutout+ }+ } catch (e: Exception) {+ when (e) {+ is ClassNotFoundException,+ is NoSuchMethodException,+ is NoSuchFieldException,+ is IllegalAccessException,+ is InvocationTargetException,+ is InstantiationException -> {+ Log.w(TAG, e)+ }+ else -> throw e+ }+ }+ return displayCutout+}
@@ -0,0 +1,76 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.layout.util++import android.content.Context+import android.content.res.Configuration+import android.os.Build+import android.util.DisplayMetrics+import android.view.WindowManager+import android.view.WindowMetrics as AndroidWindowMetrics+import androidx.annotation.RequiresApi+import androidx.annotation.UiContext++/**+ * Provides compatibility behavior for functionality related to display density.+ */+internal interface DensityCompatHelper {++ /**+ * Returns the logical density of the display associated with the [Context].+ */+ fun density(context: Context): Float++ /**+ * Returns the logical density of the display associated with the [Configuration] or+ * [AndroidWindowMetrics], depending on the SDK level.+ */+ fun density(configuration: Configuration, windowMetrics: AndroidWindowMetrics): Float++ companion object {+ fun getInstance(): DensityCompatHelper {+ return when {+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE ->+ DensityCompatHelperApi34Impl+ else ->+ DensityCompatHelperBaseImpl+ }+ }+ }+}++private object DensityCompatHelperBaseImpl : DensityCompatHelper {+ override fun density(context: Context): Float {+ return context.resources.displayMetrics.density+ }++ override fun density(configuration: Configuration, windowMetrics: AndroidWindowMetrics): Float {+ return configuration.densityDpi.toFloat() /+ DisplayMetrics.DENSITY_DEFAULT+ }+}++@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)+private object DensityCompatHelperApi34Impl : DensityCompatHelper {+ override fun density(@UiContext context: Context): Float {+ return context.getSystemService(WindowManager::class.java).currentWindowMetrics.density+ }++ override fun density(configuration: Configuration, windowMetrics: AndroidWindowMetrics): Float {+ return windowMetrics.density+ }+}
diff --git a/window/window/src/main/java/androidx/window/layout/util/DisplayHelper.kt b/window/window/src/main/java/androidx/window/layout/util/DisplayHelper.kt
new file mode 100644
index 0000000..ae46dd1
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/util/DisplayHelper.kt
@@ -0,0 +1,40 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.layout.util++import android.graphics.Point+import android.view.Display++internal object DisplayHelper {++ /**+ * Returns the full (real) size of the display, in pixels, without subtracting any window+ * decor or applying any compatibility scale factors.+ *+ * The size is adjusted based on the current rotation of the display.+ *+ * @return a point representing the real display size in pixels.+ *+ * @see Display.getRealSize+ */+ @Suppress("DEPRECATION")+ fun getRealSizeForDisplay(display: Display): Point {+ val size = Point()+ display.getRealSize(size)+ return size+ }+}
@@ -0,0 +1,238 @@
+/*+ * Copyright 2024 The Android Open Source Project+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ * http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package androidx.window.layout.util++import android.app.Activity+import android.content.Context+import android.graphics.Rect+import android.inputmethodservice.InputMethodService+import android.os.Build+import android.view.WindowManager+import android.view.WindowMetrics as AndroidWindowMetrics+import androidx.annotation.RequiresApi+import androidx.annotation.UiContext+import androidx.core.view.WindowInsetsCompat+import androidx.window.core.Bounds+import androidx.window.layout.WindowMetrics+import androidx.window.layout.WindowMetricsCalculator+import androidx.window.layout.util.ContextCompatHelper.unwrapUiContext+import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowInsets+import androidx.window.layout.util.DisplayHelper.getRealSizeForDisplay++/**+ * Provides compatibility behavior for functionality related to [WindowMetricsCalculator].+ */+internal interface WindowMetricsCompatHelper {++ /**+ * Translates platform [AndroidWindowMetrics] to jetpack [WindowMetrics].+ */+ @RequiresApi(Build.VERSION_CODES.R)+ fun translateWindowMetrics(windowMetrics: AndroidWindowMetrics, density: Float): WindowMetrics++ /**+ * Returns the [WindowMetrics] associated with the provided [Context].+ */+ fun currentWindowMetrics(+ @UiContext context: Context,+ densityCompatHelper: DensityCompatHelper+ ): WindowMetrics++ /**+ * Returns the [WindowMetrics] associated with the provided [Activity].+ */+ fun currentWindowMetrics(+ activity: Activity,+ densityCompatHelper: DensityCompatHelper+ ): WindowMetrics++ /**+ * Returns the maximum [WindowMetrics] for a given [UiContext].+ */+ fun maximumWindowMetrics(+ @UiContext context: Context,+ densityCompatHelper: DensityCompatHelper+ ): WindowMetrics++ companion object {+ fun getInstance(): WindowMetricsCompatHelper {+ return when {+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE ->+ WindowMetricsCompatHelperApi34Impl+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->+ WindowMetricsCompatHelperApi30Impl+ else ->+ WindowMetricsCompatHelperBaseImpl+ }+ }+ }+}++internal object WindowMetricsCompatHelperBaseImpl : WindowMetricsCompatHelper {++ override fun translateWindowMetrics(+ windowMetrics: AndroidWindowMetrics,+ density: Float+ ): WindowMetrics {+ throw UnsupportedOperationException("translateWindowMetrics not available before API30")+ }++ override fun currentWindowMetrics(+ @UiContext context: Context,+ densityCompatHelper: DensityCompatHelper+ ): WindowMetrics {+ when (val unwrappedContext = unwrapUiContext(context)) {+ is Activity -> {+ return currentWindowMetrics(unwrappedContext, densityCompatHelper)+ }++ is InputMethodService -> {+ val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager++ // On older SDK levels, the app and IME could show up on different displays.+ // However, there isn't a way for us to figure this out from the application+ // layer. But, this should be good enough for now given the small likelihood of+ // IMEs showing up on non-primary displays on these SDK levels.+ @Suppress("DEPRECATION")+ val displaySize = getRealSizeForDisplay(wm.defaultDisplay)++ // IME occupies the whole display bounds.+ val imeBounds = Rect(0, 0, displaySize.x, displaySize.y)+ return WindowMetrics(+ imeBounds,+ density = densityCompatHelper.density(context)+ )+ }++ else -> {+ throw IllegalArgumentException("$context is not a UiContext")+ }+ }+ }++ override fun currentWindowMetrics(+ activity: Activity,+ densityCompatHelper: DensityCompatHelper+ ): WindowMetrics {+ // TODO (b/233899790): compute insets for other platform versions below R+ return WindowMetrics(+ Bounds(BoundsHelper.getInstance().currentWindowBounds(activity)),+ WindowInsetsCompat.Builder().build(),+ densityCompatHelper.density(activity)+ )+ }++ override fun maximumWindowMetrics(+ @UiContext context: Context,+ densityCompatHelper: DensityCompatHelper+ ): WindowMetrics {+ // TODO (b/233899790): compute insets for other platform versions below Rs+ return WindowMetrics(+ Bounds(BoundsHelper.getInstance().maximumWindowBounds(context)),+ WindowInsetsCompat.Builder().build(),+ densityCompatHelper.density(context)+ )+ }+}++@RequiresApi(Build.VERSION_CODES.R)+internal object WindowMetricsCompatHelperApi30Impl : WindowMetricsCompatHelper {++ override fun translateWindowMetrics(+ windowMetrics: AndroidWindowMetrics,+ density: Float+ ): WindowMetrics {+ return WindowMetrics(+ windowMetrics.bounds,+ WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets),+ density+ )+ }++ override fun currentWindowMetrics(+ @UiContext context: Context,+ densityCompatHelper: DensityCompatHelper+ ): WindowMetrics {+ val wm = context.getSystemService(WindowManager::class.java)+ val insets = WindowInsetsCompat.toWindowInsetsCompat(wm.currentWindowMetrics.windowInsets)+ val density = context.resources.displayMetrics.density+ return WindowMetrics(wm.currentWindowMetrics.bounds, insets, density)+ }++ override fun currentWindowMetrics(+ activity: Activity,+ densityCompatHelper: DensityCompatHelper+ ): WindowMetrics {+ return WindowMetrics(+ Bounds(BoundsHelper.getInstance().currentWindowBounds(activity)),+ currentWindowInsets(activity),+ densityCompatHelper.density(activity)+ )+ }++ override fun maximumWindowMetrics(+ @UiContext context: Context,+ densityCompatHelper: DensityCompatHelper+ ): WindowMetrics {+ return WindowMetrics(+ Bounds(BoundsHelper.getInstance().maximumWindowBounds(context)),+ currentWindowInsets(context),+ densityCompatHelper.density(context)+ )+ }+}++@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)+internal object WindowMetricsCompatHelperApi34Impl : WindowMetricsCompatHelper {++ override fun translateWindowMetrics(+ windowMetrics: AndroidWindowMetrics,+ density: Float+ ): WindowMetrics {+ return WindowMetrics(+ windowMetrics.bounds,+ WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets),+ windowMetrics.density+ )+ }++ override fun currentWindowMetrics(+ @UiContext context: Context,+ densityCompatHelper: DensityCompatHelper+ ): WindowMetrics {+ val wm = context.getSystemService(WindowManager::class.java)+ return WindowMetrics(wm.currentWindowMetrics.bounds,+ WindowInsetsCompat.toWindowInsetsCompat(wm.currentWindowMetrics.windowInsets),+ wm.currentWindowMetrics.density)+ }++ override fun currentWindowMetrics(+ activity: Activity,+ densityCompatHelper: DensityCompatHelper+ ): WindowMetrics {+ return WindowMetricsCompatHelperApi30Impl+ .currentWindowMetrics(activity, densityCompatHelper)+ }++ override fun maximumWindowMetrics(+ @UiContext context: Context,+ densityCompatHelper: DensityCompatHelper+ ): WindowMetrics {+ return WindowMetricsCompatHelperApi30Impl+ .maximumWindowMetrics(context, densityCompatHelper)+ }+}
@@ -38,19 +38,33 @@
internal const val WINDOW_EXTENSIONS_CLASS = "$WINDOW_EXTENSIONS_PACKAGE_NAME.WindowExtensions"+ internal const val LAYOUT_PACKAGE = "layout"+ /** * Constant name for class [androidx.window.extensions.layout.FoldingFeature] * used for reflection */ internal const val FOLDING_FEATURE_CLASS =- "$WINDOW_EXTENSIONS_PACKAGE_NAME.layout.FoldingFeature"+ "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.FoldingFeature"++ /**+ * Constant name for class [androidx.window.extensions.layout.SupportedWindowFeatures]+ */+ internal const val SUPPORTED_WINDOW_FEATURES_CLASS =+ "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.SupportedWindowFeatures"++ /**+ * Constant name for class [androidx.window.extensions.layout.DisplayFoldFeature]+ */+ internal const val DISPLAY_FOLD_FEATURE_CLASS =+ "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.DisplayFoldFeature" /** * Constant name for class [androidx.window.extensions.layout.WindowLayoutComponent] * used for reflection */ internal const val WINDOW_LAYOUT_COMPONENT_CLASS =- "$WINDOW_EXTENSIONS_PACKAGE_NAME.layout.WindowLayoutComponent"+ "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.WindowLayoutComponent" /** * Constant name for class [androidx.window.extensions.area.WindowAreaComponent]
@@ -43,7 +43,7 @@
`ActivityRule`. The suggested usage is to set the tag to be able to differentiate between different rules in the callbacks. For example, it can be used to compute the right `SplitAttributes` for the given split rule- in `SplitAttributes` calculator function. -->+ in `SplitAttributesCalculator.computeSplitAttributesForParams`. -->