Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f3ddd87c authored by Antony Sargent's avatar Antony Sargent
Browse files

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
parent 99f0b444
Loading
Loading
Loading
Loading
+35 −0
Original line number Diff line number Diff line
@@ -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"
+31 −0
Original line number Diff line number Diff line
@@ -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;
    }

+110 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+106 −0
Original line number Diff line number Diff line
@@ -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);
            }
        }
    }
}
+118 −0
Original line number Diff line number Diff line
/**
 * 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));
    }
}