summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Richard MacGregor <rmacgregor@google.com> 2022-10-21 19:00:18 -0700
committer Richard MacGregor <rmacgregor@google.com> 2022-11-08 19:22:36 -0800
commit55a59e855fbf46980bdc72615a9d27c14083f27d (patch)
tree24b56a240357028b30dbcf843888c356b09808e9
parent2941dd6afabfc20d41b7493a656ea55482c6789f (diff)
Initial implementation of SafetyLabel parser
Currently only minimal SafetyLabel parsing targeting Permission Rationale feature work Bug: 252810718 Test: atest SafetyLabelTest Test: atest DataLabelTest Test: atest DataCategoryTest Test: atest DataTypeTest Change-Id: I431637b4468ddc0303065f672e5ac97c3f770dc7
-rw-r--r--SafetyLabel/Android.bp49
-rw-r--r--SafetyLabel/java/com/android/permission/safetylabel/DataCategory.java100
-rw-r--r--SafetyLabel/java/com/android/permission/safetylabel/DataCategoryConstants.java101
-rw-r--r--SafetyLabel/java/com/android/permission/safetylabel/DataLabel.java83
-rw-r--r--SafetyLabel/java/com/android/permission/safetylabel/DataLabelConstants.java42
-rw-r--r--SafetyLabel/java/com/android/permission/safetylabel/DataPurposeConstants.java79
-rw-r--r--SafetyLabel/java/com/android/permission/safetylabel/DataType.java154
-rw-r--r--SafetyLabel/java/com/android/permission/safetylabel/DataTypeConstants.java421
-rw-r--r--SafetyLabel/java/com/android/permission/safetylabel/SafetyLabel.java59
-rw-r--r--SafetyLabel/tests/Android.bp38
-rw-r--r--SafetyLabel/tests/AndroidManifest.xml30
-rw-r--r--SafetyLabel/tests/AndroidTest.xml40
-rw-r--r--SafetyLabel/tests/java/com/android/permission/safetylabel/DataCategoryTest.kt261
-rw-r--r--SafetyLabel/tests/java/com/android/permission/safetylabel/DataLabelTest.kt126
-rw-r--r--SafetyLabel/tests/java/com/android/permission/safetylabel/DataTypeTest.kt280
-rw-r--r--SafetyLabel/tests/java/com/android/permission/safetylabel/SafetyLabelTest.kt70
-rw-r--r--SafetyLabel/tests/java/com/android/permission/safetylabel/SafetyLabelTestPersistableBundles.kt261
17 files changed, 2194 insertions, 0 deletions
diff --git a/SafetyLabel/Android.bp b/SafetyLabel/Android.bp
new file mode 100644
index 000000000..119890d3f
--- /dev/null
+++ b/SafetyLabel/Android.bp
@@ -0,0 +1,49 @@
+// Copyright (C) 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+ name: "safety-label-java-sources",
+ srcs: [
+ "java/**/*.java",
+ ],
+ path: "java",
+ visibility: ["//visibility:private"],
+}
+
+java_library {
+ name: "safety-label",
+ sdk_version: "system_current",
+ min_sdk_version: "30",
+ target_sdk_version: "33",
+ srcs: [
+ ":safety-label-java-sources",
+ ],
+ libs: [
+ "androidx.annotation_annotation",
+ "framework-annotations-lib",
+ ],
+ apex_available: [
+ "com.android.permission",
+ "test_com.android.permission",
+ ],
+ installable: false,
+ visibility: [
+ "//packages/modules/Permission:__subpackages__",
+ ],
+}
+
diff --git a/SafetyLabel/java/com/android/permission/safetylabel/DataCategory.java b/SafetyLabel/java/com/android/permission/safetylabel/DataCategory.java
new file mode 100644
index 000000000..395468c81
--- /dev/null
+++ b/SafetyLabel/java/com/android/permission/safetylabel/DataCategory.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 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 com.android.permission.safetylabel;
+
+import android.os.PersistableBundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.permission.safetylabel.DataLabelConstants.DataUsage;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Data usage category representation containing one or more {@link DataType}. Valid category keys
+ * are defined in {@link DataCategoryConstants}, each category has a valid set of types {@link
+ * DataType}, which are mapped in {@link DataTypeConstants}
+ */
+public class DataCategory {
+ private final Map<String, DataType> mDataTypes;
+
+ private DataCategory(@NonNull Map<String, DataType> dataTypes) {
+ this.mDataTypes = dataTypes;
+ }
+
+ /**
+ * Returns a {@link java.util.Collections.UnmodifiableMap} of {@link String} category to {@link
+ * DataCategory} created by parsing a {@link PersistableBundle}
+ */
+ @NonNull
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ static Map<String, DataCategory> getDataCategoryMap(
+ @Nullable PersistableBundle dataLabelBundle, @DataUsage @NonNull String dataUsage) {
+ if (dataLabelBundle == null) {
+ return Collections.emptyMap();
+ }
+
+ PersistableBundle dataCategoryMapBundle = dataLabelBundle.getPersistableBundle(dataUsage);
+ if (dataCategoryMapBundle == null) {
+ return Collections.emptyMap();
+ }
+
+ Map<String, DataCategory> dataCategoryMap = new HashMap<>();
+ for (String category : DataCategoryConstants.VALID_CATEGORIES) {
+ DataCategory dataCategory = getDataCategory(dataCategoryMapBundle, dataUsage, category);
+ if (dataCategory != null) {
+ dataCategoryMap.put(category, dataCategory);
+ }
+ }
+ return Collections.unmodifiableMap(dataCategoryMap);
+ }
+
+ /**
+ * Returns a {@link DataCategory} created by parsing a {@link PersistableBundle}, or {@code
+ * null} if parsing results in an invalid or empty DataCategory
+ */
+ @Nullable
+ @VisibleForTesting
+ static DataCategory getDataCategory(
+ @Nullable PersistableBundle dataCategoryMapBundle,
+ @NonNull String dataUsage,
+ @NonNull String category) {
+ if (dataCategoryMapBundle == null) {
+ return null;
+ }
+
+ PersistableBundle dataCategoryBundle = dataCategoryMapBundle.getPersistableBundle(category);
+
+ Map<String, DataType> dataTypeMap =
+ DataType.getDataTypeMap(dataCategoryBundle, dataUsage, category);
+ if (dataTypeMap.isEmpty()) {
+ return null;
+ }
+
+ return new DataCategory(dataTypeMap);
+ }
+
+ /** Return the type {@link Map} of String type key to {@link DataType} */
+ @NonNull
+ public Map<String, DataType> getDataTypes() {
+ return mDataTypes;
+ }
+}
diff --git a/SafetyLabel/java/com/android/permission/safetylabel/DataCategoryConstants.java b/SafetyLabel/java/com/android/permission/safetylabel/DataCategoryConstants.java
new file mode 100644
index 000000000..af04220c0
--- /dev/null
+++ b/SafetyLabel/java/com/android/permission/safetylabel/DataCategoryConstants.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 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 com.android.permission.safetylabel;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.StringDef;
+
+import java.lang.annotation.Retention;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Constants for determining valid {@link String} data types for usage within {@link SafetyLabel},
+ * {@link DataCategory}, and {@link DataType}
+ */
+public class DataCategoryConstants {
+
+ /** List of valid Safety Label data collection/sharing categories */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "CATEGORY_",
+ value = {
+ CATEGORY_PERSONAL,
+ CATEGORY_FINANCIAL,
+ CATEGORY_LOCATION,
+ CATEGORY_EMAIL_TEXT_MESSAGE,
+ CATEGORY_PHOTO_VIDEO,
+ CATEGORY_AUDIO,
+ CATEGORY_STORAGE,
+ CATEGORY_HEALTH_FITNESS,
+ CATEGORY_CONTACTS,
+ CATEGORY_CALENDAR,
+ CATEGORY_IDENTIFIERS,
+ CATEGORY_APP_PERFORMANCE,
+ CATEGORY_ACTIONS_IN_APP,
+ CATEGORY_SEARCH_AND_BROWSING,
+ })
+ public @interface Category {}
+
+ public static final String CATEGORY_PERSONAL = "personal";
+ public static final String CATEGORY_FINANCIAL = "financial";
+ public static final String CATEGORY_LOCATION = "location";
+ public static final String CATEGORY_EMAIL_TEXT_MESSAGE = "email_text_message";
+ public static final String CATEGORY_PHOTO_VIDEO = "photo_video";
+ public static final String CATEGORY_AUDIO = "audio";
+ public static final String CATEGORY_STORAGE = "storage";
+ public static final String CATEGORY_HEALTH_FITNESS = "health_fitness";
+ public static final String CATEGORY_CONTACTS = "contacts";
+ public static final String CATEGORY_CALENDAR = "calendar";
+ public static final String CATEGORY_IDENTIFIERS = "identifiers";
+ public static final String CATEGORY_APP_PERFORMANCE = "app_performance";
+ public static final String CATEGORY_ACTIONS_IN_APP = "actions_in_app";
+ public static final String CATEGORY_SEARCH_AND_BROWSING = "search_and_browsing";
+
+ /** Set of valid categories */
+ @DataCategoryConstants.Category
+ public static final Set<String> VALID_CATEGORIES =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(
+ CATEGORY_PERSONAL,
+ CATEGORY_FINANCIAL,
+ CATEGORY_LOCATION,
+ CATEGORY_EMAIL_TEXT_MESSAGE,
+ CATEGORY_PHOTO_VIDEO,
+ CATEGORY_AUDIO,
+ CATEGORY_STORAGE,
+ CATEGORY_HEALTH_FITNESS,
+ CATEGORY_CONTACTS,
+ CATEGORY_CALENDAR,
+ CATEGORY_IDENTIFIERS,
+ CATEGORY_APP_PERFORMANCE,
+ CATEGORY_ACTIONS_IN_APP,
+ CATEGORY_SEARCH_AND_BROWSING)));
+
+ /** Returns {@link Set} of valid {@link String} category keys */
+ public static Set<String> getValidDataCategories() {
+ return VALID_CATEGORIES;
+ }
+
+ private DataCategoryConstants() {
+ /* do nothing - hide constructor */
+ }
+}
diff --git a/SafetyLabel/java/com/android/permission/safetylabel/DataLabel.java b/SafetyLabel/java/com/android/permission/safetylabel/DataLabel.java
new file mode 100644
index 000000000..68af27470
--- /dev/null
+++ b/SafetyLabel/java/com/android/permission/safetylabel/DataLabel.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 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 com.android.permission.safetylabel;
+
+import static com.android.permission.safetylabel.DataLabelConstants.DATA_USAGE_COLLECTED;
+import static com.android.permission.safetylabel.DataLabelConstants.DATA_USAGE_SHARED;
+
+import android.os.PersistableBundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.Map;
+
+/**
+ * Data label representation with data shared and data collected maps containing zero or more
+ * {@link DataCategory}
+ */
+public class DataLabel {
+ @VisibleForTesting public static final String KEY_DATA_LABEL = "data_labels";
+ private final Map<String, DataCategory> mDataCollected;
+ private final Map<String, DataCategory> mDataShared;
+
+ public DataLabel(
+ @NonNull Map<String, DataCategory> dataCollected,
+ @NonNull Map<String, DataCategory> dataShared) {
+ mDataCollected = dataCollected;
+ mDataShared = dataShared;
+ }
+
+ /** Returns a {@link DataLabel} created by parsing a SafetyLabel {@link PersistableBundle} */
+ @NonNull
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ public static DataLabel getDataLabel(@Nullable PersistableBundle safetyLabelBundle) {
+ if (safetyLabelBundle == null) {
+ return null;
+ }
+
+ PersistableBundle dataLabelBundle = safetyLabelBundle.getPersistableBundle(KEY_DATA_LABEL);
+ if (dataLabelBundle == null) {
+ return null;
+ }
+
+ Map<String, DataCategory> dataCollectedCategoryMap =
+ DataCategory.getDataCategoryMap(dataLabelBundle, DATA_USAGE_COLLECTED);
+ Map<String, DataCategory> dataSharedCategoryMap =
+ DataCategory.getDataCategoryMap(dataLabelBundle, DATA_USAGE_SHARED);
+ return new DataLabel(dataCollectedCategoryMap, dataSharedCategoryMap);
+ }
+
+ /**
+ * Returns the data collected {@link Map} of {@link
+ * com.android.permission.safetylabel.DataCategoryConstants.Category} to {@link DataCategory}
+ */
+ @NonNull
+ public Map<String, DataCategory> getDataCollected() {
+ return mDataCollected;
+ }
+
+ /**
+ * Returns the data shared {@link Map} of {@link
+ * com.android.permission.safetylabel.DataCategoryConstants.Category} to {@link DataCategory}
+ */
+ @NonNull
+ public Map<String, DataCategory> getDataShared() {
+ return mDataShared;
+ }
+}
diff --git a/SafetyLabel/java/com/android/permission/safetylabel/DataLabelConstants.java b/SafetyLabel/java/com/android/permission/safetylabel/DataLabelConstants.java
new file mode 100644
index 000000000..f592d9b0f
--- /dev/null
+++ b/SafetyLabel/java/com/android/permission/safetylabel/DataLabelConstants.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 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 com.android.permission.safetylabel;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.StringDef;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Constants and util methods for determining valid {@link String} data types for usage within
+ * {@link SafetyLabel} and {@link DataLabel}
+ */
+public class DataLabelConstants {
+
+ /** List of valid Safety Label data usages. Shared vs Collected */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "DATA_USAGE_",
+ value = {
+ DATA_USAGE_COLLECTED,
+ DATA_USAGE_SHARED,
+ })
+ public @interface DataUsage {}
+ public static final String DATA_USAGE_COLLECTED = "data_collected";
+ public static final String DATA_USAGE_SHARED = "data_shared";
+}
diff --git a/SafetyLabel/java/com/android/permission/safetylabel/DataPurposeConstants.java b/SafetyLabel/java/com/android/permission/safetylabel/DataPurposeConstants.java
new file mode 100644
index 000000000..0e1bb31ce
--- /dev/null
+++ b/SafetyLabel/java/com/android/permission/safetylabel/DataPurposeConstants.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 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 com.android.permission.safetylabel;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Constants for determining valid {@link Integer} data usage purposes for usage within
+ * {@link DataType}
+ */
+public class DataPurposeConstants {
+
+ /** List of valid data usage purposes */
+ @Retention(SOURCE)
+ @IntDef(
+ prefix = "PURPOSE_",
+ value = {
+ PURPOSE_APP_FUNCTIONALITY,
+ PURPOSE_ANALYTICS,
+ PURPOSE_DEVELOPER_COMMUNICATIONS,
+ PURPOSE_FRAUD_PREVENTION_SECURITY,
+ PURPOSE_ADVERTISING,
+ PURPOSE_PERSONALIZATION,
+ PURPOSE_ACCOUNT_MANAGEMENT,
+ })
+ public @interface Purpose {}
+
+ public static final int PURPOSE_APP_FUNCTIONALITY = 1;
+ public static final int PURPOSE_ANALYTICS = 2;
+ public static final int PURPOSE_DEVELOPER_COMMUNICATIONS = 3;
+ public static final int PURPOSE_FRAUD_PREVENTION_SECURITY = 4;
+ public static final int PURPOSE_ADVERTISING = 5;
+ public static final int PURPOSE_PERSONALIZATION = 6;
+ public static final int PURPOSE_ACCOUNT_MANAGEMENT = 7;
+ // RESERVED/DEPRECATED = 8
+ // RESERVED/DEPRECATED = 9
+
+ /** {@link Set} of valid {@link Integer} purposes */
+ @Purpose
+ public static final Set<Integer> VALID_PURPOSES =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(
+ PURPOSE_APP_FUNCTIONALITY,
+ PURPOSE_ANALYTICS,
+ PURPOSE_DEVELOPER_COMMUNICATIONS,
+ PURPOSE_FRAUD_PREVENTION_SECURITY,
+ PURPOSE_ADVERTISING,
+ PURPOSE_PERSONALIZATION,
+ PURPOSE_ACCOUNT_MANAGEMENT)));
+
+ /** Returns {@link Set} of valid {@link Integer} purpose */
+ @Purpose
+ public static Set<Integer> getValidPurposes() {
+ return VALID_PURPOSES;
+ }
+}
diff --git a/SafetyLabel/java/com/android/permission/safetylabel/DataType.java b/SafetyLabel/java/com/android/permission/safetylabel/DataType.java
new file mode 100644
index 000000000..abd69ca6c
--- /dev/null
+++ b/SafetyLabel/java/com/android/permission/safetylabel/DataType.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 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 com.android.permission.safetylabel;
+
+import android.os.PersistableBundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.permission.safetylabel.DataPurposeConstants.Purpose;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Data usage type representation. Types are specific to a {@link DataCategory} and contains
+ * metadata related to the data usage purpose.
+ */
+public class DataType {
+ @VisibleForTesting static final String KEY_USER_CONTROL = "user_control";
+ @VisibleForTesting static final String KEY_EPHEMERAL = "ephemeral";
+ @VisibleForTesting static final String KEY_PURPOSES = "purposes";
+
+ @Purpose private final Set<Integer> mPurposeSet;
+ private final Boolean mUserControl;
+ private final Boolean mEphemeral;
+
+ private DataType(
+ @NonNull @Purpose Set<Integer> purposeSet,
+ @Nullable Boolean userControl,
+ @Nullable Boolean ephemeral) {
+ this.mPurposeSet = purposeSet;
+ this.mUserControl = userControl;
+ this.mEphemeral = ephemeral;
+ }
+
+ /**
+ * Returns a {@link java.util.Collections.UnmodifiableMap} of String type key to {@link
+ * DataType} created by parsing a {@link PersistableBundle}
+ */
+ @NonNull
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ static Map<String, DataType> getDataTypeMap(
+ @Nullable PersistableBundle dataCategoryBundle,
+ @NonNull String dataUsage,
+ @NonNull String category) {
+ if (dataCategoryBundle == null || dataCategoryBundle.isEmpty()) {
+ return Collections.emptyMap();
+ }
+
+ Map<String, DataType> dataTypeMap = new HashMap<>();
+ Set<String> validDataTypesForCategory =
+ DataTypeConstants.getValidDataTypesForCategory(category);
+ for (String type : validDataTypesForCategory) {
+ PersistableBundle dataTypeBundle = dataCategoryBundle.getPersistableBundle(type);
+ DataType dataType = getDataType(dataTypeBundle, dataUsage);
+ if (dataType != null) {
+ dataTypeMap.put(type, dataType);
+ }
+ }
+ return Collections.unmodifiableMap(dataTypeMap);
+ }
+
+ /**
+ * Returns {@link DataType} created by parsing the {@link android.os.PersistableBundle}
+ * representation of DataType
+ */
+ @Nullable
+ @VisibleForTesting
+ static DataType getDataType(
+ @Nullable PersistableBundle dataTypeBundle, @NonNull String dataUsage) {
+ if (dataTypeBundle == null || dataTypeBundle.isEmpty()) {
+ return null;
+ }
+
+ // purposes are required, if not present, treat as invalid
+ int[] purposeList = dataTypeBundle.getIntArray(KEY_PURPOSES);
+ if (purposeList == null || purposeList.length == 0) {
+ return null;
+ }
+
+ // Filter to set of valid purposes, and return invalid if empty
+ Set<Integer> purposeSet = new HashSet<>();
+ for (int purpose : purposeList) {
+ if (DataPurposeConstants.getValidPurposes().contains(purpose)) {
+ purposeSet.add(purpose);
+ }
+ }
+ if (purposeSet.isEmpty()) {
+ return null;
+ }
+
+ // Only set/expected for DATA COLLECTED. DATA SHARED should be null
+ Boolean userControl = null;
+ Boolean ephemeral = null;
+ if (DataLabelConstants.DATA_USAGE_COLLECTED.equals(dataUsage)) {
+ userControl =
+ dataTypeBundle.containsKey(KEY_USER_CONTROL)
+ ? dataTypeBundle.getBoolean(KEY_USER_CONTROL)
+ : null;
+ ephemeral =
+ dataTypeBundle.containsKey(KEY_EPHEMERAL)
+ ? dataTypeBundle.getBoolean(KEY_EPHEMERAL)
+ : null;
+ }
+
+ return new DataType(purposeSet, userControl, ephemeral);
+ }
+
+ /**
+ * Returns {@link Set} of valid {@link Integer} purposes for using the associated data category
+ * and type
+ */
+ @NonNull
+ public Set<Integer> getPurposeSet() {
+ return mPurposeSet;
+ }
+
+ /**
+ * For data-collected, returns {@code true} if data usage is user optional and {@code false} if
+ * data usage is required. Should return {@code null} for data-shared.
+ */
+ @Nullable
+ public Boolean getUserControl() {
+ return mUserControl;
+ }
+
+ /**
+ * For data-collected, returns {@code true} if data usage is user optional and {@code false} if
+ * data usage is processed ephemerally. Should return {@code null} for data-shared.
+ */
+ @Nullable
+ public Boolean getEphemeral() {
+ return mEphemeral;
+ }
+}
diff --git a/SafetyLabel/java/com/android/permission/safetylabel/DataTypeConstants.java b/SafetyLabel/java/com/android/permission/safetylabel/DataTypeConstants.java
new file mode 100644
index 000000000..1b4819c41
--- /dev/null
+++ b/SafetyLabel/java/com/android/permission/safetylabel/DataTypeConstants.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 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 com.android.permission.safetylabel;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.StringDef;
+import android.util.ArrayMap;
+
+import java.lang.annotation.Retention;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Constants and util methods for determining valid {@link String} data categories for usage within
+ * {@link SafetyLabel}, {@link DataCategory}, and {@link DataType}
+ */
+public class DataTypeConstants {
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_PERSONAL}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "PERSONAL_",
+ value = {
+ PERSONAL_NAME,
+ PERSONAL_EMAIL_ADDRESS,
+ PERSONAL_PHYSICAL_ADDRESS,
+ PERSONAL_PHONE_NUMBER,
+ PERSONAL_RACE_ETHNICITY,
+ PERSONAL_POLITICAL_OR_RELIGIOUS_BELIEFS,
+ PERSONAL_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY,
+ PERSONAL_IDENTIFIERS,
+ PERSONAL_OTHER,
+ })
+ public @interface PersonalType {}
+
+ public static final String PERSONAL_NAME = "NAME";
+ public static final String PERSONAL_EMAIL_ADDRESS = "EMAIL_ADDRESS";
+ public static final String PERSONAL_PHYSICAL_ADDRESS = "PHYSICAL_ADDRESS";
+ public static final String PERSONAL_PHONE_NUMBER = "PHONE_NUMBER";
+ public static final String PERSONAL_RACE_ETHNICITY = "RACE_ETHNICITY";
+ public static final String PERSONAL_POLITICAL_OR_RELIGIOUS_BELIEFS =
+ "POLITICAL_OR_RELIGIOUS_BELIEFS";
+ public static final String PERSONAL_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY =
+ "SEXUAL_ORIENTATION_OR_GENDER_IDENTITY";
+ public static final String PERSONAL_IDENTIFIERS = "PERSONAL_IDENTIFIERS";
+ public static final String PERSONAL_OTHER = "OTHER";
+
+ @PersonalType
+ private static final Set<String> VALID_TYPES_PERSONAL =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(
+ PERSONAL_NAME,
+ PERSONAL_EMAIL_ADDRESS,
+ PERSONAL_PHYSICAL_ADDRESS,
+ PERSONAL_PHONE_NUMBER,
+ PERSONAL_RACE_ETHNICITY,
+ PERSONAL_POLITICAL_OR_RELIGIOUS_BELIEFS,
+ PERSONAL_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY,
+ PERSONAL_IDENTIFIERS,
+ PERSONAL_OTHER)));
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_FINANCIAL}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "FINANCIAL_",
+ value = {
+ FINANCIAL_CARD_BANK_ACCOUNT,
+ FINANCIAL_PURCHASE_HISTORY,
+ FINANCIAL_CREDIT_SCORE,
+ FINANCIAL_OTHER,
+ })
+ public @interface FinancialType {}
+
+ public static final String FINANCIAL_CARD_BANK_ACCOUNT = "CARD_BANK_ACCOUNT";
+ public static final String FINANCIAL_PURCHASE_HISTORY = "PURCHASE_HISTORY";
+ public static final String FINANCIAL_CREDIT_SCORE = "CREDIT_SCORE";
+ public static final String FINANCIAL_OTHER = "OTHER";
+
+ @FinancialType
+ private static final Set<String> VALID_TYPES_FINANCIAL =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(
+ FINANCIAL_CARD_BANK_ACCOUNT,
+ FINANCIAL_PURCHASE_HISTORY,
+ FINANCIAL_CREDIT_SCORE,
+ FINANCIAL_OTHER)));
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_LOCATION}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "LOCATION_",
+ value = {
+ LOCATION_APPROX_LOCATION,
+ LOCATION_PRECISE_LOCATION,
+ })
+ public @interface LocationType {}
+
+ public static final String LOCATION_APPROX_LOCATION = "APPROX_LOCATION";
+ public static final String LOCATION_PRECISE_LOCATION = "PRECISE_LOCATION";
+
+ @LocationType
+ private static final Set<String> VALID_TYPES_LOCATION =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(LOCATION_APPROX_LOCATION, LOCATION_PRECISE_LOCATION)));
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_EMAIL_TEXT_MESSAGE}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "EMAIL_TEXT_MESSAGE_",
+ value = {
+ EMAIL_TEXT_MESSAGE_EMAILS,
+ EMAIL_TEXT_MESSAGE_TEXT_MESSAGES,
+ EMAIL_TEXT_MESSAGE_OTHER,
+ })
+ public @interface EmailTextMessageType {}
+
+ public static final String EMAIL_TEXT_MESSAGE_EMAILS = "EMAILS";
+ public static final String EMAIL_TEXT_MESSAGE_TEXT_MESSAGES = "TEXT_MESSAGES";
+ public static final String EMAIL_TEXT_MESSAGE_OTHER = "OTHER";
+
+ @EmailTextMessageType
+ private static final Set<String> VALID_TYPES_EMAIL_TEXT_MESSAGE =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(
+ EMAIL_TEXT_MESSAGE_EMAILS,
+ EMAIL_TEXT_MESSAGE_TEXT_MESSAGES,
+ EMAIL_TEXT_MESSAGE_OTHER)));
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_PHOTO_VIDEO}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "PHOTO_VIDEO_",
+ value = {
+ PHOTO_VIDEO_PHOTOS,
+ PHOTO_VIDEO_VIDEOS,
+ })
+ public @interface PhotoVideoType {}
+
+ public static final String PHOTO_VIDEO_PHOTOS = "PHOTOS";
+ public static final String PHOTO_VIDEO_VIDEOS = "VIDEOS";
+
+ @PhotoVideoType
+ private static final Set<String> VALID_TYPES_PHOTO_VIDEO =
+ Collections.unmodifiableSet(
+ new HashSet<>(Arrays.asList(PHOTO_VIDEO_PHOTOS, PHOTO_VIDEO_VIDEOS)));
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_AUDIO}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "AUDIO_",
+ value = {AUDIO_SOUND_RECORDINGS, AUDIO_MUSIC_FILES, AUDIO_OTHER})
+ public @interface AudioType {}
+
+ public static final String AUDIO_SOUND_RECORDINGS = "SOUND_RECORDINGS";
+ public static final String AUDIO_MUSIC_FILES = "MUSIC_FILES";
+ public static final String AUDIO_OTHER = "OTHER";
+
+ @AudioType
+ private static final Set<String> VALID_TYPES_AUDIO =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(AUDIO_SOUND_RECORDINGS, AUDIO_MUSIC_FILES, AUDIO_OTHER)));
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_STORAGE}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "STORAGE_",
+ value = {
+ STORAGE_FILES_DOCS,
+ })
+ public @interface StorageType {}
+
+ public static final String STORAGE_FILES_DOCS = "FILES_DOCS";
+
+ @StorageType
+ private static final Set<String> VALID_TYPES_STORAGE =
+ Collections.unmodifiableSet(new HashSet<>(Arrays.asList(STORAGE_FILES_DOCS)));
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_HEALTH_FITNESS}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "HEALTH_FITNESS_",
+ value = {
+ HEALTH_FITNESS_HEALTH,
+ HEALTH_FITNESS_FITNESS,
+ })
+ public @interface HealthFitnessType {}
+
+ public static final String HEALTH_FITNESS_HEALTH = "HEALTH";
+ public static final String HEALTH_FITNESS_FITNESS = "FITNESS";
+
+ @HealthFitnessType
+ private static final Set<String> VALID_TYPES_HEALTH_FITNESS =
+ Collections.unmodifiableSet(
+ new HashSet<>(Arrays.asList(HEALTH_FITNESS_HEALTH, HEALTH_FITNESS_FITNESS)));
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_CONTACTS}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "CONTACTS_",
+ value = {
+ CONTACTS_CONTACTS,
+ })
+ public @interface ContactsType {}
+
+ public static final String CONTACTS_CONTACTS = "CONTACTS";
+
+ @ContactsType
+ private static final Set<String> VALID_TYPES_CONTACTS =
+ Collections.unmodifiableSet(new HashSet<>(Arrays.asList(CONTACTS_CONTACTS)));
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_CALENDAR}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "CALENDAR_",
+ value = {
+ CALENDAR_CALENDAR,
+ })
+ public @interface CalendarType {}
+
+ public static final String CALENDAR_CALENDAR = "CALENDAR";
+
+ @CalendarType
+ private static final Set<String> VALID_TYPES_CALENDAR =
+ Collections.unmodifiableSet(new HashSet<>(Arrays.asList(CALENDAR_CALENDAR)));
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_IDENTIFIERS}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "IDENTIFIERS_",
+ value = {
+ IDENTIFIERS_OTHER,
+ })
+ public @interface IdentifiersType {}
+
+ public static final String IDENTIFIERS_OTHER = "OTHER";
+
+ @IdentifiersType
+ private static final Set<String> VALID_TYPES_IDENTIFIERS =
+ Collections.unmodifiableSet(new HashSet<>(Arrays.asList(IDENTIFIERS_OTHER)));
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_APP_PERFORMANCE}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "APP_PERFORMANCE_",
+ value = {
+ APP_PERFORMANCE_CRASH_LOGS,
+ APP_PERFORMANCE_PERFORMANCE_DIAGNOSTICS,
+ APP_PERFORMANCE_OTHER,
+ })
+ public @interface AppPerformanceType {}
+
+ public static final String APP_PERFORMANCE_CRASH_LOGS = "CRASH_LOGS";
+ public static final String APP_PERFORMANCE_PERFORMANCE_DIAGNOSTICS = "PERFORMANCE_DIAGNOSTICS";
+ public static final String APP_PERFORMANCE_OTHER = "OTHER";
+
+ @AppPerformanceType
+ private static final Set<String> VALID_TYPES_APP_PERFORMANCE =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(
+ APP_PERFORMANCE_CRASH_LOGS,
+ APP_PERFORMANCE_PERFORMANCE_DIAGNOSTICS,
+ APP_PERFORMANCE_OTHER)));
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_ACTIONS_IN_APP}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "ACTIONS_IN_APP_",
+ value = {
+ ACTIONS_IN_APP_USER_INTERACTION,
+ ACTIONS_IN_APP_IN_APP_SEARCH_HISTORY,
+ ACTIONS_IN_APP_INSTALLED_APPS,
+ ACTIONS_IN_APP_USER_GENERATED_CONTENT,
+ ACTIONS_IN_APP_OTHER,
+ })
+ public @interface ActionsInAppType {}
+
+ public static final String ACTIONS_IN_APP_USER_INTERACTION = "USER_INTERACTION";
+ public static final String ACTIONS_IN_APP_IN_APP_SEARCH_HISTORY = "IN_APP_SEARCH_HISTORY";
+ public static final String ACTIONS_IN_APP_INSTALLED_APPS = "INSTALLED_APPS";
+ public static final String ACTIONS_IN_APP_USER_GENERATED_CONTENT = "USER_GENERATED_CONTENT";
+ public static final String ACTIONS_IN_APP_OTHER = "OTHER";
+
+ @ActionsInAppType
+ private static final Set<String> VALID_TYPES_ACTIONS_IN_APP =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(
+ ACTIONS_IN_APP_USER_INTERACTION,
+ ACTIONS_IN_APP_IN_APP_SEARCH_HISTORY,
+ ACTIONS_IN_APP_INSTALLED_APPS,
+ ACTIONS_IN_APP_USER_GENERATED_CONTENT,
+ ACTIONS_IN_APP_OTHER)));
+
+ /**
+ * List of valid Safety Label data collection/sharing types for {@link
+ * DataCategoryConstants#CATEGORY_SEARCH_AND_BROWSING}
+ */
+ @Retention(SOURCE)
+ @StringDef(
+ prefix = "SEARCH_AND_BROWSING_",
+ value = {
+ SEARCH_AND_BROWSING_WEB_BROWSING_HISTORY,
+ })
+ public @interface SearchAndBrowsingType {}
+
+ public static final String SEARCH_AND_BROWSING_WEB_BROWSING_HISTORY = "WEB_BROWSING_HISTORY";
+
+ @SearchAndBrowsingType
+ private static final Set<String> VALID_TYPES_SEARCH_AND_BROWSING =
+ Collections.unmodifiableSet(
+ new HashSet<>(Arrays.asList(SEARCH_AND_BROWSING_WEB_BROWSING_HISTORY)));
+
+ private static final Map<String, Set<String>> VALID_TYPES_FOR_CATEGORY_MAP;
+
+ /** Returns {@link Set} of valid types for the specified {@link String} category key */
+ public static Set<String> getValidDataTypesForCategory(
+ @DataCategoryConstants.Category String category) {
+ return VALID_TYPES_FOR_CATEGORY_MAP.containsKey(category)
+ ? VALID_TYPES_FOR_CATEGORY_MAP.get(category)
+ : Collections.emptySet();
+ }
+
+ static {
+ VALID_TYPES_FOR_CATEGORY_MAP = new ArrayMap<>();
+ VALID_TYPES_FOR_CATEGORY_MAP.put(
+ DataCategoryConstants.CATEGORY_PERSONAL, VALID_TYPES_PERSONAL);
+ VALID_TYPES_FOR_CATEGORY_MAP.put(
+ DataCategoryConstants.CATEGORY_FINANCIAL, VALID_TYPES_FINANCIAL);
+ VALID_TYPES_FOR_CATEGORY_MAP.put(
+ DataCategoryConstants.CATEGORY_LOCATION, VALID_TYPES_LOCATION);
+ VALID_TYPES_FOR_CATEGORY_MAP.put(
+ DataCategoryConstants.CATEGORY_EMAIL_TEXT_MESSAGE, VALID_TYPES_EMAIL_TEXT_MESSAGE);
+ VALID_TYPES_FOR_CATEGORY_MAP.put(
+ DataCategoryConstants.CATEGORY_PHOTO_VIDEO, VALID_TYPES_PHOTO_VIDEO);
+ VALID_TYPES_FOR_CATEGORY_MAP.put(DataCategoryConstants.CATEGORY_AUDIO, VALID_TYPES_AUDIO);
+ VALID_TYPES_FOR_CATEGORY_MAP.put(
+ DataCategoryConstants.CATEGORY_STORAGE, VALID_TYPES_STORAGE);
+ VALID_TYPES_FOR_CATEGORY_MAP.put(
+ DataCategoryConstants.CATEGORY_HEALTH_FITNESS, VALID_TYPES_HEALTH_FITNESS);
+ VALID_TYPES_FOR_CATEGORY_MAP.put(
+ DataCategoryConstants.CATEGORY_CONTACTS, VALID_TYPES_CONTACTS);
+ VALID_TYPES_FOR_CATEGORY_MAP.put(
+ DataCategoryConstants.CATEGORY_CALENDAR, VALID_TYPES_CALENDAR);
+ VALID_TYPES_FOR_CATEGORY_MAP.put(
+ DataCategoryConstants.CATEGORY_IDENTIFIERS, VALID_TYPES_IDENTIFIERS);
+ VALID_TYPES_FOR_CATEGORY_MAP.put(
+ DataCategoryConstants.CATEGORY_APP_PERFORMANCE, VALID_TYPES_APP_PERFORMANCE);
+ VALID_TYPES_FOR_CATEGORY_MAP.put(
+ DataCategoryConstants.CATEGORY_ACTIONS_IN_APP, VALID_TYPES_ACTIONS_IN_APP);
+ VALID_TYPES_FOR_CATEGORY_MAP.put(
+ DataCategoryConstants.CATEGORY_SEARCH_AND_BROWSING,
+ VALID_TYPES_SEARCH_AND_BROWSING);
+ }
+
+ private DataTypeConstants() {
+ /* do nothing - hide constructor */
+ }
+}
diff --git a/SafetyLabel/java/com/android/permission/safetylabel/SafetyLabel.java b/SafetyLabel/java/com/android/permission/safetylabel/SafetyLabel.java
new file mode 100644
index 000000000..a91ff901f
--- /dev/null
+++ b/SafetyLabel/java/com/android/permission/safetylabel/SafetyLabel.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 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 com.android.permission.safetylabel;
+
+import android.os.PersistableBundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+/** Safety Label representation containing zero or more {@link DataCategory} for data shared */
+public class SafetyLabel {
+ @VisibleForTesting public static final String KEY_SAFETY_LABEL = "safety_label";
+ private final DataLabel mDataLabel;
+
+ private SafetyLabel(@NonNull DataLabel dataLabel) {
+ this.mDataLabel = dataLabel;
+ }
+
+ /** Returns {@link SafetyLabel} created by parsing a metadata {@link PersistableBundle} */
+ @Nullable
+ public static SafetyLabel getSafetyLabelFromMetadata(@Nullable PersistableBundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+
+ PersistableBundle safetyLabelBundle = bundle.getPersistableBundle(KEY_SAFETY_LABEL);
+ if (safetyLabelBundle == null) {
+ return null;
+ }
+
+ DataLabel dataLabel = DataLabel.getDataLabel(safetyLabelBundle);
+ if (dataLabel == null) {
+ return null;
+ }
+
+ return new SafetyLabel(dataLabel);
+ }
+
+ /** Returns the data label for the safety label */
+ @NonNull
+ public DataLabel getDataLabel() {
+ return mDataLabel;
+ }
+}
diff --git a/SafetyLabel/tests/Android.bp b/SafetyLabel/tests/Android.bp
new file mode 100644
index 000000000..2026a6ac8
--- /dev/null
+++ b/SafetyLabel/tests/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "SafetyLabelTests",
+ defaults: ["mts-target-sdk-version-current"],
+ sdk_version: "test_current",
+ min_sdk_version: "30",
+ target_sdk_version: "32",
+ srcs: [
+ "java/**/*.kt",
+ ],
+ per_testcase_directory: true,
+ static_libs: [
+ "compatibility-device-util-axt",
+ "kotlinx-coroutines-android",
+ "safety-label",
+ ],
+ test_suites: [
+ "general-tests",
+ "mts-permission",
+ ],
+}
diff --git a/SafetyLabel/tests/AndroidManifest.xml b/SafetyLabel/tests/AndroidManifest.xml
new file mode 100644
index 000000000..4d1e62905
--- /dev/null
+++ b/SafetyLabel/tests/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 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.
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.permission.safetylabel.tests">
+
+ <application android:label="Safety Label Tests">
+ <uses-library android:name="android.test.runner"/>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.permission.safetylabel.tests"
+ android:label="Tests for the Safety Label library"/>
+</manifest>
diff --git a/SafetyLabel/tests/AndroidTest.xml b/SafetyLabel/tests/AndroidTest.xml
new file mode 100644
index 000000000..919e7ce2a
--- /dev/null
+++ b/SafetyLabel/tests/AndroidTest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 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.
+ -->
+
+<configuration description="Runs unit tests for the Safety Label library.">
+ <option name="test-tag" value="SafetyLabelTests"/>
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.Sdk33ModuleController"/>
+
+ <!-- Install test -->
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="SafetyLabelTests.apk"/>
+ <option name="cleanup-apks" value="true"/>
+ </target_preparer>
+
+ <!-- Create place to store apks -->
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="mkdir -p /data/local/tmp/safetylabel/tests/"/>
+ <option name="teardown-command" value="rm -rf /data/local/tmp/safetylabel/"/>
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="com.android.permission.safetylabel.tests"/>
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/>
+ </test>
+</configuration>
diff --git a/SafetyLabel/tests/java/com/android/permission/safetylabel/DataCategoryTest.kt b/SafetyLabel/tests/java/com/android/permission/safetylabel/DataCategoryTest.kt
new file mode 100644
index 000000000..8ed705064
--- /dev/null
+++ b/SafetyLabel/tests/java/com/android/permission/safetylabel/DataCategoryTest.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 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 com.android.permission.safetylabel
+
+import android.os.PersistableBundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.permission.safetylabel.DataCategoryConstants.CATEGORY_LOCATION
+import com.android.permission.safetylabel.DataCategoryConstants.VALID_CATEGORIES
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.INVALID_KEY
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createCategoryMapPersistableBundle
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createCategoryMapPersistableBundleWithInvalidTypes
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createDataLabelPersistableBundle
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createDataLabelPersistableBundleWithAdditonalInvalidCategory
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createInvalidCategoryMapPersistableBundle
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createInvalidDataLabelPersistableBundle
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createTypePersistableBundle
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** CTS tests for [DataCategory]. */
+@RunWith(AndroidJUnit4::class)
+class DataCategoryTest {
+ @Test
+ fun getDataCategoryMap_dataUsageCollected_nullPersistableBundle_emptyMap() {
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(null, DataLabelConstants.DATA_USAGE_COLLECTED)
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap).isEmpty()
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageCollected_emptyPersistableBundle_emptyMap() {
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(
+ PersistableBundle.EMPTY, DataLabelConstants.DATA_USAGE_COLLECTED)
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap).isEmpty()
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageCollected_invalidBundle_emptyMap() {
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(
+ createInvalidDataLabelPersistableBundle(), DataLabelConstants.DATA_USAGE_COLLECTED)
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap).isEmpty()
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageCollected_validBundle_hasAllValidCategories() {
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(
+ createDataLabelPersistableBundle(), DataLabelConstants.DATA_USAGE_COLLECTED)
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap.keys).containsExactlyElementsIn(VALID_CATEGORIES)
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageCollected_additionalInvalidCategory_hasOnlyValidCategories() {
+ // Create valid data label and put an additional invalid/unknown category key
+ val dataLabelBundle = createDataLabelPersistableBundleWithAdditonalInvalidCategory()
+
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(
+ dataLabelBundle, DataLabelConstants.DATA_USAGE_COLLECTED)
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap.keys).containsExactlyElementsIn(VALID_CATEGORIES)
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageShared_nullPersistableBundle_emptyMap() {
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(null, DataLabelConstants.DATA_USAGE_SHARED)
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap).isEmpty()
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageShared_emptyPersistableBundle_emptyMap() {
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(
+ PersistableBundle.EMPTY, DataLabelConstants.DATA_USAGE_SHARED)
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap).isEmpty()
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageShared_invalidBundle_emptyMap() {
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(
+ createInvalidDataLabelPersistableBundle(), DataLabelConstants.DATA_USAGE_SHARED)
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap).isEmpty()
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageShared_validBundle_hasAllValidCategories() {
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(
+ createDataLabelPersistableBundle(), DataLabelConstants.DATA_USAGE_SHARED)
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap.keys).containsExactlyElementsIn(VALID_CATEGORIES)
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageShared_additionalInvalidCategory_hasOnlyValidCategories() {
+ // Create valid data label and put an additional invalid/unknown category key
+ val dataLabelBundle = createDataLabelPersistableBundleWithAdditonalInvalidCategory()
+
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(dataLabelBundle, DataLabelConstants.DATA_USAGE_SHARED)
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap.keys).containsExactlyElementsIn(VALID_CATEGORIES)
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageInvalid_nullPersistableBundle_emptyMap() {
+ val dataCategoryMap = DataCategory.getDataCategoryMap(null, "invalid_datausage_key")
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap).isEmpty()
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageInvalid_emptyPersistableBundle_emptyMap() {
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(PersistableBundle.EMPTY, "invalid_datausage_key")
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap).isEmpty()
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageInvalid_invalidBundle_emptyMap() {
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(
+ createInvalidDataLabelPersistableBundle(), "invalid_datausage_key")
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap).isEmpty()
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageInvalid_validBundle_emptyMap() {
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(
+ createDataLabelPersistableBundle(), "invalid_datausage_key")
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap).isEmpty()
+ }
+
+ @Test
+ fun getDataCategoryMap_dataUsageInvalid_validBundleWithAdditionalInvalidCategory_null() {
+ // Create valid data label and put an additional invalid/unknown category key
+ val dataLabelBundle = createDataLabelPersistableBundleWithAdditonalInvalidCategory()
+
+ val dataCategoryMap =
+ DataCategory.getDataCategoryMap(dataLabelBundle, "invalid_datausage_key")
+
+ assertThat(dataCategoryMap).isNotNull()
+ assertThat(dataCategoryMap).isEmpty()
+ }
+
+ @Test
+ fun getDataCategory_nullBundle_nullDataCategory() {
+ val dataCategory =
+ DataCategory.getDataCategory(
+ null, DataLabelConstants.DATA_USAGE_SHARED, CATEGORY_LOCATION)
+
+ assertThat(dataCategory).isNull()
+ }
+
+ @Test
+ fun getDataCategory_emptyBundle_nullDataCategory() {
+ val dataCategory =
+ DataCategory.getDataCategory(
+ PersistableBundle.EMPTY, DataLabelConstants.DATA_USAGE_SHARED, CATEGORY_LOCATION)
+
+ assertThat(dataCategory).isNull()
+ }
+
+ @Test
+ fun getDataCategory_invalidBundle_nullDataCategory() {
+ val dataCategory =
+ DataCategory.getDataCategory(
+ createInvalidCategoryMapPersistableBundle(),
+ DataLabelConstants.DATA_USAGE_SHARED,
+ CATEGORY_LOCATION)
+
+ assertThat(dataCategory).isNull()
+ }
+
+ @Test
+ fun getDataCategory_validCategoriesAndInvalidType_nullDataCategory() {
+ val dataCategory =
+ DataCategory.getDataCategory(
+ createCategoryMapPersistableBundleWithInvalidTypes(),
+ DataLabelConstants.DATA_USAGE_SHARED,
+ INVALID_KEY)
+
+ assertThat(dataCategory).isNull()
+ }
+
+ @Test
+ fun getDataCategory_validBundle_validCategoryAndExpectedTypes() {
+ val dataCategory =
+ DataCategory.getDataCategory(
+ createCategoryMapPersistableBundle(),
+ DataLabelConstants.DATA_USAGE_SHARED,
+ CATEGORY_LOCATION)
+
+ assertThat(dataCategory).isNotNull()
+ assertThat(dataCategory?.dataTypes).isNotEmpty()
+ assertThat(dataCategory?.dataTypes?.keys)
+ .containsExactlyElementsIn(
+ DataTypeConstants.getValidDataTypesForCategory(CATEGORY_LOCATION))
+ }
+
+ @Test
+ fun getDataCategory_validBundleWithAddedInvalidType_validCategoryAndOnlyExpectedTypes() {
+ // Create valid bundle with additional invalid type
+ val dataTypeMapBundle = createCategoryMapPersistableBundle()
+ dataTypeMapBundle.putPersistableBundle(INVALID_KEY, createTypePersistableBundle())
+
+ val dataCategory =
+ DataCategory.getDataCategory(
+ dataTypeMapBundle, DataLabelConstants.DATA_USAGE_SHARED, CATEGORY_LOCATION)
+
+ assertThat(dataCategory).isNotNull()
+ assertThat(dataCategory?.dataTypes).isNotEmpty()
+ assertThat(dataCategory?.dataTypes?.keys)
+ .containsExactlyElementsIn(
+ DataTypeConstants.getValidDataTypesForCategory(CATEGORY_LOCATION))
+ }
+}
diff --git a/SafetyLabel/tests/java/com/android/permission/safetylabel/DataLabelTest.kt b/SafetyLabel/tests/java/com/android/permission/safetylabel/DataLabelTest.kt
new file mode 100644
index 000000000..1c0c56238
--- /dev/null
+++ b/SafetyLabel/tests/java/com/android/permission/safetylabel/DataLabelTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 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 com.android.permission.safetylabel
+
+import android.os.PersistableBundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createInvalidSafetyLabelPersistableBundle
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createSafetyLabelPersistableBundle
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createSafetyLabelPersistableBundleWithEmptyDataCollected
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createSafetyLabelPersistableBundleWithEmptyDataShared
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createSafetyLabelPersistableBundleWithInvalidDataCollected
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createSafetyLabelPersistableBundleWithInvalidDataShared
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createSafetyLabelPersistableBundleWithNullDataCollected
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createSafetyLabelPersistableBundleWithNullDataShared
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** CTS tests for [DataLabel]. */
+@RunWith(AndroidJUnit4::class)
+class DataLabelTest {
+ @Test
+ fun getDataLabel_nullBundle_nullDataLabel() {
+ val dataLabel: DataLabel? = DataLabel.getDataLabel(null)
+
+ assertThat(dataLabel).isNull()
+ }
+
+ @Test
+ fun getDataLabel_emptyBundle_nullDataLabel() {
+ val dataLabel: DataLabel? = DataLabel.getDataLabel(PersistableBundle.EMPTY)
+
+ assertThat(dataLabel).isNull()
+ }
+
+ @Test
+ fun getDataLabel_invalidBundle_nullDataLabel() {
+ val dataLabel: DataLabel? =
+ DataLabel.getDataLabel(createInvalidSafetyLabelPersistableBundle())
+
+ assertThat(dataLabel).isNull()
+ }
+
+ @Test
+ fun getDataLabel_nullDataCollectedBundle_dataCollectedEmpty() {
+ val dataLabel: DataLabel? =
+ DataLabel.getDataLabel(createSafetyLabelPersistableBundleWithNullDataCollected())
+
+ assertThat(dataLabel).isNotNull()
+ assertThat(dataLabel?.dataCollected).isEmpty()
+ assertThat(dataLabel?.dataShared).isNotEmpty()
+ }
+
+ @Test
+ fun getDataLabel_nullDataSharedBundle_dataSharedEmpty() {
+ val dataLabel: DataLabel? =
+ DataLabel.getDataLabel(createSafetyLabelPersistableBundleWithNullDataShared())
+
+ assertThat(dataLabel).isNotNull()
+ assertThat(dataLabel?.dataCollected).isNotEmpty()
+ assertThat(dataLabel?.dataShared).isEmpty()
+ }
+
+ @Test
+ fun getDataLabel_emptyDataCollectedBundle_dataCollectedEmpty() {
+ val dataLabel: DataLabel? =
+ DataLabel.getDataLabel(createSafetyLabelPersistableBundleWithEmptyDataCollected())
+
+ assertThat(dataLabel).isNotNull()
+ assertThat(dataLabel?.dataCollected).isEmpty()
+ assertThat(dataLabel?.dataShared).isNotEmpty()
+ }
+
+ @Test
+ fun getDataLabel_emptyDataSharedBundle_dataSharedEmpty() {
+ val dataLabel: DataLabel? =
+ DataLabel.getDataLabel(createSafetyLabelPersistableBundleWithEmptyDataShared())
+
+ assertThat(dataLabel).isNotNull()
+ assertThat(dataLabel?.dataCollected).isNotEmpty()
+ assertThat(dataLabel?.dataShared).isEmpty()
+ }
+
+ @Test
+ fun getDataLabel_invalidDataCollectedBundle_dataCollectedEmpty() {
+ val dataLabel: DataLabel? =
+ DataLabel.getDataLabel(createSafetyLabelPersistableBundleWithInvalidDataCollected())
+
+ assertThat(dataLabel).isNotNull()
+ assertThat(dataLabel?.dataCollected).isEmpty()
+ assertThat(dataLabel?.dataShared).isNotEmpty()
+ }
+
+ @Test
+ fun getDataLabel_invalidDataSharedBundle_dataSharedEmpty() {
+ val dataLabel: DataLabel? =
+ DataLabel.getDataLabel(createSafetyLabelPersistableBundleWithInvalidDataShared())
+
+ assertThat(dataLabel).isNotNull()
+ assertThat(dataLabel?.dataCollected).isNotEmpty()
+ assertThat(dataLabel?.dataShared).isEmpty()
+ }
+
+ @Test
+ fun getDataLabel_validBundle() {
+ val dataLabel: DataLabel? = DataLabel.getDataLabel(createSafetyLabelPersistableBundle())
+
+ assertThat(dataLabel).isNotNull()
+ assertThat(dataLabel?.dataCollected).isNotEmpty()
+ assertThat(dataLabel?.dataShared).isNotEmpty()
+ }
+}
diff --git a/SafetyLabel/tests/java/com/android/permission/safetylabel/DataTypeTest.kt b/SafetyLabel/tests/java/com/android/permission/safetylabel/DataTypeTest.kt
new file mode 100644
index 000000000..bfc9be726
--- /dev/null
+++ b/SafetyLabel/tests/java/com/android/permission/safetylabel/DataTypeTest.kt
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 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 com.android.permission.safetylabel
+
+import android.os.PersistableBundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.permission.safetylabel.DataCategoryConstants.CATEGORY_LOCATION
+import com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_ADVERTISING
+import com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_APP_FUNCTIONALITY
+import com.android.permission.safetylabel.DataType.KEY_EPHEMERAL
+import com.android.permission.safetylabel.DataType.KEY_PURPOSES
+import com.android.permission.safetylabel.DataType.KEY_USER_CONTROL
+import com.android.permission.safetylabel.DataTypeConstants.LOCATION_APPROX_LOCATION
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.INVALID_KEY
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createInvalidTypeMapPersistableBundle
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createInvalidTypePersistableBundle
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createTypeMapPersistableBundle
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createTypeMapWithInvalidTypeDataPersistableBundle
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createTypePersistableBundle
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** CTS tests for [DataType]. */
+@RunWith(AndroidJUnit4::class)
+class DataTypeTest {
+ @Test
+ fun getDataTypeMap_invalidCategory_nullPersistableBundle_emptyMap() {
+ val dataTypeMap =
+ DataType.getDataTypeMap(null, DataLabelConstants.DATA_USAGE_SHARED, INVALID_KEY)
+
+ assertThat(dataTypeMap).isNotNull()
+ assertThat(dataTypeMap).isEmpty()
+ }
+
+ @Test
+ fun getDataTypeMap_invalidCategory_emptyPersistableBundle_emptyMap() {
+ val dataTypeMap =
+ DataType.getDataTypeMap(
+ PersistableBundle.EMPTY, DataLabelConstants.DATA_USAGE_SHARED, INVALID_KEY)
+
+ assertThat(dataTypeMap).isNotNull()
+ assertThat(dataTypeMap).isEmpty()
+ }
+
+ @Test
+ fun getDataTypeMap_invalidCategory_invalidBundle_emptyMap() {
+ val dataTypeMap =
+ DataType.getDataTypeMap(
+ createInvalidTypeMapPersistableBundle(),
+ DataLabelConstants.DATA_USAGE_SHARED,
+ INVALID_KEY)
+
+ assertThat(dataTypeMap).isNotNull()
+ assertThat(dataTypeMap).isEmpty()
+ }
+
+ @Test
+ fun getDataTypeMap_invalidCategory_validBundle_emptyMap() {
+ val dataTypeMap =
+ DataType.getDataTypeMap(
+ createTypeMapPersistableBundle(CATEGORY_LOCATION),
+ DataLabelConstants.DATA_USAGE_SHARED,
+ INVALID_KEY)
+
+ assertThat(dataTypeMap).isNotNull()
+ assertThat(dataTypeMap).isEmpty()
+ }
+
+ @Test
+ fun getDataTypeMap_validCategory_nullPersistableBundle_emptyMap() {
+ val dataTypeMap =
+ DataType.getDataTypeMap(null, DataLabelConstants.DATA_USAGE_SHARED, CATEGORY_LOCATION)
+
+ assertThat(dataTypeMap).isNotNull()
+ assertThat(dataTypeMap).isEmpty()
+ }
+
+ @Test
+ fun getDataTypeMap_validCategory_emptyPersistableBundle_emptyMap() {
+ val dataTypeMap =
+ DataType.getDataTypeMap(
+ PersistableBundle.EMPTY, DataLabelConstants.DATA_USAGE_SHARED, CATEGORY_LOCATION)
+
+ assertThat(dataTypeMap).isNotNull()
+ assertThat(dataTypeMap).isEmpty()
+ }
+
+ @Test
+ fun getDataTypeMap_validCategory_invalidBundle_emptyMap() {
+ val dataTypeMap =
+ DataType.getDataTypeMap(
+ createInvalidTypeMapPersistableBundle(),
+ DataLabelConstants.DATA_USAGE_SHARED,
+ CATEGORY_LOCATION)
+
+ assertThat(dataTypeMap).isNotNull()
+ assertThat(dataTypeMap).isEmpty()
+ }
+
+ @Test
+ fun getDataTypeMap_validCategory_validBundle_hasAllExpectedTypes() {
+ val typeMapPersistableBundle = createTypeMapPersistableBundle(CATEGORY_LOCATION)
+
+ val dataTypeMap =
+ DataType.getDataTypeMap(
+ typeMapPersistableBundle, DataLabelConstants.DATA_USAGE_SHARED, CATEGORY_LOCATION)
+
+ assertThat(dataTypeMap).isNotNull()
+ assertThat(dataTypeMap.keys)
+ .containsExactlyElementsIn(
+ DataTypeConstants.getValidDataTypesForCategory(CATEGORY_LOCATION))
+ }
+
+ @Test
+ fun getDataTypeMap_validCategory_validBundleWithAddedInvalidType_hasOnlyExpectedTypes() {
+ val typeMapPersistableBundle = createTypeMapPersistableBundle(CATEGORY_LOCATION)
+ // Add additional valid persistable bundle under invalid key
+ typeMapPersistableBundle.putPersistableBundle(INVALID_KEY, createTypePersistableBundle())
+
+ val dataTypeMap =
+ DataType.getDataTypeMap(
+ typeMapPersistableBundle, DataLabelConstants.DATA_USAGE_SHARED, CATEGORY_LOCATION)
+
+ assertThat(dataTypeMap).isNotNull()
+ assertThat(dataTypeMap.keys)
+ .containsExactlyElementsIn(
+ DataTypeConstants.getValidDataTypesForCategory(CATEGORY_LOCATION))
+ assertThat(dataTypeMap.keys).doesNotContain(INVALID_KEY)
+ }
+
+ @Test
+ fun getDataTypeMap_validCategory_validType_invalidData_emptyMap() {
+ val typeMapPersistableBundle =
+ createTypeMapWithInvalidTypeDataPersistableBundle(CATEGORY_LOCATION)
+
+ val dataTypeMap =
+ DataType.getDataTypeMap(
+ typeMapPersistableBundle, DataLabelConstants.DATA_USAGE_SHARED, CATEGORY_LOCATION)
+
+ assertThat(dataTypeMap).isNotNull()
+ assertThat(dataTypeMap).isEmpty()
+ }
+
+ @Test
+ fun getDataTypeMap_dataCollected_validCategory_validBundle_validateSingleExpectedType() {
+ val dataTypeMap =
+ DataType.getDataTypeMap(
+ createTypeMapPersistableBundle(CATEGORY_LOCATION),
+ DataLabelConstants.DATA_USAGE_COLLECTED,
+ CATEGORY_LOCATION)
+
+ assertThat(dataTypeMap).isNotNull()
+ assertThat(dataTypeMap).containsKey(LOCATION_APPROX_LOCATION)
+ val type = dataTypeMap[LOCATION_APPROX_LOCATION]!!
+ assertThat(type.purposeSet).containsExactly(PURPOSE_APP_FUNCTIONALITY, PURPOSE_ADVERTISING)
+ assertThat(type.userControl).isTrue()
+ assertThat(type.ephemeral).isTrue()
+ }
+
+ @Test
+ fun getDataTypeMap_dataShared_validCategory_validBundle_validateSingleExpectedType() {
+ val dataTypeMap =
+ DataType.getDataTypeMap(
+ createTypeMapPersistableBundle(CATEGORY_LOCATION),
+ DataLabelConstants.DATA_USAGE_SHARED,
+ CATEGORY_LOCATION)
+
+ assertThat(dataTypeMap).isNotNull()
+ assertThat(dataTypeMap).containsKey(LOCATION_APPROX_LOCATION)
+ val type = dataTypeMap[LOCATION_APPROX_LOCATION]!!
+ assertThat(type.purposeSet).containsExactly(PURPOSE_APP_FUNCTIONALITY, PURPOSE_ADVERTISING)
+ assertThat(type.userControl).isNull()
+ assertThat(type.ephemeral).isNull()
+ }
+
+ @Test
+ fun getDataType_invalidBundle_nullDataType() {
+ val dataType =
+ DataType.getDataType(
+ createInvalidTypePersistableBundle(), DataLabelConstants.DATA_USAGE_SHARED)
+
+ assertThat(dataType).isNull()
+ }
+
+ @Test
+ fun getDataType_dataCollected_validDataType() {
+ val dataType =
+ DataType.getDataType(
+ createTypePersistableBundle(), DataLabelConstants.DATA_USAGE_COLLECTED)
+
+ assertThat(dataType).isNotNull()
+ assertThat(dataType?.purposeSet)
+ .containsExactly(PURPOSE_APP_FUNCTIONALITY, PURPOSE_ADVERTISING)
+ assertThat(dataType?.userControl).isTrue()
+ assertThat(dataType?.ephemeral).isTrue()
+ }
+
+ @Test
+ fun getDataType_dataShared_validDataType() {
+ val dataType =
+ DataType.getDataType(
+ createTypePersistableBundle(), DataLabelConstants.DATA_USAGE_SHARED)
+
+ assertThat(dataType).isNotNull()
+ assertThat(dataType?.purposeSet)
+ .containsExactly(PURPOSE_APP_FUNCTIONALITY, PURPOSE_ADVERTISING)
+ assertThat(dataType?.userControl).isNull()
+ assertThat(dataType?.ephemeral).isNull()
+ }
+
+ @Test
+ fun getDataType_validDataTypeWithAddedInvalidPurpose_onlyValidPurposes() {
+ val typePersistableBundle = createTypePersistableBundle()
+ val purposes: IntArray = typePersistableBundle.getIntArray(KEY_PURPOSES)!!
+ val updatedPurposes: IntArray = intArrayOf(-1, *purposes)
+ typePersistableBundle.putIntArray(KEY_PURPOSES, updatedPurposes)
+
+ val dataType =
+ DataType.getDataType(typePersistableBundle, DataLabelConstants.DATA_USAGE_SHARED)
+
+ assertThat(dataType).isNotNull()
+ // Should not contain the additional "-1" purpose added above
+ assertThat(dataType?.purposeSet)
+ .containsExactly(PURPOSE_APP_FUNCTIONALITY, PURPOSE_ADVERTISING)
+ }
+
+ @Test
+ fun getDataType_dataTypeWithInvalidPurpose_nullDataType() {
+ val typePersistableBundle = createTypePersistableBundle()
+ typePersistableBundle.remove(KEY_PURPOSES)
+ val updatedPurposes: IntArray = intArrayOf(-1)
+ typePersistableBundle.putIntArray(KEY_PURPOSES, updatedPurposes)
+
+ val dataType =
+ DataType.getDataType(typePersistableBundle, DataLabelConstants.DATA_USAGE_SHARED)
+
+ assertThat(dataType).isNull()
+ }
+
+ @Test
+ fun getDataType_noPurpose_nullDataType() {
+ val typePersistableBundle = createTypePersistableBundle()
+ typePersistableBundle.remove(KEY_PURPOSES)
+
+ val dataType =
+ DataType.getDataType(typePersistableBundle, DataLabelConstants.DATA_USAGE_SHARED)
+
+ assertThat(dataType).isNull()
+ }
+
+ @Test
+ fun getDataType_dataCollected_validDataType_noUserControl_noEphemeral() {
+ val bundle = createTypePersistableBundle()
+ bundle.remove(KEY_USER_CONTROL)
+ bundle.remove(KEY_EPHEMERAL)
+
+ val dataType = DataType.getDataType(bundle, DataLabelConstants.DATA_USAGE_COLLECTED)
+
+ assertThat(dataType).isNotNull()
+ assertThat(dataType?.purposeSet)
+ .containsExactly(PURPOSE_APP_FUNCTIONALITY, PURPOSE_ADVERTISING)
+ assertThat(dataType?.userControl).isNull()
+ assertThat(dataType?.ephemeral).isNull()
+ }
+}
diff --git a/SafetyLabel/tests/java/com/android/permission/safetylabel/SafetyLabelTest.kt b/SafetyLabel/tests/java/com/android/permission/safetylabel/SafetyLabelTest.kt
new file mode 100644
index 000000000..ce9f24634
--- /dev/null
+++ b/SafetyLabel/tests/java/com/android/permission/safetylabel/SafetyLabelTest.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 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 com.android.permission.safetylabel
+
+import android.os.PersistableBundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createInvalidMetadataPersistableBundle
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createMetadataPersistableBundle
+import com.android.permission.safetylabel.SafetyLabelTestPersistableBundles.createMetadataPersistableBundleWithInvalidSafetyLabel
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** CTS tests for [SafetyLabel]. */
+@RunWith(AndroidJUnit4::class)
+class SafetyLabelTest {
+
+ @Test
+ fun getSafetyLabelFromMetaData_nullMetadataBundle_nullSafetyLabel() {
+ val safetyLabel = SafetyLabel.getSafetyLabelFromMetadata(null)
+
+ assertThat(safetyLabel).isNull()
+ }
+
+ @Test
+ fun getSafetyLabelFromMetaData_emptyMetadataBundle_nullSafetyLabel() {
+ val safetyLabel = SafetyLabel.getSafetyLabelFromMetadata(PersistableBundle.EMPTY)
+
+ assertThat(safetyLabel).isNull()
+ }
+
+ @Test
+ fun getSafetyLabelFromMetaData_invalidMetadataBundle_nullSafetyLabel() {
+ val safetyLabel =
+ SafetyLabel.getSafetyLabelFromMetadata(createInvalidMetadataPersistableBundle())
+
+ assertThat(safetyLabel).isNull()
+ }
+
+ @Test
+ fun getSafetyLabelFromMetaData_invalidSafetyLabelBundle_dataSharedEmpty() {
+ val safetyLabel =
+ SafetyLabel.getSafetyLabelFromMetadata(
+ createMetadataPersistableBundleWithInvalidSafetyLabel())
+
+ assertThat(safetyLabel).isNull()
+ }
+
+ @Test
+ fun getSafetyLabelFromMetaData_validBundle_hasDataShared() {
+ val safetyLabel = SafetyLabel.getSafetyLabelFromMetadata(createMetadataPersistableBundle())
+
+ assertThat(safetyLabel).isNotNull()
+ assertThat(safetyLabel?.dataLabel).isNotNull()
+ }
+}
diff --git a/SafetyLabel/tests/java/com/android/permission/safetylabel/SafetyLabelTestPersistableBundles.kt b/SafetyLabel/tests/java/com/android/permission/safetylabel/SafetyLabelTestPersistableBundles.kt
new file mode 100644
index 000000000..2d2811633
--- /dev/null
+++ b/SafetyLabel/tests/java/com/android/permission/safetylabel/SafetyLabelTestPersistableBundles.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 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 com.android.permission.safetylabel
+
+import android.os.PersistableBundle
+import com.android.permission.safetylabel.DataCategoryConstants.CATEGORY_LOCATION
+import com.android.permission.safetylabel.DataCategoryConstants.Category
+import com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_ADVERTISING
+import com.android.permission.safetylabel.DataPurposeConstants.PURPOSE_APP_FUNCTIONALITY
+import com.android.permission.safetylabel.DataType.KEY_EPHEMERAL
+import com.android.permission.safetylabel.DataType.KEY_PURPOSES
+import com.android.permission.safetylabel.DataType.KEY_USER_CONTROL
+
+/** A class that facilitates creating test safety label persistable bundles. */
+object SafetyLabelTestPersistableBundles {
+ const val INVALID_KEY = "invalid_key"
+
+ fun createMetadataPersistableBundle(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(SafetyLabel.KEY_SAFETY_LABEL, createSafetyLabelPersistableBundle())
+ }
+ }
+
+ fun createInvalidMetadataPersistableBundle(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(INVALID_KEY, createSafetyLabelPersistableBundle())
+ }
+ }
+
+ fun createMetadataPersistableBundleWithInvalidSafetyLabel(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(
+ SafetyLabel.KEY_SAFETY_LABEL, createInvalidSafetyLabelPersistableBundle())
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of a valid safety label */
+ fun createSafetyLabelPersistableBundle(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(DataLabel.KEY_DATA_LABEL, createDataLabelPersistableBundle())
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of an ivnalid safety label */
+ fun createInvalidSafetyLabelPersistableBundle(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(INVALID_KEY, createDataLabelPersistableBundle())
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of an ivnalid safety label */
+ fun createSafetyLabelPersistableBundleWithNullDataCollected(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(
+ DataLabel.KEY_DATA_LABEL, createDataLabelPersistableBundleWithNullDataCollected())
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of an ivnalid safety label */
+ fun createSafetyLabelPersistableBundleWithNullDataShared(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(
+ DataLabel.KEY_DATA_LABEL, createDataLabelPersistableBundleWithNullDataShared())
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of an ivnalid safety label */
+ fun createSafetyLabelPersistableBundleWithEmptyDataCollected(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(
+ DataLabel.KEY_DATA_LABEL, createDataLabelPersistableBundleWithEmptyDataCollected())
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of an ivnalid safety label */
+ fun createSafetyLabelPersistableBundleWithEmptyDataShared(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(
+ DataLabel.KEY_DATA_LABEL, createDataLabelPersistableBundleWithEmptyDataShared())
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of an ivnalid safety label */
+ fun createSafetyLabelPersistableBundleWithInvalidDataCollected(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(
+ DataLabel.KEY_DATA_LABEL,
+ createDataLabelPersistableBundleWithInvalidDataCollected())
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of an ivnalid safety label */
+ fun createSafetyLabelPersistableBundleWithInvalidDataShared(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(
+ DataLabel.KEY_DATA_LABEL, createDataLabelPersistableBundleWithInvalidDataShared())
+ }
+ }
+
+ fun createDataLabelPersistableBundle(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(
+ DataLabelConstants.DATA_USAGE_SHARED, createCategoryMapPersistableBundle())
+ putPersistableBundle(
+ DataLabelConstants.DATA_USAGE_COLLECTED, createCategoryMapPersistableBundle())
+ }
+ }
+
+ fun createInvalidDataLabelPersistableBundle(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(INVALID_KEY, createCategoryMapPersistableBundle())
+ }
+ }
+
+ private fun createDataLabelPersistableBundleWithNullDataCollected(): PersistableBundle {
+ val bundle = createDataLabelPersistableBundle()
+ bundle.remove(DataLabelConstants.DATA_USAGE_COLLECTED)
+ return bundle
+ }
+
+ private fun createDataLabelPersistableBundleWithNullDataShared(): PersistableBundle {
+ val bundle = createDataLabelPersistableBundle()
+ bundle.remove(DataLabelConstants.DATA_USAGE_SHARED)
+ return bundle
+ }
+
+ private fun createDataLabelPersistableBundleWithEmptyDataCollected(): PersistableBundle {
+ val bundle = createDataLabelPersistableBundle()
+ bundle.remove(DataLabelConstants.DATA_USAGE_COLLECTED)
+ bundle.putPersistableBundle(
+ DataLabelConstants.DATA_USAGE_COLLECTED, PersistableBundle.EMPTY)
+ return bundle
+ }
+
+ private fun createDataLabelPersistableBundleWithEmptyDataShared(): PersistableBundle {
+ val bundle = createDataLabelPersistableBundle()
+ bundle.remove(DataLabelConstants.DATA_USAGE_SHARED)
+ bundle.putPersistableBundle(DataLabelConstants.DATA_USAGE_SHARED, PersistableBundle.EMPTY)
+ return bundle
+ }
+
+ private fun createDataLabelPersistableBundleWithInvalidDataCollected(): PersistableBundle {
+ val bundle = createDataLabelPersistableBundle()
+ bundle.remove(DataLabelConstants.DATA_USAGE_COLLECTED)
+ bundle.putPersistableBundle(
+ DataLabelConstants.DATA_USAGE_COLLECTED, createInvalidCategoryMapPersistableBundle())
+ return bundle
+ }
+
+ private fun createDataLabelPersistableBundleWithInvalidDataShared(): PersistableBundle {
+ val bundle = createDataLabelPersistableBundle()
+ bundle.remove(DataLabelConstants.DATA_USAGE_SHARED)
+ bundle.putPersistableBundle(
+ DataLabelConstants.DATA_USAGE_SHARED, createInvalidCategoryMapPersistableBundle())
+ return bundle
+ }
+
+ fun createDataLabelPersistableBundleWithAdditonalInvalidCategory(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(
+ DataLabelConstants.DATA_USAGE_SHARED,
+ createCategoryMapPersistableBundleWithAdditionalInvalidCategory())
+ putPersistableBundle(
+ DataLabelConstants.DATA_USAGE_COLLECTED,
+ createCategoryMapPersistableBundleWithAdditionalInvalidCategory())
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of a [Map] of valid data categories */
+ fun createCategoryMapPersistableBundle(): PersistableBundle {
+ return PersistableBundle().apply {
+ DataCategoryConstants.VALID_CATEGORIES.forEach { categoryKey ->
+ putPersistableBundle(categoryKey, createTypeMapPersistableBundle(categoryKey))
+ }
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of a [Map] of valid data categories */
+ fun createCategoryMapPersistableBundleWithAdditionalInvalidCategory(): PersistableBundle {
+ return PersistableBundle().apply {
+ DataCategoryConstants.VALID_CATEGORIES.forEach { categoryKey ->
+ putPersistableBundle(categoryKey, createTypeMapPersistableBundle(categoryKey))
+ }
+ putPersistableBundle(INVALID_KEY, createTypeMapPersistableBundle(CATEGORY_LOCATION))
+ }
+ }
+
+ /**
+ * Returns [PersistableBundle] representation of a [Map] of valid data categories and invalid
+ * types
+ */
+ fun createCategoryMapPersistableBundleWithInvalidTypes(): PersistableBundle {
+ return PersistableBundle().apply {
+ DataCategoryConstants.VALID_CATEGORIES.forEach { categoryKey ->
+ putPersistableBundle(categoryKey, createInvalidTypeMapPersistableBundle())
+ }
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of a [Map] of invalid data categories */
+ fun createInvalidCategoryMapPersistableBundle(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(INVALID_KEY, createTypeMapPersistableBundle(CATEGORY_LOCATION))
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of a [Map] of valid data type */
+ fun createTypeMapPersistableBundle(@Category category: String): PersistableBundle {
+ return PersistableBundle().apply {
+ DataTypeConstants.getValidDataTypesForCategory(category).forEach { type ->
+ putPersistableBundle(type, createTypePersistableBundle())
+ }
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of a [Map] of invalid data type */
+ fun createInvalidTypeMapPersistableBundle(): PersistableBundle {
+ return PersistableBundle().apply {
+ putPersistableBundle(INVALID_KEY, createTypePersistableBundle())
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of a [Map] of valid type, with invalid data */
+ fun createTypeMapWithInvalidTypeDataPersistableBundle(
+ @Category category: String
+ ): PersistableBundle {
+ return PersistableBundle().apply {
+ DataTypeConstants.getValidDataTypesForCategory(category).forEach { type ->
+ putPersistableBundle(type, createInvalidTypePersistableBundle())
+ }
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of a valid data type */
+ fun createTypePersistableBundle(): PersistableBundle {
+ return PersistableBundle().apply {
+ putIntArray(KEY_PURPOSES, intArrayOf(PURPOSE_APP_FUNCTIONALITY, PURPOSE_ADVERTISING))
+ putBoolean(KEY_USER_CONTROL, true)
+ putBoolean(KEY_EPHEMERAL, true)
+ }
+ }
+
+ /** Returns [PersistableBundle] representation of an invalid data type */
+ fun createInvalidTypePersistableBundle(): PersistableBundle {
+ return PersistableBundle().apply { putLong(INVALID_KEY, 0) }
+ }
+}