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
diff --git a/packages/CarrierDefaultApp/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml
index c4bb17c..3f86aba 100644
--- a/packages/CarrierDefaultApp/AndroidManifest.xml
+++ b/packages/CarrierDefaultApp/AndroidManifest.xml
@@ -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" />
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
index c524037..c8bc771 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
@@ -19,7 +19,6 @@
 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 @@
                 + ", 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 @@
             return;
         }
 
-        // Create a reference to this activity in SlicePurchaseBroadcastReceiver
-        SlicePurchaseBroadcastReceiver.updateSlicePurchaseActivity(mCapability, this);
-
         // Create and configure WebView
         setupWebView();
     }
@@ -161,7 +156,6 @@
         logd("onDestroy: User canceled the purchase by closing the application.");
         SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
                 mIntent, SlicePurchaseController.EXTRA_INTENT_CANCELED);
-        SlicePurchaseBroadcastReceiver.removeSlicePurchaseActivity(mCapability);
         super.onDestroy();
     }
 
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
index 367ae06..3cc2a554 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
@@ -24,7 +24,11 @@
 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 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 @@
      */
     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 @@
             "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 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 @@
         }
     }
 
+    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 @@
 
         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,26 +288,56 @@
                         // 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);
-        sendSlicePurchaseAppResponse(intent,
-                SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN);
+        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);
     }
 
     /**
@@ -343,16 +390,13 @@
                 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 @@
         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);
     }
 
diff --git a/packages/CarrierDefaultApp/tests/unit/Android.bp b/packages/CarrierDefaultApp/tests/unit/Android.bp
index cdf7957..0d08ec6 100644
--- a/packages/CarrierDefaultApp/tests/unit/Android.bp
+++ b/packages/CarrierDefaultApp/tests/unit/Android.bp
@@ -37,5 +37,6 @@
     // Include all test java files.
     srcs: ["src/**/*.java"],
     platform_apis: true,
+    use_embedded_native_libs: false,
     instrumentation_for: "CarrierDefaultApp",
 }
diff --git a/packages/CarrierDefaultApp/tests/unit/AndroidManifest.xml b/packages/CarrierDefaultApp/tests/unit/AndroidManifest.xml
index 7a26d95..995170a 100644
--- a/packages/CarrierDefaultApp/tests/unit/AndroidManifest.xml
+++ b/packages/CarrierDefaultApp/tests/unit/AndroidManifest.xml
@@ -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>
 
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
index ab99a76..20ffb27 100644
--- a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
@@ -19,9 +19,12 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
@@ -35,11 +38,11 @@
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.os.UserHandle;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
-import android.util.DisplayMetrics;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -52,6 +55,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Locale;
+
 @RunWith(AndroidJUnit4.class)
 public class SlicePurchaseBroadcastReceiverTest {
     private static final int PHONE_ID = 0;
@@ -67,24 +72,26 @@
     @Mock PendingIntent mNotificationShownIntent;
     @Mock Context mContext;
     @Mock Resources mResources;
+    @Mock Configuration mConfiguration;
     @Mock NotificationManager mNotificationManager;
     @Mock ApplicationInfo mApplicationInfo;
     @Mock PackageManager mPackageManager;
-    @Mock DisplayMetrics mDisplayMetrics;
-    @Mock SlicePurchaseActivity mSlicePurchaseActivity;
 
     private SlicePurchaseBroadcastReceiver mSlicePurchaseBroadcastReceiver;
-    private ArgumentCaptor<Intent> mIntentCaptor;
-    private ArgumentCaptor<Notification> mNotificationCaptor;
+    private Resources mSpiedResources;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+        mSpiedResources = spy(Resources.getSystem());
+
+        doReturn("").when(mResources).getString(anyInt());
         doReturn(mNotificationManager).when(mContext)
                 .getSystemService(eq(NotificationManager.class));
+        doReturn(mApplicationInfo).when(mContext).getApplicationInfo();
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+        doReturn(mSpiedResources).when(mContext).getResources();
 
-        mIntentCaptor = ArgumentCaptor.forClass(Intent.class);
-        mNotificationCaptor = ArgumentCaptor.forClass(Notification.class);
         mSlicePurchaseBroadcastReceiver = spy(new SlicePurchaseBroadcastReceiver());
     }
 
