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

Commit f8fb10ff authored by Sarah Chin's avatar Sarah Chin
Browse files

Slice purchase application update notification on locale change

If the locale changes, update the existing notification with the same
information but with resources from the current locale instead of the
previous locale. Use sIntents instead of sSlicePurchaseActivities to
check whether the notification or webview is visible. Don't close
activity on timeout. Fix build errors in tests and add additional tests.

Test: manual verify locale switching, timeout, purchase flow, etc
Test: atest SlicePurchaseBroadcastReceiverTest, SliceStoreActivityTest
Bug: 260262527
Change-Id: I7f0a6a5e32560197e5dc95df00fb59878ad51aa8
parent e1753073
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -77,6 +77,7 @@
        <receiver android:name="com.android.carrierdefaultapp.SlicePurchaseBroadcastReceiver"
                  android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.LOCALE_CHANGED" />
                <action android:name="com.android.phone.slice.action.START_SLICE_PURCHASE_APP" />
                <action android:name="com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_TIMEOUT" />
                <action android:name="com.android.phone.slice.action.NOTIFICATION_CANCELED" />
+1 −7
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ package com.android.carrierdefaultapp;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
@@ -81,8 +80,7 @@ public class SlicePurchaseActivity extends Activity {
                + ", url=" + mUrl);

        // Cancel network boost notification
        mApplicationContext.getSystemService(NotificationManager.class)
                .cancel(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG, mCapability);
        SlicePurchaseBroadcastReceiver.cancelNotification(mApplicationContext, mCapability);

        // Verify intent and values are valid
        if (!SlicePurchaseBroadcastReceiver.isIntentValid(mIntent)) {
@@ -113,9 +111,6 @@ public class SlicePurchaseActivity extends Activity {
            return;
        }

        // Create a reference to this activity in SlicePurchaseBroadcastReceiver
        SlicePurchaseBroadcastReceiver.updateSlicePurchaseActivity(mCapability, this);

        // Create and configure WebView
        setupWebView();
    }
@@ -161,7 +156,6 @@ public class SlicePurchaseActivity extends Activity {
        logd("onDestroy: User canceled the purchase by closing the application.");
        SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
                mIntent, SlicePurchaseController.EXTRA_INTENT_CANCELED);
        SlicePurchaseBroadcastReceiver.removeSlicePurchaseActivity(mCapability);
        super.onDestroy();
    }

+87 −44
Original line number Diff line number Diff line
@@ -24,7 +24,11 @@ import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.drawable.Icon;
import android.os.LocaleList;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.telephony.AnomalyReporter;
import android.telephony.SubscriptionManager;
@@ -36,8 +40,8 @@ import android.webkit.WebView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.phone.slice.SlicePurchaseController;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;

@@ -57,10 +61,6 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
     */
    private static final String UUID_BAD_PENDING_INTENT = "c360246e-95dc-4abf-9dc1-929a76cd7e53";

    /** Weak references to {@link SlicePurchaseActivity} for each capability, if it exists. */
    private static final Map<Integer, WeakReference<SlicePurchaseActivity>>
            sSlicePurchaseActivities = new HashMap<>();

    /** Channel ID for the network boost notification. */
    private static final String NETWORK_BOOST_NOTIFICATION_CHANNEL_ID = "network_boost";
    /** Tag for the network boost notification. */
