Create notification for premium capability purchase

Test: manual test to cancel/timeout/accept/delay/manage
Test: atest TelephonyManagerTest#testPremiumCapabilities
Bug: 245882092
Change-Id: I34b4b549d78a68be15b6d14c8f7f494fdc091385
diff --git a/res/drawable/ic_network_boost.xml b/res/drawable/ic_network_boost.xml
new file mode 100644
index 0000000..b6a7ae1
--- /dev/null
+++ b/res/drawable/ic_network_boost.xml
@@ -0,0 +1,24 @@
+<!--
+  ~ Copyright (C) 2022 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24"
+        android:tint="?attr/colorControlNormal">
+<path android:fillColor="@android:color/white"
+      android:pathData="M3,17V15H8Q8,15 8,15Q8,15 8,15V13Q8,13 8,13Q8,13 8,13H3V7H10V9H5V11H8Q8.825,11 9.413,11.587Q10,12.175 10,13V15Q10,15.825 9.413,16.413Q8.825,17 8,17ZM21,11V15Q21,15.825 20.413,16.413Q19.825,17 19,17H14Q13.175,17 12.588,16.413Q12,15.825 12,15V9Q12,8.175 12.588,7.587Q13.175,7 14,7H19Q19.825,7 20.413,7.587Q21,8.175 21,9H14Q14,9 14,9Q14,9 14,9V15Q14,15 14,15Q14,15 14,15H19Q19,15 19,15Q19,15 19,15V13H16.5V11Z"/>
+</vector>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 38a86f9..53ca126 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -2198,4 +2198,15 @@
     <!-- Telephony notification channel name for a channel containing SIP accounts removed
      notificatios -->
     <string name="notification_channel_sip_account">Deprecated SIP accounts</string>
+
+    <!-- Telephony notification channel name for network boost notifications. -->
+    <string name="network_boost_notification_channel">Network Boost</string>
+    <!-- Notification title text for the network boost notification. -->
+    <string name="network_boost_notification_title">%s recommends a data boost</string>
+    <!-- Notification detail text for the network boost notification. -->
+    <string name="network_boost_notification_detail">Buy a network boost for better performance</string>
+    <!-- Notification button text to delay the network boost notification. -->
+    <string name="network_boost_notification_button_delay">Not now</string>
+    <!-- Notification button text to manage the network boost notification. -->
+    <string name="network_boost_notification_button_manage">Manage</string>
 </resources>
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index 5699ca8..23390cf 100755
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -472,6 +472,19 @@
         }
     }
 
