diff options
author | 2022-10-21 19:00:18 -0700 | |
---|---|---|
committer | 2022-11-08 19:22:36 -0800 | |
commit | 55a59e855fbf46980bdc72615a9d27c14083f27d (patch) | |
tree | 24b56a240357028b30dbcf843888c356b09808e9 | |
parent | 2941dd6afabfc20d41b7493a656ea55482c6789f (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
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) } + } +} |