@@ -109,8 +116,9 @@
                 eq(EXTRA), eq(PendingIntent.class));
         SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(
                 mContext, mIntent, EXTRA, mDataIntent);
-        verify(mPendingIntent).send(eq(mContext), eq(0), mIntentCaptor.capture());
-        assertEquals(mDataIntent, mIntentCaptor.getValue());
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        verify(mPendingIntent).send(eq(mContext), eq(0), captor.capture());
+        assertEquals(mDataIntent, captor.getValue());
     }
 
     @Test
@@ -138,17 +146,29 @@
 
     @Test
     public void testDisplayNetworkBoostNotification() throws Exception {
-        // set up intent
-        doReturn(SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP).when(mIntent).getAction();
-        doReturn(PHONE_ID).when(mIntent).getIntExtra(
-                eq(SlicePurchaseController.EXTRA_PHONE_ID), anyInt());
-        doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mIntent).getIntExtra(
-                eq(SlicePurchaseController.EXTRA_SUB_ID), anyInt());
-        doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
-                eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
-        doReturn(TAG).when(mIntent).getStringExtra(
-                eq(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME));
+        displayNetworkBoostNotification();
 
+        // verify network boost notification was shown
+        ArgumentCaptor<Notification> captor = ArgumentCaptor.forClass(Notification.class);
+        verify(mNotificationManager).notifyAsUser(
+                eq(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG),
+                eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
+                captor.capture(),
+                eq(UserHandle.ALL));
+
+        // verify notification fields
+        Notification notification = captor.getValue();
+        assertEquals(mContentIntent1, notification.contentIntent);
+        assertEquals(mPendingIntent, notification.deleteIntent);
+        assertEquals(2, notification.actions.length);
+        assertEquals(mCanceledIntent, notification.actions[0].actionIntent);
+        assertEquals(mContentIntent2, notification.actions[1].actionIntent);
+
+        // verify SlicePurchaseController was notified
+        verify(mNotificationShownIntent).send();
+    }
+
+    private void displayNetworkBoostNotification() {
         // set up pending intents
         doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mPendingIntent).getCreatorPackage();
         doReturn(true).when(mPendingIntent).isBroadcast();
@@ -161,14 +181,7 @@
                 eq(SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN),
                 eq(PendingIntent.class));
 
-        // set up notification
-        doReturn(mResources).when(mContext).getResources();
-        doReturn(mDisplayMetrics).when(mResources).getDisplayMetrics();
-        doReturn("").when(mResources).getString(anyInt());
-        doReturn(mApplicationInfo).when(mContext).getApplicationInfo();
-        doReturn(mPackageManager).when(mContext).getPackageManager();
-
-        // set up intents created by broadcast receiver
+        // spy notification intents to prevent PendingIntent issues
         doReturn(mContentIntent1).when(mSlicePurchaseBroadcastReceiver).createContentIntent(
                 eq(mContext), eq(mIntent), eq(1));
         doReturn(mContentIntent2).when(mSlicePurchaseBroadcastReceiver).createContentIntent(
@@ -176,35 +189,28 @@
         doReturn(mCanceledIntent).when(mSlicePurchaseBroadcastReceiver).createCanceledIntent(
                 eq(mContext), eq(mIntent));
 
+        // spy resources to prevent resource not found issues
+        doReturn(mResources).when(mSlicePurchaseBroadcastReceiver).getResources(eq(mContext));
+
         // send ACTION_START_SLICE_PURCHASE_APP
+        doReturn(SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP).when(mIntent).getAction();
+        doReturn(PHONE_ID).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_PHONE_ID), anyInt());
+        doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_SUB_ID), anyInt());
+        doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
+        doReturn(TAG).when(mIntent).getStringExtra(
+                eq(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME));
         mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent);
