New design for instant apps in app details header

Bug: 35098444
Test: make RunSettingsRoboTests

In the previous design for instant apps, some metadata about the app
such as developer title, maturity rating, etc. was going to be shown
in the app header. In the latest design, we instead are just showing
a little label that says "Instant app".

The two CL's for this topic work together to change this:

frameworks/base : adds code to work around the problem that
robolectric doesn't know about the new isInstantApp method of the
ApplicationInfo class, so we need to avoid calling it during tests.

pacakges/apps/Settings: removes the code that previously displayed
the instant app metadata, and instead just insert the "Instant app"
label.

Change-Id: I2cbc70bf4827c401e862c58ea4ca7f8f9ba1cf58
This commit is contained in:
Antony Sargent
2017-02-28 10:57:45 -08:00
parent bee2554f13
commit d06aaa8fd0
9 changed files with 39 additions and 375 deletions

View File

@@ -87,6 +87,17 @@
android:gravity="start" android:gravity="start"
android:paddingTop="8dp"/> android:paddingTop="8dp"/>
<TextView
android:id="@+id/install_type"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:singleLine="true"
android:ellipsize="marquee"
android:textAppearance="@android:style/TextAppearance.Material.Body1"
android:textColor="?android:attr/textColorSecondary"/>
<TextView <TextView
android:id="@+id/app_detail_summary" android:id="@+id/app_detail_summary"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -101,38 +112,4 @@
</RelativeLayout> </RelativeLayout>
<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> </LinearLayout>

View File

@@ -8220,6 +8220,9 @@
<!-- On status for the automatic storage manager. [CHAR_LIMIT=10] --> <!-- On status for the automatic storage manager. [CHAR_LIMIT=10] -->
<string name="storage_manager_indicator_on">On</string> <string name="storage_manager_indicator_on">On</string>
<!-- Added as the value of a header field indicating this is an instant app (as opposed to installed normally) -->
<string name="install_type_instant">Instant app</string>
<!-- Title of games app storage screen [CHAR LIMIT=30] --> <!-- Title of games app storage screen [CHAR LIMIT=30] -->
<string name="game_storage_settings">Games</string> <string name="game_storage_settings">Games</string>

View File

