Added transparency-metadata display.
This feature enables 'App Details' to support MBAs with metadata file and display in an offline UI. This change includes a new fragment and user view that sources data from within any MBAs apk file that contains application-metadata.xml.
Test: make -j64 RunSettingsRoboTests , AVD manual test
Change-Id: If7822100a90f5fb8d33ce5d85958391fc33ecbee
Bug: 244215932
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 6c0dbff..ec5e227 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -4605,6 +4605,17 @@
android:exported="false">
</activity>
+ <activity
+ android:name="com.android.settings.applications.mobilebundledapps.MobileBundledAppDetailsActivity"
+ android:label="@string/mobile_bundled_apps_details_title"
+ android:exported="true"
+ android:icon="@drawable/ic_homepage_connected_device">
+ <intent-filter android:priority="1">
+ <action android:name="android.settings.TRANSPARENCY_METADATA" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
<!-- This is the longest AndroidManifest.xml ever. -->
</application>
</manifest>
diff --git a/res/layout/mobile_bundled_apps_details_fragment.xml b/res/layout/mobile_bundled_apps_details_fragment.xml
new file mode 100644
index 0000000..b2b8a23
--- /dev/null
+++ b/res/layout/mobile_bundled_apps_details_fragment.xml
@@ -0,0 +1,107 @@
+<?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.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/app_details_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="10dp">
+
+ <TextView
+ android:id="@+id/contains_ads_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/mobile_bundled_apps_details_contains_ad_title"
+ android:textSize="18sp"
+ android:textStyle="bold" />
+ <TextView
+ android:id="@+id/contains_ads"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp" />
+
+ <TextView
+ android:id="@+id/developer_info_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/mobile_bundled_apps_details_developer_info_title"
+ android:textSize="18sp"
+ android:textStyle="bold" />
+ <LinearLayout
+ android:id="@+id/developer_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" />
+
+ <TextView
+ android:id="@+id/contact_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/mobile_bundled_apps_details_contact_title"
+ android:textSize="18sp"
+ android:textStyle="bold" />
+ <TextView
+ android:id="@+id/contact_url"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp" />
+ <TextView
+ android:id="@+id/contact_email"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp" />
+
+ <TextView
+ android:id="@+id/description_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/mobile_bundled_apps_details_description_title"
+ android:textSize="18sp"
+ android:textStyle="bold" />
+ <TextView
+ android:id="@+id/description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp" />
+
+ <TextView
+ android:id="@+id/privacy_policy_url_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/mobile_bundled_apps_details_privacy_policy_title"
+ android:textSize="18sp"
+ android:textStyle="bold" />
+ <TextView
+ android:id="@+id/privacy_policy_url"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp" />
+
+ <TextView
+ android:id="@+id/category_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/mobile_bundled_apps_details_category_title"
+ android:textSize="18sp"
+ android:textStyle="bold" />
+ <TextView
+ android:id="@+id/category"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp" />
+</LinearLayout>
diff --git a/res/layout/mobile_bundled_apps_developer_fragment_row.xml b/res/layout/mobile_bundled_apps_developer_fragment_row.xml
new file mode 100644
index 0000000..1ec0694
--- /dev/null
+++ b/res/layout/mobile_bundled_apps_developer_fragment_row.xml
@@ -0,0 +1,50 @@
+<?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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="10dp">
+ <TextView
+ android:id="@+id/mba_info"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ android:textAppearance="?android:attr/textAppearanceListItemSecondary"/>
+ <TextView
+ android:id="@+id/developer_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp" />
+ <TextView
+ android:id="@+id/developer_relationship"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp" />
+ <TextView
+ android:id="@+id/developer_email"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp" />
+ <TextView
+ android:id="@+id/developer_country"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="12sp" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 0c0e883..ee26136 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -11907,4 +11907,26 @@
<!-- [CHAR LIMIT=NONE] Title for Accessibility Software Cursor setting for keyboard shift. -->
<string name="software_cursor_trigger_keyboard_shift_enabled_title" translatable="false">Shift gesture detection region above keyboard</string>
+ <!-- Mobile Bundled Apps Transparency Metadata-->
+
+ <!-- [CHAR_LIMIT=NONE] Label for mobile bundled apps screen -->
+ <string name="mobile_bundled_apps">Mobile bundled apps</string>
+ <!-- [CHAR_LIMIT=NONE] Title for mobile bundled apps screen -->
+ <string name="mobile_bundled_apps_title">Mobile bundled apps</string>
+ <!-- [CHAR_LIMIT=NONE] Title for mobile bundled apps transparency information details screen -->
+ <string name="mobile_bundled_apps_details_title">Mobile bundled apps transparency info</string>
+ <!-- [CHAR_LIMIT=NONE] Subheading for mobile bundled apps transparency information details screen -->
+ <string name="mobile_bundled_apps_details_contains_ad_title">Contains ads</string>
+ <!-- [CHAR_LIMIT=NONE] Subheading for mobile bundled apps transparency information details screen -->
+ <string name="mobile_bundled_apps_details_developer_info_title">Developer(s) Information</string>
+ <!-- [CHAR_LIMIT=NONE] Subheading for mobile bundled apps transparency information details screen -->
+ <string name="mobile_bundled_apps_details_contact_title">Contact Information</string>
+ <!-- [CHAR_LIMIT=NONE] Subheading for mobile bundled apps transparency information details screen -->
+ <string name="mobile_bundled_apps_details_description_title">Description</string>
+ <!-- [CHAR_LIMIT=NONE] Subheading for mobile bundled apps transparency information details screen -->
+ <string name="mobile_bundled_apps_details_privacy_policy_title">Privacy Policy</string>
+ <!-- [CHAR_LIMIT=NONE] Subheading for mobile bundled apps transparency information details screen -->
+ <string name="mobile_bundled_apps_details_category_title">Category</string>
+ <!-- [CHAR_LIMIT=NONE] Summary for App Details in App Info page if app is mobile bundled app -->
+ <string name="app_install_details_mba_summary">Mobile Bundled App</string>
</resources>
diff --git a/res/xml/mobile_bundled_apps_details_preference.xml b/res/xml/mobile_bundled_apps_details_preference.xml
new file mode 100644
index 0000000..2bef352
--- /dev/null
+++ b/res/xml/mobile_bundled_apps_details_preference.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ android:title="">
+ <com.android.settingslib.widget.LayoutPreference
+ android:key="metadata"
+ android:selectable="false"
+ android:layout="@layout/mobile_bundled_apps_details_fragment" />
+</PreferenceScreen>
\ No newline at end of file
diff --git a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
index 18cb4b3..39e8ea8 100755
--- a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
+++ b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
@@ -56,6 +56,7 @@
import com.android.settings.SettingsActivity;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.applications.manageapplications.ManageApplications;
+import com.android.settings.applications.mobilebundledapps.ApplicationMetadataUtils;
import com.android.settings.applications.specialaccess.interactacrossprofiles.InteractAcrossProfilesDetailsPreferenceController;
import com.android.settings.applications.specialaccess.pictureinpicture.PictureInPictureDetailPreferenceController;
import com.android.settings.core.SubSettingLauncher;
@@ -162,6 +163,8 @@
use(AppInstallerInfoPreferenceController.class);
installer.setPackageName(packageName);
installer.setParentFragment(this);
+ installer.setMbaWithMetadataStatus(ApplicationMetadataUtils.getDefaultInstance(),
+ packageName);
use(AppInstallerPreferenceCategoryController.class).setChildren(Arrays.asList(installer));
use(AppNotificationPreferenceController.class).setParentFragment(this);
diff --git a/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceController.java b/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceController.java
index 5e99e8b..c91d288 100644
--- a/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceController.java
+++ b/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceController.java
@@ -16,42 +16,50 @@
package com.android.settings.applications.appinfo;
+import static com.android.settings.applications.mobilebundledapps.MobileBundledAppDetailsActivity.ACTION_TRANSPARENCY_METADATA;
+
import android.content.Context;
import android.content.Intent;
import android.os.UserManager;
+import android.provider.DeviceConfig;
+import android.text.TextUtils;
+import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.applications.AppStoreUtil;
+import com.android.settings.applications.mobilebundledapps.ApplicationMetadataUtils;
import com.android.settingslib.applications.AppUtils;
-
public class AppInstallerInfoPreferenceController extends AppInfoPreferenceControllerBase {
+ private static final String KEY_ENABLE_PROMPT = "enable_prompt";
private String mPackageName;
private String mInstallerPackage;
private CharSequence mInstallerLabel;
+ private Boolean mAppIsMbaWithMetadata;
+ private Boolean mEnableMbaUiFlag = false;
public AppInstallerInfoPreferenceController(Context context, String key) {
super(context, key);
+ updateFromDeviceConfigFlags();
}
@Override
public int getAvailabilityStatus() {
- if (UserManager.get(mContext).isManagedProfile()) {
+ if (UserManager.get(mContext).isManagedProfile()
+ || AppUtils.isMainlineModule(mContext.getPackageManager(), mPackageName)) {
return DISABLED_FOR_USER;
}
-
- if (AppUtils.isMainlineModule(mContext.getPackageManager(), mPackageName)) {
- return DISABLED_FOR_USER;
+ if (mInstallerLabel != null || (mAppIsMbaWithMetadata && mEnableMbaUiFlag)) {
+ return AVAILABLE;
}
-
- return mInstallerLabel != null ? AVAILABLE : DISABLED_FOR_USER;
+ return DISABLED_FOR_USER;
}
@Override
- public void updateState(Preference preference) {
+ public void updateState(final Preference preference) {
final int detailsStringId = AppUtils.isInstant(mParent.getPackageInfo().applicationInfo)
? R.string.instant_app_details_summary
: R.string.app_install_details_summary;
@@ -60,14 +68,52 @@
Intent intent = AppStoreUtil.getAppStoreLink(mContext, mInstallerPackage, mPackageName);
if (intent != null) {
preference.setIntent(intent);
+ } else if (mAppIsMbaWithMetadata && mEnableMbaUiFlag) {
+ preference.setIntent(generateMetadataXmlViewerIntent());
+ preference.setSummary(mContext.getString(R.string.app_install_details_mba_summary));
} else {
preference.setEnabled(false);
}
}
- public void setPackageName(String packageName) {
+ /**
+ * Sets the packageName in context for the controller.
+ */
+ public void setPackageName(final String packageName) {
mPackageName = packageName;
mInstallerPackage = AppStoreUtil.getInstallerPackageName(mContext, mPackageName);
mInstallerLabel = Utils.getApplicationLabel(mContext, mInstallerPackage);
}
+
+ /**
+ * Setups and determines if the current package in context is an mobile-bundled-app with
+ * an application metadata file embedded within.
+ */
+ public void setMbaWithMetadataStatus(final ApplicationMetadataUtils appMetadataUtils,
+ final String packageName) {
+ mAppIsMbaWithMetadata = appMetadataUtils.packageContainsXmlFile(
+ mContext.getPackageManager(), packageName);
+ }
+
+ private Intent generateMetadataXmlViewerIntent() {
+ final Intent metadataXmlIntent = new Intent(ACTION_TRANSPARENCY_METADATA)
+ .setPackage(mContext.getPackageName());
+ metadataXmlIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, mPackageName);
+ return metadataXmlIntent;
+ }
+
+ private void updateFromDeviceConfigFlags() {
+ String enablePromptFlag = DeviceConfig.getProperty(
+ DeviceConfig.NAMESPACE_TRANSPARENCY_METADATA,
+ KEY_ENABLE_PROMPT);
+ //No-op for empty field and relies on default value of false
+ if (!TextUtils.isEmpty(enablePromptFlag)) {
+ setEnableMbaFlag(Boolean.parseBoolean(enablePromptFlag));
+ }
+ }
+
+ @VisibleForTesting
+ void setEnableMbaFlag(final boolean flagValue) {
+ mEnableMbaUiFlag = flagValue;
+ }
}
diff --git a/src/com/android/settings/applications/mobilebundledapps/ApplicationMetadataUtils.java b/src/com/android/settings/applications/mobilebundledapps/ApplicationMetadataUtils.java
new file mode 100644
index 0000000..5611fc4
--- /dev/null
+++ b/src/com/android/settings/applications/mobilebundledapps/ApplicationMetadataUtils.java
@@ -0,0 +1,201 @@
+/*
+ * 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.settings.applications.mobilebundledapps;
+
+import android.content.pm.PackageManager;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipFile;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+
+/**
+ * Used for parsing application-metadata.xml and return relevant fields
+ */
+public class ApplicationMetadataUtils {
+ private static final String TAG = ApplicationMetadataUtils.class.getSimpleName();
+
+ private static final ApplicationMetadataUtils DEFAULT_INSTANCE = new ApplicationMetadataUtils();
+ private static final String TRANSPARENCY_XML_DIR = "APP-INF/application-metadata.xml";
+ private static final String DESCRIPTION_TAG = "description";
+ private static final String CONTAINS_ADS_TAG = "contains-ads";
+ private static final String PRIVACY_POLICY_TAG = "privacy-policy";
+ private static final String CONTACT_TAG = "contact";
+ private static final String CATEGORY_TAG = "category";
+ private static final String DEVELOPER_TAG = "developer";
+ private static final String URL_TAG = "url";
+ private static final String EMAIL_TAG = "email";
+ private static final String NAME_TAG = "name";
+ private static final String RELATIONSHIP_TAG = "relationship";
+ private static final String COUNTRY_TAG = "country";
+
+ private final PackageManager mPackageManager;
+
+ private Document mXmlDoc;
+
+ @VisibleForTesting
+ ApplicationMetadataUtils() {
+ mPackageManager = null;
+ }
+
+ //Need to create singleton factory as Android is unable to mock static for testing.
+ public static ApplicationMetadataUtils getDefaultInstance() {
+ return DEFAULT_INSTANCE;
+ }
+
+ /**
+ * Generates a new instance that also provisions and reads the XML file.
+ */
+ public static ApplicationMetadataUtils newInstance(final PackageManager packageManager,
+ String packageName) {
+ return new ApplicationMetadataUtils(packageManager, packageName);
+ }
+ private ApplicationMetadataUtils(final PackageManager packageManager,
+ final String packageName) {
+ mPackageManager = packageManager;
+ try (ZipFile apk = new ZipFile(getApkDirectory(packageName, mPackageManager))) {
+ mXmlDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+ .parse(apk.getInputStream(apk.getEntry(TRANSPARENCY_XML_DIR)));
+ } catch (final Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @VisibleForTesting
+ void setXmlDoc(final Document xmlDoc) {
+ mXmlDoc = xmlDoc;
+ }
+
+ private static String getApkDirectory(final String packageName,
+ final PackageManager packageManager)
+ throws PackageManager.NameNotFoundException {
+ return packageManager
+ .getApplicationInfo(packageName,
+ PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA))
+ .sourceDir;
+ }
+ public boolean getContainsAds() {
+ return mXmlDoc != null
+ && mXmlDoc.getElementsByTagName(CONTAINS_ADS_TAG) != null
+ && mXmlDoc.getElementsByTagName(CONTAINS_ADS_TAG).getLength() > 0;
+ }
+
+ public String getPrivacyPolicyUrl() {
+ return retrieveElementAttributeValue(PRIVACY_POLICY_TAG, URL_TAG);
+ }
+
+ private String retrieveElementAttributeValue(final String elementTag, final String attribute) {
+ try {
+ return mXmlDoc.getElementsByTagName(elementTag).item(0)
+ .getAttributes().getNamedItem(attribute).getNodeValue();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public String getDescription() {
+ return retrieveElementValue(DESCRIPTION_TAG);
+ }
+
+ private String retrieveElementValue(final String elementTag) {
+ try {
+ return mXmlDoc.getElementsByTagName(elementTag).item(0).getTextContent();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public String getCategoryName() {
+ return retrieveElementAttributeValue(CATEGORY_TAG, NAME_TAG);
+ }
+
+ public String getContactUrl() {
+ return retrieveElementAttributeValue(CONTACT_TAG, URL_TAG);
+ }
+
+ public String getContactEmail() {
+ return retrieveElementAttributeValue(CONTACT_TAG, EMAIL_TAG);
+ }
+
+ public String getPlayStoreUrl() {
+ return retrieveElementValue(DESCRIPTION_TAG);
+ }
+
+ /**
+ * Retrieves the list of relevant major parties involved with this MBA
+ */
+ public List<MbaDeveloper> getDevelopers() {
+ final List<MbaDeveloper> developersDetails = new ArrayList();
+ try {
+ final NodeList developers = mXmlDoc.getElementsByTagName(DEVELOPER_TAG);
+ if (developers == null) return developersDetails;
+ for (int i = 0; i < developers.getLength(); ++i) {
+ final NamedNodeMap developerAttributes = developers.item(i).getAttributes();
+ developersDetails.add(new MbaDeveloper(
+ developerAttributes.getNamedItem(NAME_TAG).getNodeValue(),
+ developerAttributes.getNamedItem(RELATIONSHIP_TAG).getNodeValue(),
+ developerAttributes.getNamedItem(EMAIL_TAG).getNodeValue(),
+ developerAttributes.getNamedItem(COUNTRY_TAG).getNodeValue()
+ ));
+ }
+ } catch (final Exception e) {
+ Log.d(TAG, e.getMessage());
+ }
+ return developersDetails;
+ }
+
+ /**
+ * Determines if the a package can be parsed and extrapolate metadata from.
+ */
+ public boolean packageContainsXmlFile(final PackageManager packageManager,
+ final String packageName) {
+ try (ZipFile apk = new ZipFile(getApkDirectory(packageName, packageManager))) {
+ return apk.getEntry(TRANSPARENCY_XML_DIR) != null;
+ } catch (final Exception e) {
+ Log.d(TAG, e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Used to return developer details
+ */
+ public static class MbaDeveloper {
+ public final String name;
+ public final String relationship;
+ public final String email;
+ public final String country;
+
+ public MbaDeveloper(final String name,
+ final String relationship,
+ final String email,
+ final String country) {
+ this.name = name;
+ this.relationship = relationship;
+ this.email = email;
+ this.country = country;
+ }
+ }
+}
diff --git a/src/com/android/settings/applications/mobilebundledapps/MobileBundledAppDetailsActivity.java b/src/com/android/settings/applications/mobilebundledapps/MobileBundledAppDetailsActivity.java
new file mode 100644
index 0000000..52e3268
--- /dev/null
+++ b/src/com/android/settings/applications/mobilebundledapps/MobileBundledAppDetailsActivity.java
@@ -0,0 +1,47 @@
+/*
+ * 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.settings.applications.mobilebundledapps;
+
+import android.content.Intent;
+import android.net.Uri;
+
+import com.android.settings.SettingsActivity;
+/**
+ * An activity that is used to parse and display mobile-bundled apps application metadata xml file.
+ */
+public class MobileBundledAppDetailsActivity extends SettingsActivity {
+ public static final String ACTION_TRANSPARENCY_METADATA =
+ "android.settings.TRANSPARENCY_METADATA";
+
+ public MobileBundledAppDetailsActivity() {
+ super();
+ }
+
+ @Override
+ public Intent getIntent() {
+ final Intent modIntent = new Intent(super.getIntent());
+ modIntent.setData(Uri.parse("package:"
+ + super.getIntent().getExtra(Intent.EXTRA_PACKAGE_NAME).toString()));
+ modIntent.putExtra(EXTRA_SHOW_FRAGMENT, MobileBundledAppsDetailsFragment.class.getName());
+ return modIntent;
+ }
+
+ @Override
+ protected boolean isValidFragment(final String fragmentName) {
+ return MobileBundledAppsDetailsFragment.class.getName().equals(fragmentName);
+ }
+}
diff --git a/src/com/android/settings/applications/mobilebundledapps/MobileBundledAppsDetailsFragment.java b/src/com/android/settings/applications/mobilebundledapps/MobileBundledAppsDetailsFragment.java
new file mode 100644
index 0000000..2779467
--- /dev/null
+++ b/src/com/android/settings/applications/mobilebundledapps/MobileBundledAppsDetailsFragment.java
@@ -0,0 +1,133 @@
+/*
+ * 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.settings.applications.mobilebundledapps;
+
+import android.app.Application;
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.settings.R;
+import com.android.settings.applications.AppInfoWithHeader;
+import com.android.settings.applications.mobilebundledapps.ApplicationMetadataUtils.MbaDeveloper;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.widget.LayoutPreference;
+
+import java.util.List;
+
+/**
+ * A fragment for retrieving the transparency metadata and PSL in the in-APK XML file and displaying
+ * them.
+ */
+public class MobileBundledAppsDetailsFragment extends AppInfoWithHeader {
+ private static final String METADATA_PREF_KEY = "metadata";
+
+ protected PackageManager mPackageManager;
+ private Context mContext;
+ private LayoutPreference mMetadataPreferenceView;
+ private ApplicationsState mApplicationState;
+ private boolean mCreated = false;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mContext = getActivity();
+ mPackageManager = mContext.getPackageManager();
+ addPreferencesFromResource(R.xml.mobile_bundled_apps_details_preference);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ if (mCreated) {
+ return;
+ }
+ super.onActivityCreated(savedInstanceState);
+ final ApplicationMetadataUtils appUtil = ApplicationMetadataUtils.newInstance(
+ mPackageManager,
+ mPackageName);
+ if (mAppEntry == null) {
+ mApplicationState =
+ ApplicationsState.getInstance((Application) (mContext.getApplicationContext()));
+ mAppEntry = mApplicationState.getEntry(mPackageName, mContext.getUserId());
+ }
+ mMetadataPreferenceView = findPreference(METADATA_PREF_KEY);
+ createView(appUtil);
+ mCreated = true;
+ }
+
+ private void createView(final ApplicationMetadataUtils appUtil) {
+ final LinearLayout devListLayout =
+ mMetadataPreferenceView.findViewById(R.id.developer_list);
+ populateDeveloperList(appUtil.getDevelopers(), devListLayout);
+
+ ((TextView) mMetadataPreferenceView.findViewById(R.id.contains_ads))
+ .setText(Boolean.toString(appUtil.getContainsAds()));
+
+ ((TextView) mMetadataPreferenceView.findViewById(R.id.contact_url))
+ .setText(appUtil.getContactUrl());
+ ((TextView) mMetadataPreferenceView.findViewById(R.id.contact_email))
+ .setText(appUtil.getContactEmail());
+
+ ((TextView) mMetadataPreferenceView.findViewById(R.id.privacy_policy_url))
+ .setText(appUtil.getPrivacyPolicyUrl());
+
+ ((TextView) mMetadataPreferenceView.findViewById(R.id.description))
+ .setText(appUtil.getDescription());
+
+ ((TextView) mMetadataPreferenceView.findViewById(R.id.category))
+ .setText(appUtil.getCategoryName());
+ }
+
+ private void populateDeveloperList(List<MbaDeveloper> developersDetails, ViewGroup parent) {
+ for (MbaDeveloper dev : developersDetails) {
+ View itemView = LayoutInflater.from(mContext)
+ .inflate(R.layout.mobile_bundled_apps_developer_fragment_row, parent, false);
+
+ ((TextView) itemView.findViewById(R.id.developer_name)).setText(dev.name);
+ ((TextView) itemView.findViewById(R.id.developer_relationship))
+ .setText(dev.relationship);
+ ((TextView) itemView.findViewById(R.id.developer_email)).setText(dev.email);
+ ((TextView) itemView.findViewById(R.id.developer_country)).setText(dev.country);
+
+ parent.addView(itemView);
+ }
+ }
+
+ @Override
+ protected AlertDialog createDialog(int id, int errorCode) {
+ return null;
+ }
+
+ @Override
+ protected boolean refreshUi() {
+ return true;
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return SettingsEnums.TRANSPARENCY_METADATA;
+ }
+
+}
diff --git a/tests/robotests/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceControllerTest.java
index deb5a3f..242f9a4 100644
--- a/tests/robotests/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceControllerTest.java
@@ -16,6 +16,8 @@
package com.android.settings.applications.appinfo;
+import static com.android.settings.applications.mobilebundledapps.MobileBundledAppDetailsActivity.ACTION_TRANSPARENCY_METADATA;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -41,11 +43,14 @@
import androidx.preference.Preference;
+import com.android.settings.applications.mobilebundledapps.ApplicationMetadataUtils;
import com.android.settings.core.BasePreferenceController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
@@ -53,7 +58,8 @@
@RunWith(RobolectricTestRunner.class)
public class AppInstallerInfoPreferenceControllerTest {
-
+ private static final String TEST_PACKAGE_NAME = "Package1";
+ private static final String TEST_CONTEXT_KEY = "test_key";
@Mock
private UserManager mUserManager;
@Mock
@@ -67,11 +73,17 @@
@Mock
private Preference mPreference;
+ @Mock
+ private ApplicationMetadataUtils mApplicationMetadataUtils;
+
+ @Captor
+ ArgumentCaptor<Intent> mIntentArgumentCaptor;
+
private Context mContext;
private AppInstallerInfoPreferenceController mController;
@Before
- public void setUp() throws PackageManager.NameNotFoundException {
+ public void setup() throws PackageManager.NameNotFoundException {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
@@ -81,9 +93,13 @@
when(mInstallSourceInfo.getInstallingPackageName()).thenReturn(installerPackage);
when(mPackageManager.getApplicationInfo(eq(installerPackage), anyInt()))
.thenReturn(mAppInfo);
- mController = new AppInstallerInfoPreferenceController(mContext, "test_key");
- mController.setPackageName("Package1");
+ mController = new AppInstallerInfoPreferenceController(mContext, TEST_CONTEXT_KEY);
mController.setParentFragment(mFragment);
+ mController.setPackageName(TEST_PACKAGE_NAME);
+ when(mApplicationMetadataUtils.packageContainsXmlFile(mPackageManager, TEST_PACKAGE_NAME))
+ .thenReturn(false);
+ mController.setMbaWithMetadataStatus(mApplicationMetadataUtils, TEST_PACKAGE_NAME);
+ mController.setEnableMbaFlag(true);
}
@Test
@@ -95,8 +111,42 @@
}
@Test
- public void getAvailabilityStatus_noAppLabel_shouldReturnDisabled() {
+ public void getAvailabilityStatus_noAppLabel_andNotMbaWithMetadata_shouldReturnDisabled()
+ throws PackageManager.NameNotFoundException {
when(mUserManager.isManagedProfile()).thenReturn(false);
+ mockMainlineModule(TEST_PACKAGE_NAME, false /* isMainlineModule */);
+
+ assertThat(mController.getAvailabilityStatus())
+ .isEqualTo(BasePreferenceController.DISABLED_FOR_USER);
+ }
+
+ @Test
+ public void getAvailabilityStatus_noAppLabel_andHaveMbaFile_shouldReturnAvailable()
+ throws PackageManager.NameNotFoundException {
+ mController = new AppInstallerInfoPreferenceController(mContext, TEST_CONTEXT_KEY);
+ mController.setPackageName(TEST_PACKAGE_NAME);
+ mController.setParentFragment(mFragment);
+ when(mApplicationMetadataUtils.packageContainsXmlFile(mPackageManager, TEST_PACKAGE_NAME))
+ .thenReturn(true);
+ mController.setMbaWithMetadataStatus(mApplicationMetadataUtils, TEST_PACKAGE_NAME);
+ mockMainlineModule(TEST_PACKAGE_NAME, false /* isMainlineModule */);
+
+ assertThat(mController.getAvailabilityStatus())
+ .isEqualTo(BasePreferenceController.DISABLED_FOR_USER);
+ }
+
+ @Test
+ public void getAvailabilityStatus_noAppLabel_andMbaFeatureFlagDisabled_shouldReturnDisabled()
+ throws PackageManager.NameNotFoundException {
+ mController.setEnableMbaFlag(false);
+ when(mUserManager.isManagedProfile()).thenReturn(false);
+ mController = new AppInstallerInfoPreferenceController(mContext, TEST_CONTEXT_KEY);
+ mController.setPackageName(TEST_PACKAGE_NAME);
+ mController.setParentFragment(mFragment);
+ when(mApplicationMetadataUtils.packageContainsXmlFile(mPackageManager, TEST_PACKAGE_NAME))
+ .thenReturn(true);
+ mController.setMbaWithMetadataStatus(mApplicationMetadataUtils, TEST_PACKAGE_NAME);
+ mockMainlineModule(TEST_PACKAGE_NAME, false /* isMainlineModule */);
assertThat(mController.getAvailabilityStatus())
.isEqualTo(BasePreferenceController.DISABLED_FOR_USER);
@@ -105,13 +155,12 @@
@Test
public void getAvailabilityStatus_hasAppLabel_shouldReturnAvailable()
throws PackageManager.NameNotFoundException {
- final String packageName = "Package1";
when(mUserManager.isManagedProfile()).thenReturn(false);
when(mAppInfo.loadLabel(mPackageManager)).thenReturn("Label1");
- mController = new AppInstallerInfoPreferenceController(mContext, "test_key");
- mController.setPackageName(packageName);
+ mController = new AppInstallerInfoPreferenceController(mContext, TEST_CONTEXT_KEY);
+ mController.setPackageName(TEST_PACKAGE_NAME);
mController.setParentFragment(mFragment);
- mockMainlineModule(packageName, false /* isMainlineModule */);
+ mockMainlineModule(TEST_PACKAGE_NAME, false /* isMainlineModule */);
assertThat(mController.getAvailabilityStatus())
.isEqualTo(BasePreferenceController.AVAILABLE);
@@ -129,7 +178,7 @@
}
@Test
- public void updateState_noAppStoreLink_shouldDisablePreference() {
+ public void updateState_noAppStoreLink_andNotMbaWithMetadata_shouldDisablePreference() {
final PackageInfo packageInfo = mock(PackageInfo.class);
packageInfo.applicationInfo = mAppInfo;
when(mFragment.getPackageInfo()).thenReturn(packageInfo);
@@ -139,6 +188,39 @@
verify(mPreference).setEnabled(false);
}
+ @Test
+ public void updateState_noAppStoreLink_andMbaFeatureFlagDisabled_shouldDisablePreference() {
+ mController.setEnableMbaFlag(false);
+ when(mApplicationMetadataUtils.packageContainsXmlFile(mPackageManager, TEST_PACKAGE_NAME))
+ .thenReturn(true);
+ mController.setMbaWithMetadataStatus(mApplicationMetadataUtils, TEST_PACKAGE_NAME);
+ final PackageInfo packageInfo = mock(PackageInfo.class);
+ packageInfo.applicationInfo = mAppInfo;
+ when(mFragment.getPackageInfo()).thenReturn(packageInfo);
+ when(mPackageManager.resolveActivity(any(), anyInt())).thenReturn(null);
+
+ mController.updateState(mPreference);
+
+ verify(mPreference).setEnabled(false);
+ }
+
+ @Test
+ public void updateState_noAppStoreLink_andMbaWithMetadata_shouldSetPreferenceIntent() {
+ when(mApplicationMetadataUtils.packageContainsXmlFile(mPackageManager, TEST_PACKAGE_NAME))
+ .thenReturn(true);
+ mController.setMbaWithMetadataStatus(mApplicationMetadataUtils, TEST_PACKAGE_NAME);
+ final PackageInfo packageInfo = mock(PackageInfo.class);
+ packageInfo.applicationInfo = mAppInfo;
+ when(mFragment.getPackageInfo()).thenReturn(packageInfo);
+ when(mPackageManager.resolveActivity(any(), anyInt())).thenReturn(null);
+
+ mController.updateState(mPreference);
+
+ verify(mPreference, never()).setEnabled(false);
+ verify(mPreference).setIntent(mIntentArgumentCaptor.capture());
+ assertThat(mIntentArgumentCaptor.getValue().getAction())
+ .isEqualTo(ACTION_TRANSPARENCY_METADATA);
+ }
@Test
public void updateState_hasAppStoreLink_shouldSetPreferenceIntent() {
@@ -154,7 +236,9 @@
mController.updateState(mPreference);
verify(mPreference, never()).setEnabled(false);
- verify(mPreference).setIntent(any(Intent.class));
+ verify(mPreference).setIntent(mIntentArgumentCaptor.capture());
+ assertThat(mIntentArgumentCaptor.getValue().getAction())
+ .isEqualTo(Intent.ACTION_SHOW_APP_INFO);
}
@Test
diff --git a/tests/robotests/src/com/android/settings/applications/mobilebundledapps/ApplicationMetadataUtilsTest.java b/tests/robotests/src/com/android/settings/applications/mobilebundledapps/ApplicationMetadataUtilsTest.java
new file mode 100644
index 0000000..a9eaec4
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/applications/mobilebundledapps/ApplicationMetadataUtilsTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.settings.applications.mobilebundledapps;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+
+import com.google.common.io.CharSource;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+@RunWith(RobolectricTestRunner.class)
+public class ApplicationMetadataUtilsTest {
+ private static final String TEST_PACKAGE_NAME = "test";
+ private static final String TEST_SOURCE_DIR = "sourcedir";
+
+ private static final String TEST_XML_SCHEMA = "<transparency-info>\n"
+ + " <template/>\n"
+ + " <contains-ads/>\n"
+ + " <developers>\n"
+ + " <developer name=\"Example ODM\" relationship=\"ODM\" email=\"odm@example.com\""
+ + " \n"
+ + " website=\"http://odm.example.com\" country=\"US\"/>\n"
+ + " <developer name=\"Example carrier\" relationship=\"CARRIER\" "
+ + "email=\"carrier@example.com\" \n"
+ + " country=\"US\"/>\n"
+ + " </developers>\n"
+ + " <contact url=\"http://example.com/contact-us\" email=\"contact@example.com\"/>\n"
+ + " <privacy-policy url=\"https://www.example.com/privacy-policy.html\"/>\n"
+ + " <description>This application provides the user with news "
+ + "headlines</description>\n"
+ + " <category name=\"News and magazines\"/>\n"
+ + "</transparency-info>";
+ @Mock
+ private PackageManager mPackageManager;
+
+ private Document mDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+ .parse(CharSource.wrap(TEST_XML_SCHEMA).asByteSource(StandardCharsets.UTF_8)
+ .openStream());
+
+
+ public ApplicationMetadataUtilsTest()
+ throws IOException, ParserConfigurationException, SAXException {
+ }
+
+ @Before
+ public void setup()
+ throws PackageManager.NameNotFoundException, IOException, ParserConfigurationException,
+ SAXException {
+ MockitoAnnotations.initMocks(this);
+ final ApplicationInfo appInfo = new ApplicationInfo();
+ appInfo.sourceDir = TEST_SOURCE_DIR;
+ when(mPackageManager.getApplicationInfo(eq(TEST_PACKAGE_NAME),
+ any(PackageManager.ApplicationInfoFlags.class))).thenReturn(appInfo);
+ }
+
+ @Test
+ public void getDefaultInstance_alwaysReturnSameInstance() {
+ final ApplicationMetadataUtils firstInstance =
+ ApplicationMetadataUtils.getDefaultInstance();
+
+ assertThat(firstInstance).isEqualTo(ApplicationMetadataUtils.getDefaultInstance());
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void createInstance_bubblesUpException() throws PackageManager.NameNotFoundException {
+ final String testErrorMsg = "test";
+ when(mPackageManager.getApplicationInfo(eq(TEST_PACKAGE_NAME),
+ any(PackageManager.ApplicationInfoFlags.class)))
+ .thenThrow(new Exception(testErrorMsg));
+
+ ApplicationMetadataUtils.newInstance(mPackageManager, TEST_PACKAGE_NAME);
+ }
+
+ @Test
+ public void fieldGetters_toReturnNull_whenEmptyOrError() {
+ final ApplicationMetadataUtils appUtils = new ApplicationMetadataUtils();
+ assertThat(appUtils.getContainsAds()).isEqualTo(false);
+ assertThat(appUtils.getCategoryName()).isNull();
+ assertThat(appUtils.getPrivacyPolicyUrl()).isNull();
+ assertThat(appUtils.getDescription()).isNull();
+ assertThat(appUtils.getDevelopers()).isEmpty();
+ }
+
+ @Test
+ public void fieldGetters_toReturnCorrectValues_whenExists() {
+ final ApplicationMetadataUtils appUtils = new ApplicationMetadataUtils();
+ appUtils.setXmlDoc(mDocument);
+ assertThat(appUtils.getContainsAds()).isEqualTo(true);
+ assertThat(appUtils.getCategoryName()).isEqualTo("News and magazines");
+ assertThat(appUtils.getPrivacyPolicyUrl())
+ .isEqualTo("https://www.example.com/privacy-policy.html");
+ assertThat(appUtils.getDescription())
+ .isEqualTo("This application provides the user with news headlines");
+ }
+
+ @Test
+ public void getDevelopers_returnsCorrectValues() {
+ final ApplicationMetadataUtils appUtils = new ApplicationMetadataUtils();
+ appUtils.setXmlDoc(mDocument);
+ final List<ApplicationMetadataUtils.MbaDeveloper> developers = appUtils.getDevelopers();
+
+ assertThat(developers.size()).isEqualTo(2);
+ assertThat(developers.get(0).country).isEqualTo("US");
+ assertThat(developers.get(0).email).isEqualTo("odm@example.com");
+ assertThat(developers.get(0).name).isEqualTo("Example ODM");
+ assertThat(developers.get(1).relationship).isEqualTo("CARRIER");
+ assertThat(developers.get(1).country).isEqualTo("US");
+ assertThat(developers.get(1).email).isEqualTo("carrier@example.com");
+ assertThat(developers.get(1).name).isEqualTo("Example carrier");
+ assertThat(developers.get(1).relationship).isEqualTo("CARRIER");
+ }
+}