-
-        // verify network boost notification was shown
-        verify(mNotificationManager).notifyAsUser(
-                eq(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG),
-                eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
-                mNotificationCaptor.capture(),
-                eq(UserHandle.ALL));
-
-        Notification notification = mNotificationCaptor.getValue();
-        assertEquals(mContentIntent1, notification.contentIntent);
-        assertEquals(mPendingIntent, notification.deleteIntent);
-        assertEquals(2, notification.actions.length);
-        assertEquals(mCanceledIntent, notification.actions[0].actionIntent);
-        assertEquals(mContentIntent2, notification.actions[1].actionIntent);
-
-        // verify SlicePurchaseController was notified
-        verify(mNotificationShownIntent).send();
     }
 
     @Test
     public void testNotificationCanceled() {
-        // set up intent
+        // send ACTION_NOTIFICATION_CANCELED
         doReturn("com.android.phone.slice.action.NOTIFICATION_CANCELED").when(mIntent).getAction();
         doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
                 eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
-
-        // send ACTION_NOTIFICATION_CANCELED
         mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent);
 
         // verify notification was canceled
@@ -215,14 +221,14 @@
     }
 
     @Test
-    public void testNotificationTimeout() {
-        // set up intent
+    public void testNotificationTimeout() throws Exception {
+        displayNetworkBoostNotification();
+
+        // send ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT
         doReturn(SlicePurchaseController.ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT).when(mIntent)
                 .getAction();
         doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
                 eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
-
-        // send ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT
         mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent);
 
         // verify notification was canceled
@@ -233,27 +239,52 @@
     }
 
     @Test
-    // TODO: WebView/Activity should not close on timeout.
-    //  This test should be removed once implementation is fixed.
-    public void testActivityTimeout() {
-        // create and track activity
-        SlicePurchaseBroadcastReceiver.updateSlicePurchaseActivity(
-                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, mSlicePurchaseActivity);
+    public void testLocaleChanged() throws Exception {
+        // get previous locale
+        doReturn(mConfiguration).when(mSpiedResources).getConfiguration();
+        Locale before = getLocale();
 
-        // set up intent
-        doReturn(SlicePurchaseController.ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT).when(mIntent)
-                .getAction();
-        doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
-                eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
+        // display notification
+        displayNetworkBoostNotification();
+        clearInvocations(mNotificationManager);
+        clearInvocations(mNotificationShownIntent);
 
-        // send ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT
+        // change current locale from previous value
+        Locale newLocale = Locale.forLanguageTag("en-US");
+        if (before.equals(newLocale)) {
+            newLocale = Locale.forLanguageTag("ko-KR");
+        }
+        doReturn(newLocale).when(mSlicePurchaseBroadcastReceiver).getCurrentLocale();
+
+        // send ACTION_LOCALE_CHANGED
+        doReturn(Intent.ACTION_LOCALE_CHANGED).when(mIntent).getAction();
         mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent);
 
-        // verify activity was canceled
-        verify(mSlicePurchaseActivity).finishAndRemoveTask();
+        // verify notification was updated and SlicePurchaseController was not notified
+        verify(mNotificationManager).cancelAsUser(
+                eq(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG),
+                eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
+                eq(UserHandle.ALL));
+        verify(mNotificationManager).notifyAsUser(
+                eq(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG),
+                eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
+                any(Notification.class),
+                eq(UserHandle.ALL));
+        verify(mNotificationShownIntent, never()).send();
 
-        // untrack activity
-        SlicePurchaseBroadcastReceiver.removeSlicePurchaseActivity(
-                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
+        // verify locale was changed successfully
+        doCallRealMethod().when(mSlicePurchaseBroadcastReceiver).getResources(eq(mContext));
+        assertEquals(newLocale, getLocale());
+    }
+
+    private Locale getLocale() {
+        try {
+            mSlicePurchaseBroadcastReceiver.getResources(mContext);
+            fail("getLocale should not have completed successfully.");
+        } catch (NullPointerException expected) { }
+        ArgumentCaptor<Locale> captor = ArgumentCaptor.forClass(Locale.class);
+        verify(mConfiguration).setLocale(captor.capture());
+        clearInvocations(mConfiguration);
+        return captor.getValue();
     }
 }