@@ -70,27 +70,28 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
            "com.android.phone.slice.action.NOTIFICATION_CANCELED";

    /**
     * Create a weak reference to {@link SlicePurchaseActivity}. The reference will be removed when
     * {@link SlicePurchaseActivity#onDestroy()} is called.
     *
     * @param capability The premium capability requested.
     * @param slicePurchaseActivity The instance of SlicePurchaseActivity.
     * A map of Intents sent by {@link SlicePurchaseController} for each capability.
     * If this map contains an Intent for a given capability, the network boost notification to
     * purchase the capability is visible to the user.
     * If this map does not contain an Intent for a given capability, either the capability was
     * never requested or the {@link SlicePurchaseActivity} is visible to the user.
     * An Intent is added to this map when the network boost notification is displayed to the user
     * and removed from the map when the notification is canceled.
     */
    public static void updateSlicePurchaseActivity(
            @TelephonyManager.PremiumCapability int capability,
            @NonNull SlicePurchaseActivity slicePurchaseActivity) {
        sSlicePurchaseActivities.put(capability, new WeakReference<>(slicePurchaseActivity));
    }
    private static final Map<Integer, Intent> sIntents = new HashMap<>();

    /**
     * Remove the weak reference to {@link SlicePurchaseActivity} when
     * {@link SlicePurchaseActivity#onDestroy()} is called.
     * Cancel the network boost notification for the given capability and
     * remove the corresponding notification intent from the map.
     *
     * @param capability The premium capability requested.
     * @param context The context to cancel the notification in.
     * @param capability The premium capability to cancel the notification for.
     */
    public static void removeSlicePurchaseActivity(
    public static void cancelNotification(@NonNull Context context,
            @TelephonyManager.PremiumCapability int capability) {
        sSlicePurchaseActivities.remove(capability);
        context.getSystemService(NotificationManager.class).cancelAsUser(
                NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
        sIntents.remove(capability);
    }

    /**
@@ -223,8 +224,11 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
    public void onReceive(@NonNull Context context, @NonNull Intent intent) {
        logd("onReceive intent: " + intent.getAction());
        switch (intent.getAction()) {
            case Intent.ACTION_LOCALE_CHANGED:
                onLocaleChanged(context);
                break;
            case SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP:
                onDisplayNetworkBoostNotification(context, intent);
                onDisplayNetworkBoostNotification(context, intent, false);
                break;
            case SlicePurchaseController.ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT:
                onTimeout(context, intent);
@@ -237,17 +241,31 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
        }
    }

    private void onLocaleChanged(@NonNull Context context) {
        if (sIntents.isEmpty()) return;

        for (int capability : new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY}) {
            if (sIntents.get(capability) != null) {
                // Notification is active -- update notification for new locale
                context.getSystemService(NotificationManager.class).cancelAsUser(
                        NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
                onDisplayNetworkBoostNotification(context, sIntents.get(capability), true);
            }
        }
    }

    private void onDisplayNetworkBoostNotification(@NonNull Context context,
            @NonNull Intent intent) {
        if (!isIntentValid(intent)) {
            @NonNull Intent intent, boolean repeat) {
        if (!repeat && !isIntentValid(intent)) {
            sendSlicePurchaseAppResponse(intent,
                    SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED);
            return;
        }

        Resources res = getResources(context);
        NotificationChannel channel = new NotificationChannel(
                NETWORK_BOOST_NOTIFICATION_CHANNEL_ID,
                context.getResources().getString(R.string.network_boost_notification_channel),
                res.getString(R.string.network_boost_notification_channel),
                NotificationManager.IMPORTANCE_DEFAULT);
        // CarrierDefaultApp notifications are unblockable by default. Make this channel blockable
        //  to allow users to disable notifications posted to this channel without affecting other
@@ -257,12 +275,11 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{

        Notification notification =
                new Notification.Builder(context, NETWORK_BOOST_NOTIFICATION_CHANNEL_ID)
                        .setContentTitle(String.format(context.getResources().getString(
                        .setContentTitle(String.format(res.getString(
                                R.string.network_boost_notification_title),
                                intent.getStringExtra(
                                        SlicePurchaseController.EXTRA_REQUESTING_APP_NAME)))
                        .setContentText(context.getResources().getString(
                                R.string.network_boost_notification_detail))
                        .setContentText(res.getString(R.string.network_boost_notification_detail))
                        .setSmallIcon(R.drawable.ic_network_boost)
                        .setContentIntent(createContentIntent(context, intent, 1))
                        .setDeleteIntent(intent.getParcelableExtra(
@@ -271,27 +288,57 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
                        // the user canceling or closing the notification.
                        .addAction(new Notification.Action.Builder(
                                Icon.createWithResource(context, R.drawable.ic_network_boost),
                                context.getResources().getString(
                                        R.string.network_boost_notification_button_not_now),
                                res.getString(R.string.network_boost_notification_button_not_now),
                                createCanceledIntent(context, intent)).build())
                        // Add an action for the "Manage" button, which has the same behavior as
                        // the user clicking on the notification.
                        .addAction(new Notification.Action.Builder(
                                Icon.createWithResource(context, R.drawable.ic_network_boost),
                                context.getResources().getString(
                                        R.string.network_boost_notification_button_manage),
                                res.getString(R.string.network_boost_notification_button_manage),
                                createContentIntent(context, intent, 2)).build())
                        .build();

        int capability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
        logd("Display the network boost notification for capability "
        logd((repeat ? "Update" : "Display") + " the network boost notification for capability "
                + TelephonyManager.convertPremiumCapabilityToString(capability));
        context.getSystemService(NotificationManager.class).notifyAsUser(
                NETWORK_BOOST_NOTIFICATION_TAG, capability, notification, UserHandle.ALL);
        if (!repeat) {
            sIntents.put(capability, intent);
            sendSlicePurchaseAppResponse(intent,
                    SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN);
        }
    }

    /**
     * Get the {@link Resources} for the current locale.
     *
     * @param context The context to get the resources in.
     *
     * @return The resources in the current locale.
     */
    @VisibleForTesting
    @NonNull public Resources getResources(@NonNull Context context) {
        Resources resources = context.getResources();
        Configuration config = resources.getConfiguration();
        config.setLocale(getCurrentLocale());
        return new Resources(resources.getAssets(), resources.getDisplayMetrics(), config);
    }

    /**
     * Get the current {@link Locale} from the system property {@code persist.sys.locale}.
     *
     * @return The user's default/preferred language.
     */
    @VisibleForTesting
    @NonNull public Locale getCurrentLocale() {
        String languageTag = SystemProperties.get("persist.sys.locale");
        if (TextUtils.isEmpty(languageTag)) {
            return LocaleList.getAdjustedDefault().get(0);
        }
        return Locale.forLanguageTag(languageTag);
    }

    /**
     * Create the intent for when the user clicks on the "Manage" button on the network boost
@@ -343,16 +390,13 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
                SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
        logd("Purchase capability " + TelephonyManager.convertPremiumCapabilityToString(capability)
                + " timed out.");
        if (sSlicePurchaseActivities.get(capability) == null) {
            // Notification is still active
        if (sIntents.get(capability) != null) {
            // Notification is still active -- cancel pending notification
            logd("Closing network boost notification since the user did not respond in time.");
            context.getSystemService(NotificationManager.class).cancelAsUser(
                    NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
            cancelNotification(context, capability);
        } else {
            // Notification was dismissed but SlicePurchaseActivity is still active
            logd("Closing slice purchase application WebView since the user did not complete the "
                    + "purchase in time.");
            sSlicePurchaseActivities.get(capability).get().finishAndRemoveTask();
            // SlicePurchaseActivity is still active -- ignore timer
            logd("Ignoring timeout since the SlicePurchaseActivity is still active.");
        }
    }

@@ -360,8 +404,7 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
        int capability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
        logd("onUserCanceled: " + TelephonyManager.convertPremiumCapabilityToString(capability));
        context.getSystemService(NotificationManager.class)
                .cancelAsUser(NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
        cancelNotification(context, capability);
        sendSlicePurchaseAppResponse(intent, SlicePurchaseController.EXTRA_INTENT_CANCELED);
    }

+1 −0
Original line number Diff line number Diff line
@@ -37,5 +37,6 @@ android_test {
    // Include all test java files.
    srcs: ["src/**/*.java"],
    platform_apis: true,
    use_embedded_native_libs: false,
    instrumentation_for: "CarrierDefaultApp",
}
+1 −1
Original line number Diff line number Diff line
@@ -17,7 +17,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.carrierdefaultapp.tests.unit">
    <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT" />
    <application>
    <application android:extractNativeLibs="true">
        <uses-library android:name="android.test.runner" />
    </application>

Loading