@@ -37,7 +37,6 @@ import android.widget.TextView;
import com.android.settings.AppHeader; import com.android.settings.AppHeader;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.Utils; import com.android.settings.Utils;
import com.android.settings.applications.instantapps.InstantAppDetails;
import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState;
@@ -81,7 +80,7 @@ public class AppHeaderController {
@ActionType @ActionType
private int mRightAction; private int mRightAction;
private InstantAppDetails mInstantAppDetails; private boolean mIsInstantApp;
public AppHeaderController(Context context, Fragment fragment, View appHeader) { public AppHeaderController(Context context, Fragment fragment, View appHeader) {
mContext = context; mContext = context;
@@ -154,8 +153,8 @@ public class AppHeaderController {
return this; return this;
} }
public AppHeaderController setInstantAppDetails(InstantAppDetails instantAppDetails) { public AppHeaderController setIsInstantApp(boolean isInstantApp) {
mInstantAppDetails = instantAppDetails; this.mIsInstantApp = isInstantApp;
return this; return this;
} }
@@ -220,26 +219,9 @@ public class AppHeaderController {
bindAppHeaderButtons(); bindAppHeaderButtons();
} }
if (mInstantAppDetails != null) { if (mIsInstantApp) {
setText(R.id.instant_app_developer_title, mInstantAppDetails.developerTitle); setText(R.id.install_type,
View maturity = mAppHeader.findViewById(R.id.instant_app_maturity); mAppHeader.getResources().getString(R.string.install_type_instant));
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; return mAppHeader;

View File

@@ -23,6 +23,7 @@ import android.util.Log;
import com.android.settings.AppHeader; import com.android.settings.AppHeader;
import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.applications.AppUtils;
import static com.android.settings.applications.AppHeaderController.ActionType; import static com.android.settings.applications.AppHeaderController.ActionType;
@@ -52,6 +53,7 @@ public abstract class AppInfoWithHeader extends AppInfoBase {
.setIcon(mPackageInfo.applicationInfo.loadIcon(mPm)) .setIcon(mPackageInfo.applicationInfo.loadIcon(mPm))
.setLabel(mPackageInfo.applicationInfo.loadLabel(mPm)) .setLabel(mPackageInfo.applicationInfo.loadLabel(mPm))
.setSummary(mPackageInfo) .setSummary(mPackageInfo)
.setIsInstantApp(AppUtils.isInstant(mPackageInfo.applicationInfo))
.setPackageName(mPackageName) .setPackageName(mPackageName)
.setUid(mPackageInfo.applicationInfo.uid) .setUid(mPackageInfo.applicationInfo.uid)
.setButtonActions(ActionType.ACTION_APP_INFO, ActionType.ACTION_NONE) .setButtonActions(ActionType.ACTION_APP_INFO, ActionType.ACTION_NONE)

View File

@@ -563,6 +563,7 @@ public class InstalledAppDetails extends AppInfoBase
.setLabel(mAppEntry) .setLabel(mAppEntry)
.setIcon(mAppEntry) .setIcon(mAppEntry)
.setSummary(getString(getInstallationStatus(mAppEntry.info))) .setSummary(getString(getInstallationStatus(mAppEntry.info)))
.setIsInstantApp(AppUtils.isInstant(mPackageInfo.applicationInfo))
.done(false /* rebindActions */); .done(false /* rebindActions */);
mVersionPreference.setSummary(getString(R.string.version_text, pkgInfo.versionName)); mVersionPreference.setSummary(getString(R.string.version_text, pkgInfo.versionName));
} else { } else {

View File

@@ -1,110 +0,0 @@
/*
* 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

@@ -36,8 +36,6 @@ import android.widget.TextView;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.SettingsRobolectricTestRunner; import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig; 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 com.android.settingslib.applications.ApplicationsState;
import org.junit.Before; import org.junit.Before;
@@ -251,102 +249,27 @@ public class AppHeaderControllerTest {
.isEqualTo(View.GONE); .isEqualTo(View.GONE);
} }
// Ensure that no instant app related information shows up when the AppHeaderController's // Ensure that the instant app label does not show up when we haven't told the controller the
// InstantAppDetails are null. // app is instant.
@Test @Test
public void instantApps_nullInstantAppDetails() { public void instantApps_normalAppsDontGetLabel() {
final View appHeader = mLayoutInflater.inflate(R.layout.app_details, null /* root */); final View appHeader = mLayoutInflater.inflate(R.layout.app_details, null /* root */);
mController = new AppHeaderController(mContext, mFragment, appHeader); mController = new AppHeaderController(mContext, mFragment, appHeader);
mController.setInstantAppDetails(null);
mController.done(); mController.done();
assertThat(appHeader.findViewById(R.id.instant_app_developer_title).getVisibility()) assertThat(appHeader.findViewById(R.id.install_type).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); .isEqualTo(View.GONE);
} }
// Ensure that no instant app related information shows up when the AppHeaderController has // Test that the "instant apps" label is present in the header when we have an instant app.
// a non-null InstantAppDetails, but each member of it is null.
@Test @Test
public void instantApps_detailsMembersNull() { public void instantApps_expectedHeaderItem() {
final View appHeader = mLayoutInflater.inflate(R.layout.app_details, null /* root */); final View appHeader = mLayoutInflater.inflate(R.layout.app_details, null /* root */);
mController = new AppHeaderController(mContext, mFragment, appHeader); mController = new AppHeaderController(mContext, mFragment, appHeader);
mController.setIsInstantApp(true);
InstantAppDetails details = InstantDataBuilder.build(mContext, EnumSet.noneOf(Param.class));
mController.setInstantAppDetails(details);
mController.done(); mController.done();
assertThat(appHeader.findViewById(R.id.instant_app_developer_title).getVisibility()) TextView label = (TextView)appHeader.findViewById(R.id.install_type);
.isEqualTo(View.GONE); assertThat(label.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(appHeader.findViewById(R.id.instant_app_maturity).getVisibility()) assertThat(label.getText()).isEqualTo(
.isEqualTo(View.GONE); appHeader.getResources().getString(R.string.install_type_instant));
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

@@ -28,6 +28,8 @@ import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.SettingsRobolectricTestRunner; import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig; import com.android.settings.TestConfig;
import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settingslib.applications.AppUtils;
import com.android.settingslib.applications.instantapps.InstantAppDataProvider;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -37,6 +39,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowApplication; import org.robolectric.shadows.ShadowApplication;
import org.robolectric.util.ReflectionHelpers;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@@ -88,7 +91,8 @@ public class AppInfoWithHeaderTest {
mScreen = mock(PreferenceScreen.class); mScreen = mock(PreferenceScreen.class);
mPackageInfo = new PackageInfo(); mPackageInfo = new PackageInfo();
mPackageInfo.applicationInfo = new ApplicationInfo(); mPackageInfo.applicationInfo = new ApplicationInfo();
ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider",
(InstantAppDataProvider) (info -> false));
when(mManager.getContext()) when(mManager.getContext())
.thenReturn(ShadowApplication.getInstance().getApplicationContext()); .thenReturn(ShadowApplication.getInstance().getApplicationContext());
} }

View File

@@ -1,118 +0,0 @@
/**
* 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));
}
}