Infrastructure for showing instant app metadata in app header

This adds infrastructure for displaying the following instant app
metadata in the app header:
-Developer title
-Maturity Rating icon and description string
-Monetization notice (eg ads and/or in-app purchases)

Bug: 35098444
Test: includes new robotests in AppHeaderControllerTest.java
Change-Id: Ifadfedc7f5f349869d6616aeb5ed19eb2b22a038
This commit is contained in:
Antony Sargent
2017-02-07 11:28:58 -08:00
parent 99f0b44440
commit f3ddd87c7a
5 changed files with 400 additions and 0 deletions

View File

@@ -55,6 +55,41 @@
android:textAppearance="@android:style/TextAppearance.Material.Body1"
android:textColor="?android:attr/textColorSecondary"/>
<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"

View File

@@ -37,6 +37,7 @@ import android.widget.TextView;
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 @@ public class AppHeaderController {
@ActionType
private int mRightAction;
private InstantAppDetails mInstantAppDetails;
public AppHeaderController(Context context, Fragment fragment, View appHeader) {
mContext = context;
mFragment = fragment;
@@ -147,6 +150,11 @@ public class AppHeaderController {
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 @@ public class AppHeaderController {
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;
}

View File

@@ -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;
}
}

View File

@@ -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.Intent;
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.mock;
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 @@ public class AppHeaderControllerTest {
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);
}
}
}
}

View File

@@ -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));
}
}