+    private static final class PurchasePremiumCapabilityArgument {
+        public @TelephonyManager.PremiumCapability int capability;
+        public @NonNull String appName;
+        public @NonNull IIntegerConsumer callback;
+
+        PurchasePremiumCapabilityArgument(@TelephonyManager.PremiumCapability int capability,
+                @NonNull String appName, @NonNull IIntegerConsumer callback) {
+            this.capability = capability;
+            this.appName = appName;
+            this.callback = callback;
+        }
+    }
+
     /**
      * A request object for use with {@link MainThreadHandler}. Requesters should wait() on the
      * request after sending. The main thread will notify the request when it is complete.
@@ -2140,35 +2153,38 @@
                     break;
                 }
 
-                case CMD_PURCHASE_PREMIUM_CAPABILITY:
+                case CMD_PURCHASE_PREMIUM_CAPABILITY: {
                     request = (MainThreadRequest) msg.obj;
                     onCompleted = obtainMessage(EVENT_PURCHASE_PREMIUM_CAPABILITY_DONE, request);
+                    PurchasePremiumCapabilityArgument arg =
+                            (PurchasePremiumCapabilityArgument) request.argument;
                     SliceStore.getInstance(request.phone).purchasePremiumCapability(
-                            ((Pair<Integer, IIntegerConsumer>) request.argument).first,
-                            onCompleted);
+                            arg.capability, arg.appName, onCompleted);
                     break;
+                }
 
-                case EVENT_PURCHASE_PREMIUM_CAPABILITY_DONE:
+                case EVENT_PURCHASE_PREMIUM_CAPABILITY_DONE: {
                     ar = (AsyncResult) msg.obj;
                     request = (MainThreadRequest) ar.userObj;
-                    Pair<Integer, IIntegerConsumer> pair =
-                            (Pair<Integer, IIntegerConsumer>) request.argument;
+                    PurchasePremiumCapabilityArgument arg =
+                            (PurchasePremiumCapabilityArgument) request.argument;
                     try {
                         int result = (int) ar.result;
-                        pair.second.accept(result);
+                        arg.callback.accept(result);
                         log("purchasePremiumCapability: capability="
-                                + TelephonyManager.convertPremiumCapabilityToString(pair.first)
+                                + TelephonyManager.convertPremiumCapabilityToString(arg.capability)
                                 + ", result= "
                                 + TelephonyManager.convertPurchaseResultToString(result));
                     } catch (RemoteException e) {
                         String logStr = "Purchase premium capability "
-                                + TelephonyManager.convertPremiumCapabilityToString(pair.first)
+                                + TelephonyManager.convertPremiumCapabilityToString(arg.capability)
                                 + " failed: " + e;
                         if (DBG) log(logStr);
                         AnomalyReporter.reportAnomaly(
                                 UUID.fromString(PURCHASE_PREMIUM_CAPABILITY_ERROR_UUID), logStr);
                     }
                     break;
+                }
 
                 case CMD_PREPARE_UNATTENDED_REBOOT:
                     request = (MainThreadRequest) msg.obj;
@@ -11274,8 +11290,15 @@
         }
 
         Phone phone = getPhone(subId);
-        Pair<Integer, IIntegerConsumer> argument = new Pair<>(capability, callback);
-        sendRequestAsync(CMD_PURCHASE_PREMIUM_CAPABILITY, argument, phone, null);
+        String appName;
+        try {
+            appName = mApp.getPackageManager().getApplicationLabel(mApp.getPackageManager()
+                    .getApplicationInfo(getCurrentPackageName(), 0)).toString();
+        } catch (PackageManager.NameNotFoundException e) {
+            appName = "An application";
+        }
+        sendRequestAsync(CMD_PURCHASE_PREMIUM_CAPABILITY,
+                new PurchasePremiumCapabilityArgument(capability, appName, callback), phone, null);
     }
 
     /**
diff --git a/src/com/android/phone/slicestore/SliceStore.java b/src/com/android/phone/slicestore/SliceStore.java
index aa564c8..4bf6874 100644
--- a/src/com/android/phone/slicestore/SliceStore.java
+++ b/src/com/android/phone/slicestore/SliceStore.java
@@ -18,6 +18,15 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.drawable.Icon;
 import android.net.ConnectivityManager;
 import android.os.AsyncResult;
 import android.os.Handler;
@@ -26,19 +35,24 @@
 import android.os.PersistableBundle;
 import android.telephony.AnomalyReporter;
 import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.telephony.data.NetworkSliceInfo;
 import android.telephony.data.NetworkSlicingConfig;
 import android.util.Log;
-import android.util.SparseBooleanArray;
+import android.webkit.WebView;
 
 import com.android.internal.telephony.Phone;
+import com.android.phone.R;
 
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 
 /**
@@ -48,10 +62,9 @@
  * they can then call {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
  * to purchase the premium capability. If all conditions are met, a notification will be displayed
  * to the user prompting them to purchase the premium capability. If the user confirms on the
- * notification, a (TODO: add link) WebView will open that allows the user to purchase the
- * premium capability from the carrier. If the purchase is successful, the premium capability
- * will be available for all applications to request through
- * {@link ConnectivityManager#requestNetwork}.
+ * notification, a {@link WebView} will open that allows the user to purchase the premium capability
+ * from the carrier. If the purchase is successful, the premium capability will be available for
+ * all applications to request through {@link ConnectivityManager#requestNetwork}.
  */
 public class SliceStore extends Handler {
     @NonNull private static final String TAG = "SliceStore";
@@ -67,22 +80,104 @@
     /** UUID to report an anomaly when a premium capability is throttled twice in a row. */
     private static final String UUID_CAPABILITY_THROTTLED_TWICE =
             "15574927-e2e2-4593-99d4-2f340d22b383";
+    /** UUID to report an anomaly when the BroadcastReceiver receives an invalid phone ID. */
+    private static final String UUID_INVALID_PHONE_ID = "ced79f1a-8ac0-4260-8cf3-08b54c0494f3";
+    /** UUID to report an anomaly when the BroadcastReceiver receives an unknown action. */
+    private static final String UUID_UNKNOWN_ACTION = "0197efb0-dab1-4b0a-abaf-ac9336ec7923";
 
-    /** Map of phone ID -> SliceStore. */
+    /** Channel ID for the network boost notification. */
+    private static final String NETWORK_BOOST_NOTIFICATION_CHANNEL_ID = "network_boost";
+    /** Tag for the network boost notification. */
+    private static final String NETWORK_BOOST_NOTIFICATION_TAG = "SliceStore.Notification";
+
+    /** Action for when the network boost notification is cancelled. */
+    private static final String ACTION_NOTIFICATION_CANCELED =
+            "com.android.phone.slicestore.action.NOTIFICATION_CANCELED";
+    /** Action for when the user clicks the "Not now" button on the network boost notification. */
+    private static final String ACTION_NOTIFICATION_DELAYED =
+            "com.android.phone.slicestore.action.NOTIFICATION_DELAYED";
+    /** Action for when the user clicks the "Manage" button on the network boost notification. */
+    private static final String ACTION_NOTIFICATION_MANAGE =
+            "com.android.phone.slicestore.action.NOTIFICATION_MANAGE";
+    /** Extra for phone ID to send from the network boost notification. */
+    private static final String EXTRA_PHONE_ID = "com.android.phone.slicestore.extra.PHONE_ID";
+    /** Extra for premium capability to send from the network boost notification. */
+    private static final String EXTRA_PREMIUM_CAPABILITY =
+            "com.android.phone.slicestore.extra.PREMIUM_CAPABILITY";
+
+    /** Map of phone ID -> SliceStore instances. */
     @NonNull private static final Map<Integer, SliceStore> sInstances = new HashMap<>();
 
+    /** The Phone instance used to create the SliceStore */
     @NonNull private final Phone mPhone;
-    @NonNull private final SparseBooleanArray mPurchasedCapabilities = new SparseBooleanArray();
-    @NonNull private final SparseBooleanArray mThrottledCapabilities = new SparseBooleanArray();
-    @NonNull private final SparseBooleanArray mPendingPurchaseCapabilities =
-            new SparseBooleanArray();
+    /** The set of purchased capabilities. */
+    @NonNull private final Set<Integer> mPurchasedCapabilities = new HashSet<>();
+    /** The set of throttled capabilities. */
+    @NonNull private final Set<Integer> mThrottledCapabilities = new HashSet<>();
+    /** A map of pending capabilities to the onComplete message for the purchase request. */
+    @NonNull private final Map<Integer, Message> mPendingPurchaseCapabilities = new HashMap<>();
+    /** A map of capabilities to the CapabilityBroadcastReceiver for the boost notification. */
+    @NonNull private final Map<Integer, CapabilityBroadcastReceiver> mBroadcastReceivers =
+            new HashMap<>();
+    /** The current network slicing configuration. */
     @Nullable private NetworkSlicingConfig mSlicingConfig;
 
+    private final class CapabilityBroadcastReceiver extends BroadcastReceiver {
+        @TelephonyManager.PremiumCapability final int mCapability;
+
+        CapabilityBroadcastReceiver(@TelephonyManager.PremiumCapability int capability) {
+            mCapability = capability;
+        }
+
+        @Override
+        public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+            String action = intent.getAction();
+            log("CapabilityBroadcastReceiver("
+                    + TelephonyManager.convertPremiumCapabilityToString(mCapability)
+                    + ") received action: " + action);
+            int phoneId = intent.getIntExtra(EXTRA_PHONE_ID,
+                    SubscriptionManager.INVALID_PHONE_INDEX);
+            int capability = intent.getIntExtra(EXTRA_PREMIUM_CAPABILITY, -1);
+            if (SliceStore.getInstance(phoneId) == null) {
+                String logStr = "CapabilityBroadcastReceiver( "
+                        + TelephonyManager.convertPremiumCapabilityToString(mCapability)
+                        + ") received invalid phoneId: " + phoneId;
+                loge(logStr);
+                AnomalyReporter.reportAnomaly(UUID.fromString(UUID_INVALID_PHONE_ID), logStr);
+                return;
+            } else if (capability != mCapability) {
+                log("CapabilityBroadcastReceiver("
+                        + TelephonyManager.convertPremiumCapabilityToString(mCapability)
+                        + ") received invalid capability: "
+                        + TelephonyManager.convertPremiumCapabilityToString(capability));
+                return;
+            }
+            switch (action) {
+                case ACTION_NOTIFICATION_CANCELED:
+                    SliceStore.getInstance(phoneId).onUserCanceled(capability);
+                    break;
+                case ACTION_NOTIFICATION_DELAYED:
+                    SliceStore.getInstance(phoneId).onUserDelayed(capability);
+                    break;
+                case ACTION_NOTIFICATION_MANAGE:
+                    SliceStore.getInstance(phoneId).onUserManage(capability);
+                    break;
+                default:
+                    String logStr = "CapabilityBroadcastReceiver("
+                            + TelephonyManager.convertPremiumCapabilityToString(mCapability)
+                            + ") received unknown action: " + action;
+                    loge(logStr);
+                    AnomalyReporter.reportAnomaly(UUID.fromString(UUID_UNKNOWN_ACTION), logStr);
+                    break;
+            }
+        }
+    }
+
     /**
-     * Get the static SliceStore instance for the given phone.
+     * Get the static SliceStore instance for the given phone or create one if it doesn't exist.
      *
-     * @param phone The phone to get the SliceStore for
-     * @return The static SliceStore instance
+     * @param phone The Phone to get the SliceStore for.
+     * @return The static SliceStore instance.
      */
     @NonNull public static synchronized SliceStore getInstance(@NonNull Phone phone) {
         // TODO: Add listeners for multi sim setting changed (maybe carrier config changed too)
@@ -94,6 +189,16 @@
         return sInstances.get(phoneId);
     }
 
