Merge "Infrastructure for showing instant app metadata in app header"
diff --git a/res/layout/app_details.xml b/res/layout/app_details.xml
index 9f349de..3088865 100644
--- a/res/layout/app_details.xml
+++ b/res/layout/app_details.xml
@@ -75,6 +75,41 @@
</LinearLayout>
+ <TextView
+ android:id="@+id/instant_app_developer_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:visibility="gone"/>
+
+ <LinearLayout
+ android:id="@+id/instant_app_maturity"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:visibility="gone">
+
+ <ImageView
+ android:id="@+id/instant_app_maturity_icon"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:scaleType="fitXY"/>
+ <TextView
+ android:id="@+id/instant_app_maturity_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/instant_app_monetization"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:visibility="gone"/>
+
+
<LinearLayout
android:id="@+id/app_detail_links"
android:layout_width="match_parent"
diff --git a/src/com/android/settings/applications/AppHeaderController.java b/src/com/android/settings/applications/AppHeaderController.java
index a6321df..5b243ff 100644
--- a/src/com/android/settings/applications/AppHeaderController.java
+++ b/src/com/android/settings/applications/AppHeaderController.java
@@ -37,6 +37,7 @@
import com.android.settings.AppHeader;
import com.android.settings.R;
import com.android.settings.Utils;
+import com.android.settings.applications.instantapps.InstantAppDetails;
import com.android.settingslib.applications.ApplicationsState;
import java.lang.annotation.Retention;
@@ -78,6 +79,8 @@
@ActionType
private int mRightAction;
+ private InstantAppDetails mInstantAppDetails;
+
public AppHeaderController(Context context, Fragment fragment, View appHeader) {
mContext = context;
mFragment = fragment;
@@ -147,6 +150,11 @@
return this;
}
+ public AppHeaderController setInstantAppDetails(InstantAppDetails instantAppDetails) {
+ mInstantAppDetails = instantAppDetails;
+ return this;
+ }
+
/**
* Binds app header view and data from {@code PackageInfo} and {@code AppEntry}.
*/
@@ -207,6 +215,29 @@
if (rebindActions) {
bindAppHeaderButtons();
}
+
+ if (mInstantAppDetails != null) {
+ setText(R.id.instant_app_developer_title, mInstantAppDetails.developerTitle);
+ View maturity = mAppHeader.findViewById(R.id.instant_app_maturity);
+
+ if (maturity != null) {
+ String maturityText = mInstantAppDetails.maturityRatingString;
+ Drawable maturityIcon = mInstantAppDetails.maturityRatingIcon;
+ if (!TextUtils.isEmpty(maturityText) || maturityIcon != null) {
+ maturity.setVisibility(View.VISIBLE);
+ }
+ setText(R.id.instant_app_maturity_text, maturityText);
+ if (maturityIcon != null) {
+ ImageView maturityIconView = (ImageView) mAppHeader.findViewById(
+ R.id.instant_app_maturity_icon);
+ if (maturityIconView != null) {
+ maturityIconView.setImageDrawable(maturityIcon);
+ }
+ }
+ }
+ setText(R.id.instant_app_monetization, mInstantAppDetails.monetizationNotice);
+ }
+
return mAppHeader;
}
diff --git a/src/com/android/settings/applications/instantapps/InstantAppDetails.java b/src/com/android/settings/applications/instantapps/InstantAppDetails.java
new file mode 100644
index 0000000..8b54c20
--- /dev/null
+++ b/src/com/android/settings/applications/instantapps/InstantAppDetails.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 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.instantapps;
+
+import android.graphics.drawable.Drawable;
+import java.net.URL;
+
+/**
+ * Encapsulates state about instant apps that is provided by an app store implementation.
+ */
+public class InstantAppDetails {
+
+ // Most of these members are self-explanatory; the one that may not be is
+ // monetizationNotice, which is a string alerting users that the app contains ads and/or uses
+ // in-app purchases (this may eventually become two separate members).
+ public final Drawable maturityRatingIcon;
+ public final String maturityRatingString;
+ public final String monetizationNotice;
+ public final String developerTitle;
+ public final URL privacyPolicy;
+ public final URL developerWebsite;
+ public final String developerEmail;
+ public final String developerMailingAddress;
+
+ public static class Builder {
+ private Drawable mMaturityRatingIcon;
+ private String mMaturityRatingString;
+ private String mMonetizationNotice;
+ private String mDeveloperTitle;
+ private URL mPrivacyPolicy;
+ private URL mDeveloperWebsite;
+ private String mDeveloperEmail;
+ private String mDeveloperMailingAddress;
+
+ public Builder maturityRatingIcon(Drawable maturityRatingIcon) {
+ this.mMaturityRatingIcon = maturityRatingIcon;
+ return this;
+ }
+
+ public Builder maturityRatingString(String maturityRatingString) {
+ mMaturityRatingString = maturityRatingString;
+ return this;
+ }
+
+ public Builder monetizationNotice(String monetizationNotice) {
+ mMonetizationNotice = monetizationNotice;
+ return this;
+ }
+
+ public Builder developerTitle(String developerTitle) {
+ mDeveloperTitle = developerTitle;
+ return this;
+ }
+
+ public Builder privacyPolicy(URL privacyPolicy) {
+ mPrivacyPolicy = privacyPolicy;
+ return this;
+ }
+
+ public Builder developerWebsite(URL developerWebsite) {
+ mDeveloperWebsite = developerWebsite;
+ return this;
+ }
+
+ public Builder developerEmail(String developerEmail) {
+ mDeveloperEmail = developerEmail;
+ return this;
+ }
+
+ public Builder developerMailingAddress(String developerMailingAddress) {
+ mDeveloperMailingAddress = developerMailingAddress;
+ return this;
+ }
+
+ public InstantAppDetails build() {
+ return new InstantAppDetails(mMaturityRatingIcon, mMaturityRatingString,
+ mMonetizationNotice, mDeveloperTitle, mPrivacyPolicy, mDeveloperWebsite,
+ mDeveloperEmail, mDeveloperMailingAddress);
+ }
+ }
+
+ public static Builder builder() { return new Builder(); }
+
+ private InstantAppDetails(Drawable maturityRatingIcon, String maturityRatingString,
+ String monetizationNotice, String developerTitle, URL privacyPolicy,
+ URL developerWebsite, String developerEmail, String developerMailingAddress) {
+ this.maturityRatingIcon = maturityRatingIcon;
+ this.maturityRatingString = maturityRatingString;
+ this.monetizationNotice = monetizationNotice;
+ this.developerTitle = developerTitle;
+ this.privacyPolicy = privacyPolicy;
+ this.developerWebsite = developerWebsite;
+ this.developerEmail = developerEmail;
+ this.developerMailingAddress = developerMailingAddress;
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/applications/AppHeaderControllerTest.java b/tests/robotests/src/com/android/settings/applications/AppHeaderControllerTest.java
index 229ba45..1625e1c 100644
--- a/tests/robotests/src/com/android/settings/applications/AppHeaderControllerTest.java
+++ b/tests/robotests/src/com/android/settings/applications/AppHeaderControllerTest.java
@@ -17,6 +17,7 @@
package com.android.settings.applications;
+import android.annotation.IdRes;
import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
@@ -24,15 +25,19 @@
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.support.v7.preference.Preference;
import android.view.LayoutInflater;
import android.view.View;
+import android.widget.ImageView;
import android.widget.TextView;
import com.android.settings.R;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
+import com.android.settings.applications.InstantDataBuilder.Param;
+import com.android.settings.applications.instantapps.InstantAppDetails;
import com.android.settingslib.applications.ApplicationsState;
import org.junit.Before;
@@ -51,6 +56,8 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.util.EnumSet;
+
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class AppHeaderControllerTest {
@@ -243,4 +250,103 @@
assertThat(appLinks.findViewById(R.id.right_button).getVisibility())
.isEqualTo(View.GONE);
}
+
+ // Ensure that no instant app related information shows up when the AppHeaderController's
+ // InstantAppDetails are null.
+ @Test
+ public void instantApps_nullInstantAppDetails() {
+ final View appHeader = mLayoutInflater.inflate(R.layout.app_details, null /* root */);
+ mController = new AppHeaderController(mContext, mFragment, appHeader);
+ mController.setInstantAppDetails(null);
+ mController.done();
+ assertThat(appHeader.findViewById(R.id.instant_app_developer_title).getVisibility())
+ .isEqualTo(View.GONE);
+ assertThat(appHeader.findViewById(R.id.instant_app_maturity).getVisibility())
+ .isEqualTo(View.GONE);
+ assertThat(appHeader.findViewById(R.id.instant_app_monetization).getVisibility())
+ .isEqualTo(View.GONE);
+ }
+
+ // Ensure that no instant app related information shows up when the AppHeaderController has
+ // a non-null InstantAppDetails, but each member of it is null.
+ @Test
+ public void instantApps_detailsMembersNull() {
+ final View appHeader = mLayoutInflater.inflate(R.layout.app_details, null /* root */);
+ mController = new AppHeaderController(mContext, mFragment, appHeader);
+
+ InstantAppDetails details = InstantDataBuilder.build(mContext, EnumSet.noneOf(Param.class));
+ mController.setInstantAppDetails(details);
+ mController.done();
+ assertThat(appHeader.findViewById(R.id.instant_app_developer_title).getVisibility())
+ .isEqualTo(View.GONE);
+ assertThat(appHeader.findViewById(R.id.instant_app_maturity).getVisibility())
+ .isEqualTo(View.GONE);
+ assertThat(appHeader.findViewById(R.id.instant_app_monetization).getVisibility())
+ .isEqualTo(View.GONE);
+ }
+
+ // Helper to assert a TextView for a given id is visible and has a certain string value.
+ private void assertVisibleContent(View header, @IdRes int id, String expectedValue) {
+ TextView view = (TextView)header.findViewById(id);
+ assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(view.getText()).isEqualTo(expectedValue);
+ }
+
+ // Helper to assert an ImageView for a given id is visible and has a certain Drawable value.
+ private void assertVisibleContent(View header, @IdRes int id, Drawable expectedValue) {
+ ImageView view = (ImageView)header.findViewById(id);
+ assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(view.getDrawable()).isEqualTo(expectedValue);
+ }
+
+ // Test that expected items are present in the header when we have a complete InstantAppDetails.
+ @Test
+ public void instantApps_expectedHeaderItems() {
+ final View header = mLayoutInflater.inflate(R.layout.app_details, null /* root */);
+ mController = new AppHeaderController(mContext, mFragment, header);
+
+ InstantAppDetails details = InstantDataBuilder.build(mContext);
+ mController.setInstantAppDetails(details);
+ mController.done();
+
+ assertVisibleContent(header, R.id.instant_app_developer_title, details.developerTitle);
+ assertVisibleContent(header, R.id.instant_app_maturity_icon,
+ details.maturityRatingIcon);
+ assertVisibleContent(header, R.id.instant_app_maturity_text,
+ details.maturityRatingString);
+ assertVisibleContent(header, R.id.instant_app_monetization,
+ details.monetizationNotice);
+ }
+
+ // Test having each member of InstantAppDetails be null.
+ @Test
+ public void instantApps_expectedHeaderItemsWithSingleNullMembers() {
+ final EnumSet<Param> allParams = EnumSet.allOf(Param.class);
+ for (Param paramToRemove : allParams) {
+ EnumSet<Param> params = allParams.clone();
+ params.remove(paramToRemove);
+ final View header = mLayoutInflater.inflate(R.layout.app_details, null /* root */);
+ mController = new AppHeaderController(mContext, mFragment, header);
+ InstantAppDetails details = InstantDataBuilder.build(mContext, params);
+ mController.setInstantAppDetails(details);
+ mController.done();
+
+ if (params.contains(Param.DEVELOPER_TITLE)) {
+ assertVisibleContent(header, R.id.instant_app_developer_title,
+ details.developerTitle);
+ }
+ if (params.contains(Param.MATURITY_RATING_ICON)) {
+ assertVisibleContent(header, R.id.instant_app_maturity_icon,
+ details.maturityRatingIcon);
+ }
+ if (params.contains(Param.MATURITY_RATING_STRING)) {
+ assertVisibleContent(header, R.id.instant_app_maturity_text,
+ details.maturityRatingString);
+ }
+ if (params.contains(Param.MONETIZATION_NOTICE)) {
+ assertVisibleContent(header, R.id.instant_app_monetization,
+ details.monetizationNotice);
+ }
+ }
+ }
}
diff --git a/tests/robotests/src/com/android/settings/applications/InstantDataBuilder.java b/tests/robotests/src/com/android/settings/applications/InstantDataBuilder.java
new file mode 100644
index 0000000..81ebe06
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/applications/InstantDataBuilder.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright (C) 2017 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;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.settings.applications.instantapps.InstantAppDetails;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.EnumSet;
+
+/**
+ * Utility class for generating fake InstantAppDetails data to use in tests.
+ */
+public class InstantDataBuilder {
+ public enum Param {
+ MATURITY_RATING_ICON,
+ MATURITY_RATING_STRING,
+ MONETIZATION_NOTICE,
+ DEVELOPER_TITLE,
+ PRIVACY_POLICY,
+ DEVELOPER_WEBSITE,
+ DEVELOPER_EMAIL,
+ DEVELOPER_MAILING_ADDRESS
+ }
+
+ /**
+ * Creates an InstantAppDetails with any desired combination of null/non-null members.
+ *
+ * @param context An optional context, required only if MATURITY_RATING_ICON is a member of
+ * params
+ * @param params Specifies which elements of the returned InstantAppDetails should be non-null
+ * @return InstantAppDetails
+ */
+ public static InstantAppDetails build(@Nullable Context context, EnumSet<Param> params) {
+ Drawable ratingIcon = null;
+ String rating = null;
+ String monetizationNotice = null;
+ String developerTitle = null;
+ URL privacyPolicy = null;
+ URL developerWebsite = null;
+ String developerEmail = null;
+ String developerMailingAddress = null;
+
+ if (params.contains(Param.MATURITY_RATING_ICON)) {
+ ratingIcon = context.getDrawable(R.drawable.ic_android);
+ }
+ if (params.contains(Param.MATURITY_RATING_STRING)) {
+ rating = "everyone";
+ }
+ if (params.contains(Param.MONETIZATION_NOTICE)) {
+ monetizationNotice = "Uses in-app purchases";
+ }
+ if (params.contains(Param.DEVELOPER_TITLE)) {
+ developerTitle = "Instant Apps Inc.";
+ }
+ if (params.contains(Param.DEVELOPER_EMAIL)) {
+ developerEmail = "developer@instant-apps.com";
+ }
+ if (params.contains(Param.DEVELOPER_MAILING_ADDRESS)) {
+ developerMailingAddress = "1 Main Street, Somewhere, CA, 94043";
+ }
+
+ if (params.contains(Param.PRIVACY_POLICY)) {
+ try {
+ privacyPolicy = new URL("https://test.com/privacy");
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ if (params.contains(Param.DEVELOPER_WEBSITE)) {
+ try {
+ developerWebsite = new URL("https://test.com");
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return InstantAppDetails.builder()
+ .maturityRatingIcon(ratingIcon)
+ .maturityRatingString(rating)
+ .monetizationNotice(monetizationNotice)
+ .developerTitle(developerTitle)
+ .privacyPolicy(privacyPolicy)
+ .developerWebsite(developerWebsite)
+ .developerEmail(developerEmail)
+ .developerMailingAddress(developerMailingAddress)
+ .build();
+ }
+
+ /**
+ * Convenience method to create an InstantAppDetails with all non-null members.
+ *
+ * @param context a required Context for loading a test maturity rating icon
+ * @return InstantAppDetails
+ */
+ public static InstantAppDetails build(Context context) {
+ return build(context, EnumSet.allOf(Param.class));
+ }
+}