+    /**
+     * Get the static SliceStore instance for the given phone ID if it exists.
+     *
+     * @param phoneId The phone ID to get the SliceStore for.
+     * @return The static SliceStore instance or {@code null} if it hasn't been created yet.
+     */
+    @Nullable private static SliceStore getInstance(int phoneId) {
+        return sInstances.get(phoneId);
+    }
+
     private SliceStore(@NonNull Phone phone) {
         super(Looper.myLooper());
         mPhone = phone;
@@ -108,7 +213,7 @@
                 int capability = (int) msg.obj;
                 log("EVENT_PURCHASE_UNTHROTTLED: for capability "
                         + TelephonyManager.convertPremiumCapabilityToString(capability));
-                mThrottledCapabilities.setValueAt(capability, false);
+                mThrottledCapabilities.remove(capability);
                 break;
             }
             case EVENT_SLICING_CONFIG_CHANGED: {
@@ -119,14 +224,18 @@
                 break;
             }
             case EVENT_DISPLAY_BOOSTER_NOTIFICATION: {
-                onDisplayBoosterNotification(msg.arg1, (Message) msg.obj);
+                int capability = msg.arg1;
+                String appName = (String) msg.obj;
+                log("EVENT_DISPLAY_BOOSTER_NOTIFICATION: " + appName + " requests capability "
+                        + TelephonyManager.convertPremiumCapabilityToString(capability));
+                onDisplayBoosterNotification(capability, appName);
                 break;
             }
             case EVENT_PURCHASE_TIMEOUT: {
-                int capability = msg.arg1;
+                int capability = (int) msg.obj;
                 log("EVENT_PURCHASE_TIMEOUT: for capability "
                         + TelephonyManager.convertPremiumCapabilityToString(capability));
-                onTimeout(capability, (Message) msg.obj);
+                onTimeout(capability);
                 break;
             }
         }
@@ -164,11 +273,13 @@
      * Purchase the given premium capability from the carrier.
      *
      * @param capability The premium capability to purchase.
+     * @param appName The name of the application requesting premium capabilities.
      * @param onComplete The callback message to send when the purchase request is complete.
      */
     public synchronized void purchasePremiumCapability(
-            @TelephonyManager.PremiumCapability int capability, @NonNull Message onComplete) {
-        log("purchasePremiumCapability: "
+            @TelephonyManager.PremiumCapability int capability, @NonNull String appName,
+            @NonNull Message onComplete) {
+        log("purchasePremiumCapability: " + appName + " requests capability "
                 + TelephonyManager.convertPremiumCapabilityToString(capability));
         // Check whether the premium capability can be purchased.
         if (!arePremiumCapabilitiesSupportedByDevice()) {
@@ -189,13 +300,14 @@
                     onComplete);
             return;
         }
-        if (mPurchasedCapabilities.get(capability) || isSlicingConfigActive(capability)) {
+        if (mPurchasedCapabilities.contains(capability) || isSlicingConfigActive(capability)) {
+            // TODO (b/245882601): Handle capability expiry
             sendPurchaseResult(capability,
                     TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED,
                     onComplete);
             return;
         }
-        if (mThrottledCapabilities.get(capability)) {
+        if (mThrottledCapabilities.contains(capability)) {
             sendPurchaseResult(capability,
                     TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED,
                     onComplete);
@@ -208,13 +320,14 @@
             return;
         }
         if (isNetworkCongested(capability)) {
-            throttleCapability(capability);
+            throttleCapability(capability, getThrottleDuration(
+                    TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED));
             sendPurchaseResult(capability,
                     TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED,
                     onComplete);
             return;
         }
-        if (mPendingPurchaseCapabilities.get(capability)) {
+        if (mPendingPurchaseCapabilities.containsKey(capability)) {
             sendPurchaseResult(capability,
                     TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS,
                     onComplete);
@@ -223,9 +336,9 @@
 
         // All state checks passed. Mark purchase pending and display the booster notification to
         // prompt user purchase. Process through the handler since this method is synchronized.
-        mPendingPurchaseCapabilities.put(capability, true);
-        sendMessage(obtainMessage(EVENT_DISPLAY_BOOSTER_NOTIFICATION,
-                capability, 0 /* unused */, onComplete));
+        mPendingPurchaseCapabilities.put(capability, onComplete);
+        sendMessage(obtainMessage(EVENT_DISPLAY_BOOSTER_NOTIFICATION, capability, 0 /* unused */,
+                appName));
     }
 
     private void sendPurchaseResult(@TelephonyManager.PremiumCapability int capability,
@@ -239,55 +352,176 @@
         onComplete.sendToTarget();
     }
 
-    private void throttleCapability(@TelephonyManager.PremiumCapability int capability) {
+    private void throttleCapability(@TelephonyManager.PremiumCapability int capability,
+            long throttleDuration) {
         // Throttle subsequent requests if necessary.
-        if (!mThrottledCapabilities.get(capability)) {
-            long throttleTime = getThrottleDuration(capability);
-            if (throttleTime > 0) {
+        if (!mThrottledCapabilities.contains(capability)) {
+            if (throttleDuration > 0) {
                 log("Throttle purchase requests for capability "
                         + TelephonyManager.convertPremiumCapabilityToString(capability) + " for "
-                        + (throttleTime / 1000) + " seconds.");
-                mThrottledCapabilities.setValueAt(capability, true);
+                        + TimeUnit.MILLISECONDS.toMinutes(throttleDuration) + " minutes.");
+                mThrottledCapabilities.add(capability);
                 sendMessageDelayed(obtainMessage(EVENT_PURCHASE_UNTHROTTLED, capability),
-                        throttleTime);
+                        throttleDuration);
             }
         } else {
             String logStr = TelephonyManager.convertPremiumCapabilityToString(capability)
                     + " is already throttled.";
-            log(logStr);
+            loge(logStr);
             AnomalyReporter.reportAnomaly(UUID.fromString(UUID_CAPABILITY_THROTTLED_TWICE), logStr);
         }
     }
 
     private void onDisplayBoosterNotification(@TelephonyManager.PremiumCapability int capability,
-            @NonNull Message onComplete) {
+            @NonNull String appName) {
+        // Start timeout on handler instead of setTimeoutAfter to differentiate cancel and timeout.
         long timeout = getCarrierConfigs().getLong(CarrierConfigManager
                 .KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG);
+        sendMessageDelayed(obtainMessage(EVENT_PURCHASE_TIMEOUT, capability), timeout);
+
         log("Display the booster notification for capability "
                 + TelephonyManager.convertPremiumCapabilityToString(capability) + " for "
-                + (timeout / 1000) + " seconds.");
-        sendMessageDelayed(
-                obtainMessage(EVENT_PURCHASE_TIMEOUT, capability, 0 /* unused */, onComplete),
-                timeout);
-        // TODO(b/245882092): Display notification with listener for
-        //  EVENT_USER_ACTION or EVENT_USER_CANCELED + EVENT_USER_CONFIRMED
+                + TimeUnit.MILLISECONDS.toMinutes(timeout) + " minutes.");
+
+        mPhone.getContext().getSystemService(NotificationManager.class).createNotificationChannel(
+                new NotificationChannel(NETWORK_BOOST_NOTIFICATION_CHANNEL_ID,
+                        mPhone.getContext().getResources().getString(
+                                R.string.network_boost_notification_channel),
+                        NotificationManager.IMPORTANCE_DEFAULT));
+        mBroadcastReceivers.put(capability, new CapabilityBroadcastReceiver(capability));
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(ACTION_NOTIFICATION_CANCELED);
+        filter.addAction(ACTION_NOTIFICATION_DELAYED);
+        filter.addAction(ACTION_NOTIFICATION_MANAGE);
+        mPhone.getContext().registerReceiver(mBroadcastReceivers.get(capability), filter);
+
+        Notification notification =
+                new Notification.Builder(mPhone.getContext(), NETWORK_BOOST_NOTIFICATION_CHANNEL_ID)
+                .setContentTitle(String.format(mPhone.getContext().getResources().getString(
+                        R.string.network_boost_notification_title), appName))
+                .setContentText(mPhone.getContext().getResources().getString(
+                        R.string.network_boost_notification_detail))
+                .setSmallIcon(R.drawable.ic_network_boost)
+                .setContentIntent(getContentIntent(capability))
+                .setDeleteIntent(getDeleteIntent(capability))
+                .addAction(new Notification.Action.Builder(
+                        Icon.createWithResource(mPhone.getContext(), R.drawable.ic_network_boost),
+                        mPhone.getContext().getResources().getString(
+                                R.string.network_boost_notification_button_delay),
+                        getDelayIntent(capability)).build())
+                .addAction(new Notification.Action.Builder(
+                        Icon.createWithResource(mPhone.getContext(), R.drawable.ic_network_boost),
+                        mPhone.getContext().getResources().getString(
+                                R.string.network_boost_notification_button_manage),
+                        getManageIntent(capability)).build())
+                .setAutoCancel(true)
+                .build();
+
+        mPhone.getContext().getSystemService(NotificationManager.class)
+                .notify(NETWORK_BOOST_NOTIFICATION_TAG, capability, notification);
     }
 
-    private void closeBoosterNotification(@TelephonyManager.PremiumCapability int capability) {
-        // TODO(b/245882092): Close notification; maybe cancel purchase timeout
+    /**
+     * Create the content intent for when the user clicks on the network boost notification.
+     * Ths will start the {@link SliceStoreActivity} and display the {@link android.webkit.WebView}
+     * to purchase the premium capability from the carrier.
+     *
+     * @param capability The premium capability that was requested.
+     * @return The content intent.
+     */
+    @NonNull private PendingIntent getContentIntent(
+            @TelephonyManager.PremiumCapability int capability) {
+        Intent intent = new Intent(mPhone.getContext(), SliceStoreActivity.class);
+        intent.putExtra(EXTRA_PHONE_ID, mPhone.getPhoneId());
+        intent.putExtra(EXTRA_PREMIUM_CAPABILITY, capability);
+        return PendingIntent.getActivity(mPhone.getContext(), 0, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
     }
 
-    private void onTimeout(@TelephonyManager.PremiumCapability int capability,
-            @NonNull Message onComplete) {
-        closeBoosterNotification(capability);
-        mPendingPurchaseCapabilities.put(capability, false);
-        throttleCapability(capability);
-        sendPurchaseResult(capability, TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT,
-                onComplete);
+    /**
+     * Create the delete intent for when the user cancels the network boost notification.
+     * This will send {@link #ACTION_NOTIFICATION_CANCELED}.
+     *
+     * @param capability The premium capability that was requested.
+     * @return The delete intent.
+     */
+    @NonNull private PendingIntent getDeleteIntent(
+            @TelephonyManager.PremiumCapability int capability) {
+        Intent intent = new Intent(ACTION_NOTIFICATION_CANCELED);
+        intent.putExtra(EXTRA_PHONE_ID, mPhone.getPhoneId());
+        intent.putExtra(EXTRA_PREMIUM_CAPABILITY, capability);
+        return PendingIntent.getBroadcast(mPhone.getContext(), 0, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
+    }
+
+    /**
+     * Create the delay intent for when the user clicks the "Not now" button on the network boost
+     * notification. This will send {@link #ACTION_NOTIFICATION_DELAYED}.
+     *
+     * @param capability The premium capability that was requested.
+     * @return The delay intent.
+     */
+    @NonNull private PendingIntent getDelayIntent(
+            @TelephonyManager.PremiumCapability int capability) {
+        Intent intent = new Intent(ACTION_NOTIFICATION_DELAYED);
+        intent.putExtra(EXTRA_PHONE_ID, mPhone.getPhoneId());
+        intent.putExtra(EXTRA_PREMIUM_CAPABILITY, capability);
+        return PendingIntent.getBroadcast(mPhone.getContext(), 0, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
+    }
+
+    /**
+     * Create the manage intent for when the user clicks the "Manage" button on the network boost
+     * notification. This will send {@link #ACTION_NOTIFICATION_MANAGE}.
+     *
+     * @param capability The premium capability that was requested.
+     * @return The manage intent.
+     */
+    @NonNull private PendingIntent getManageIntent(
+            @TelephonyManager.PremiumCapability int capability) {
+        Intent intent = new Intent(ACTION_NOTIFICATION_MANAGE);
+        intent.putExtra(EXTRA_PHONE_ID, mPhone.getPhoneId());
+        intent.putExtra(EXTRA_PREMIUM_CAPABILITY, capability);
+        return PendingIntent.getBroadcast(mPhone.getContext(), 0, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
+    }
+
+    private void cleanupBoosterNotification(@TelephonyManager.PremiumCapability int capability,
+            @TelephonyManager.PurchasePremiumCapabilityResult int result) {
+        mPhone.getContext().getSystemService(NotificationManager.class)
+                .cancel(NETWORK_BOOST_NOTIFICATION_TAG, capability);
+        mPhone.getContext().unregisterReceiver(mBroadcastReceivers.remove(capability));
+        Message onComplete = mPendingPurchaseCapabilities.remove(capability);
+        throttleCapability(capability, getThrottleDuration(result));
+        sendPurchaseResult(capability, result, onComplete);
+    }
+
+    private void onTimeout(@TelephonyManager.PremiumCapability int capability) {
+        log("onTimeout: " + TelephonyManager.convertPremiumCapabilityToString(capability));
+        cleanupBoosterNotification(capability,
+                TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT);
+        // TODO: Cancel SliceStoreActivity as well.
     }
 
     private void onUserCanceled(@TelephonyManager.PremiumCapability int capability) {
-        // TODO(b/245882092): Process and return user canceled; throttle
+        log("onUserCanceled: " + TelephonyManager.convertPremiumCapabilityToString(capability));
+        if (hasMessages(EVENT_PURCHASE_TIMEOUT, capability)) {
+            log("onUserCanceled: Removing timeout for capability "
+                    + TelephonyManager.convertPremiumCapabilityToString(capability));
+            removeMessages(EVENT_PURCHASE_TIMEOUT, capability);
+        }
+        cleanupBoosterNotification(capability,
+                TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED);
+    }
+
+    private void onUserDelayed(@TelephonyManager.PremiumCapability int capability) {
+        log("onUserDelayed: " + TelephonyManager.convertPremiumCapabilityToString(capability));
+        // TODO(b/245882092): implement
+    }
+
+    private void onUserManage(@TelephonyManager.PremiumCapability int capability) {
+        log("onUserManage: " + TelephonyManager.convertPremiumCapabilityToString(capability));
+        // TODO(b/245882092): implement
     }
 
     private void onUserConfirmed(@TelephonyManager.PremiumCapability int capability) {
@@ -361,7 +595,7 @@
         return false;
     }
 
-    private @NetworkSliceInfo.SliceServiceType int getSliceServiceType(
+    @NetworkSliceInfo.SliceServiceType private int getSliceServiceType(
             @TelephonyManager.PremiumCapability int capability) {
         // TODO: Implement properly -- potentially need to add new slice service types?
         return NetworkSliceInfo.SLICE_SERVICE_TYPE_NONE;
@@ -375,4 +609,8 @@
     private void log(String s) {
         Log.d(TAG + "-" + mPhone.getPhoneId(), s);
     }
+
+    private void loge(String s) {
+        Log.e(TAG + "-" + mPhone.getPhoneId(), s);
+    }
 }
diff --git a/src/com/android/phone/slicestore/SliceStoreActivity.java b/src/com/android/phone/slicestore/SliceStoreActivity.java
new file mode 100644
index 0000000..b93f757
--- /dev/null
+++ b/src/com/android/phone/slicestore/SliceStoreActivity.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2022 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.phone.slicestore;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * Activity that launches when the user clicks on the network boost notification.
+ */
+public class SliceStoreActivity extends Activity {
+    private static final String TAG = "SliceStoreActivity";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Log.d(TAG, "onCreate");
+    }
+}