Merge "Add support for daily and monthly notification maximums"
diff --git a/src/com/android/phone/slice/SlicePurchaseController.java b/src/com/android/phone/slice/SlicePurchaseController.java
index ead6b8c..f258e2c 100644
--- a/src/com/android/phone/slice/SlicePurchaseController.java
+++ b/src/com/android/phone/slice/SlicePurchaseController.java
@@ -31,6 +31,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.SharedPreferences;
 import android.net.ConnectivityManager;
 import android.os.AsyncResult;
 import android.os.Handler;
@@ -57,6 +58,9 @@
 import java.net.MalformedURLException;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeParseException;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -115,8 +119,8 @@
     private static final int EVENT_PURCHASE_UNTHROTTLED = 1;
     /** Slicing config changed. */
     private static final int EVENT_SLICING_CONFIG_CHANGED = 2;
-    /** Display booster notification. */
-    private static final int EVENT_DISPLAY_BOOSTER_NOTIFICATION = 3;
+    /** Start slice purchase application. */
+    private static final int EVENT_START_SLICE_PURCHASE_APP = 3;
     /**
      * Premium capability was not purchased within the timeout specified by
      * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG}.
@@ -170,6 +174,9 @@
     /** Action indicating the purchase request was successful. */
     private static final String ACTION_SLICE_PURCHASE_APP_RESPONSE_SUCCESS =
             "com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_SUCCESS";
+    /** Action indicating the slice purchase application showed the network boost notification. */
+    private static final String ACTION_SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN =
+            "com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN";
 
     /** Extra for the phone index to send to the slice purchase application. */
     public static final String EXTRA_PHONE_ID = "com.android.phone.slice.extra.PHONE_ID";
@@ -235,12 +242,35 @@
      */
     public static final String EXTRA_INTENT_SUCCESS =
             "com.android.phone.slice.extra.INTENT_SUCCESS";
+    /**
+     * Extra for the PendingIntent that the slice purchase application can send to indicate
+     * that it displayed the network boost notification to the user.
+     * Sends {@link #ACTION_SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN}.
+     */
+    public static final String EXTRA_INTENT_NOTIFICATION_SHOWN =
+            "com.android.phone.slice.extra.NOTIFICATION_SHOWN";
 
-    /** Component name to send an explicit broadcast to SlicePurchaseBroadcastReceiver. */
+    /** Component name for the SlicePurchaseBroadcastReceiver. */
     private static final ComponentName SLICE_PURCHASE_APP_COMPONENT_NAME =
             ComponentName.unflattenFromString(
                     "com.android.carrierdefaultapp/.SlicePurchaseBroadcastReceiver");
 
+    /** Shared preference name for network boost notification preferences. */
+    private static final String NETWORK_BOOST_NOTIFICATION_PREFERENCES =
+            "network_boost_notification_preferences";
+    /** Shared preference key for daily count of network boost notifications. */
+    private static final String KEY_DAILY_NOTIFICATION_COUNT = "daily_notification_count";
+    /** Shared preference key for monthly count of network boost notifications. */
+    private static final String KEY_MONTHLY_NOTIFICATION_COUNT = "monthly_notification_count";
+    /**
+     * Shared preference key for the date the daily or monthly counts of network boost notifications
+     * were last reset.
+     * A String with ISO-8601 format {@code YYYY-MM-DD}, from {@link LocalDate#toString}.
+     * For example, if the count was last updated on December 25, 2020, this would be `2020-12-25`.
+     */
+    private static final String KEY_NOTIFICATION_COUNT_LAST_RESET_DATE =
+            "notification_count_last_reset_date";
+
     /** Map of phone ID -> SlicePurchaseController instances. */
     @NonNull private static final Map<Integer, SlicePurchaseController> sInstances =
             new HashMap<>();
@@ -261,21 +291,37 @@
             mSlicePurchaseControllerBroadcastReceivers = new HashMap<>();
     /** The current network slicing configuration. */
     @Nullable private NetworkSlicingConfig mSlicingConfig;
-    /** Premium network entitlement query API */
+    /** Premium network entitlement query API. */
     @NonNull private final PremiumNetworkEntitlementApi mPremiumNetworkEntitlementApi;
+    /** LocalDate to use when resetting notification counts. {@code null} except when testing. */
+    @Nullable private LocalDate mLocalDate;
+    /** The number of times the network boost notification has been shown today. */
+    private int mDailyCount;
+    /** The number of times the network boost notification has been shown this month. */
+    private int mMonthlyCount;
 
     /**
      * BroadcastReceiver to receive responses from the slice purchase application.
      */
-    @VisibleForTesting
-    public class SlicePurchaseControllerBroadcastReceiver extends BroadcastReceiver {
+    private class SlicePurchaseControllerBroadcastReceiver extends BroadcastReceiver {
         @TelephonyManager.PremiumCapability private final int mCapability;
 
+        /**
+         * Create a SlicePurchaseControllerBroadcastReceiver for the given capability
+         *
+         * @param capability The requested capability to listen to response for.
+         */
         SlicePurchaseControllerBroadcastReceiver(
                 @TelephonyManager.PremiumCapability int capability) {
             mCapability = capability;
         }
 
+        /**
+         * Process responses from the slice purchase application.
+         *
+         * @param context The Context in which the receiver is running.
+         * @param intent The Intent being received.
+         */
         @Override
         public void onReceive(@NonNull Context context, @NonNull Intent intent) {
             String action = intent.getAction();
@@ -340,6 +386,10 @@
                             capability, duration);
                     break;
                 }
+                case ACTION_SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN: {
+                    SlicePurchaseController.getInstance(phoneId).onNotificationShown();
+                    break;
+                }
                 default:
                     reportAnomaly(UUID_UNKNOWN_ACTION, "SlicePurchaseControllerBroadcastReceiver("
                             + TelephonyManager.convertPremiumCapabilityToString(mCapability)
@@ -381,6 +431,7 @@
 
     /**
      * Create a SlicePurchaseController for the given phone on the given looper.
+     *
      * @param phone The Phone to create the SlicePurchaseController for.
      * @param looper The Looper to run the SlicePurchaseController on.
      */
@@ -390,8 +441,19 @@
         mPhone = phone;
         // TODO: Create a cached value for slicing config in DataIndication and initialize here
         mPhone.mCi.registerForSlicingConfigChanged(this, EVENT_SLICING_CONFIG_CHANGED, null);
-        mPremiumNetworkEntitlementApi = new PremiumNetworkEntitlementApi(mPhone,
-                getCarrierConfigs());
+        mPremiumNetworkEntitlementApi =
+                new PremiumNetworkEntitlementApi(mPhone, getCarrierConfigs());
+        updateNotificationCounts();
+    }
+
+    /**
+     * Set the LocalDate to use for resetting daily and monthly notification counts.
+     *
+     * @param localDate The LocalDate instance to use.
+     */
+    @VisibleForTesting
+    public void setLocalDate(@NonNull LocalDate localDate) {
+        mLocalDate = localDate;
     }
 
     @Override
@@ -412,12 +474,12 @@
                 onSlicingConfigChanged();
                 break;
             }
-            case EVENT_DISPLAY_BOOSTER_NOTIFICATION: {
+            case EVENT_START_SLICE_PURCHASE_APP: {
                 int capability = msg.arg1;
                 String appName = (String) msg.obj;
-                logd("EVENT_DISPLAY_BOOSTER_NOTIFICATION: " + appName + " requests capability "
+                logd("EVENT_START_SLICE_PURCHASE_APP: " + appName + " requests capability "
                         + TelephonyManager.convertPremiumCapabilityToString(capability));
-                onDisplayBoosterNotification(capability, appName);
+                onStartSlicePurchaseApplication(capability, appName);
                 break;
             }
             case EVENT_PURCHASE_TIMEOUT: {
@@ -531,10 +593,10 @@
             return;
         }
 
-        // 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.
+        // All state checks passed. Mark purchase pending and start the slice purchase application.
+        // Process through the handler since this method is synchronized.
         mPendingPurchaseCapabilities.put(capability, onComplete);
-        sendMessage(obtainMessage(EVENT_DISPLAY_BOOSTER_NOTIFICATION, capability, 0 /* unused */,
+        sendMessage(obtainMessage(EVENT_START_SLICE_PURCHASE_APP, capability, 0 /* unused */,
                 appName));
     }
 
@@ -594,7 +656,7 @@
         }
     }
 
-    private void onDisplayBoosterNotification(@TelephonyManager.PremiumCapability int capability,
+    private void onStartSlicePurchaseApplication(@TelephonyManager.PremiumCapability int capability,
             @NonNull String appName) {
         PremiumNetworkEntitlementResponse premiumNetworkEntitlementResponse =
                 mPremiumNetworkEntitlementApi.checkEntitlementStatus(capability);
@@ -627,6 +689,17 @@
             return;
         }
 
+        updateNotificationCounts();
+        if (mMonthlyCount >= getCarrierConfigs().getInt(
+                CarrierConfigManager.KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT)
+                || mDailyCount >= getCarrierConfigs().getInt(
+                CarrierConfigManager.KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT)) {
+            logd("Reached maximum number of network boost notifications.");
+            handlePurchaseResult(capability,
+                    TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED, false);
+            return;
+        }
+
         // Start timeout for purchase completion.
         long timeout = getCarrierConfigs().getLong(CarrierConfigManager
                 .KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG);
@@ -653,6 +726,8 @@
                 false));
         intent.putExtra(EXTRA_INTENT_SUCCESS, createPendingIntent(
                 ACTION_SLICE_PURCHASE_APP_RESPONSE_SUCCESS, capability, true));
+        intent.putExtra(EXTRA_INTENT_NOTIFICATION_SHOWN, createPendingIntent(
+                ACTION_SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN, capability, false));
         logd("Broadcasting start intent to SlicePurchaseBroadcastReceiver.");
         mPhone.getContext().sendBroadcast(intent);
 
@@ -665,6 +740,7 @@
         filter.addAction(ACTION_SLICE_PURCHASE_APP_RESPONSE_REQUEST_FAILED);
         filter.addAction(ACTION_SLICE_PURCHASE_APP_RESPONSE_NOT_DEFAULT_DATA_SUBSCRIPTION);
         filter.addAction(ACTION_SLICE_PURCHASE_APP_RESPONSE_SUCCESS);
+        filter.addAction(ACTION_SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN);
         mPhone.getContext().registerReceiver(
                 mSlicePurchaseControllerBroadcastReceivers.get(capability), filter);
     }
@@ -742,6 +818,73 @@
         }
     }
 
+    private void onNotificationShown() {
+        SharedPreferences sp =
+                mPhone.getContext().getSharedPreferences(NETWORK_BOOST_NOTIFICATION_PREFERENCES, 0);
+        mDailyCount = sp.getInt((KEY_DAILY_NOTIFICATION_COUNT + mPhone.getPhoneId()), 0) + 1;
+        mMonthlyCount = sp.getInt((KEY_MONTHLY_NOTIFICATION_COUNT + mPhone.getPhoneId()), 0) + 1;
+        logd("Network boost notification was shown " + mDailyCount + " times today and "
+                + mMonthlyCount + " times this month.");
+
+        SharedPreferences.Editor editor = sp.edit();
+        editor.putInt((KEY_DAILY_NOTIFICATION_COUNT + mPhone.getPhoneId()), mDailyCount);
+        editor.putInt((KEY_MONTHLY_NOTIFICATION_COUNT + mPhone.getPhoneId()), mMonthlyCount);
+        editor.apply();
+
+        // Don't call updateNotificationCounts here because it will be called whenever a new
+        // purchase request comes in or when SlicePurchaseController is initialized.
+    }
+
+    /**
+     * Update the current daily and monthly network boost notification counts.
+     * If it has been at least a day since the last daily reset or at least a month since the last
+     * monthly reset, reset the current daily or monthly notification counts.
+     */
+    @VisibleForTesting
+    public void updateNotificationCounts() {
+        SharedPreferences sp =
+                mPhone.getContext().getSharedPreferences(NETWORK_BOOST_NOTIFICATION_PREFERENCES, 0);
+        mDailyCount = sp.getInt((KEY_DAILY_NOTIFICATION_COUNT + mPhone.getPhoneId()), 0);
+        mMonthlyCount = sp.getInt((KEY_MONTHLY_NOTIFICATION_COUNT + mPhone.getPhoneId()), 0);
+
+        if (mLocalDate == null) {
+            // Standardize to UTC to prevent default time zone dependency
+            mLocalDate = LocalDate.now(ZoneId.of("UTC"));
+        }
+        LocalDate lastLocalDate = LocalDate.of(1, 1, 1);
+        String lastLocalDateString = sp.getString(
+                (KEY_NOTIFICATION_COUNT_LAST_RESET_DATE + mPhone.getPhoneId()), "");
+        if (!TextUtils.isEmpty(lastLocalDateString)) {
+            try {
+                lastLocalDate = LocalDate.parse(lastLocalDateString);
+            } catch (DateTimeParseException e) {
+                loge("Error parsing LocalDate from SharedPreferences: " + e);
+            }
+        }
+        logd("updateNotificationCounts: mDailyCount=" + mDailyCount + ", mMonthlyCount="
+                + mMonthlyCount + ", mLocalDate=" + mLocalDate + ", lastLocalDate="
+                + lastLocalDate);
+
+        boolean resetMonthly = lastLocalDate.getYear() != mLocalDate.getYear()
+                || lastLocalDate.getMonthValue() != mLocalDate.getMonthValue();
+        boolean resetDaily = resetMonthly
+                || lastLocalDate.getDayOfMonth() != mLocalDate.getDayOfMonth();
+        if (resetDaily) {
+            logd("Resetting daily" + (resetMonthly ? " and monthly" : "") + " notification count.");
+            SharedPreferences.Editor editor = sp.edit();
+            if (resetMonthly) {
+                mMonthlyCount = 0;
+                editor.putInt((KEY_MONTHLY_NOTIFICATION_COUNT + mPhone.getPhoneId()),
+                        mMonthlyCount);
+            }
+            mDailyCount = 0;
+            editor.putInt((KEY_DAILY_NOTIFICATION_COUNT + mPhone.getPhoneId()), mDailyCount);
+            editor.putString((KEY_NOTIFICATION_COUNT_LAST_RESET_DATE + mPhone.getPhoneId()),
+                    mLocalDate.toString());
+            editor.apply();
+        }
+    }
+
     @Nullable private PersistableBundle getCarrierConfigs() {
         return mPhone.getContext().getSystemService(CarrierConfigManager.class)
                 .getConfigForSubId(mPhone.getSubId());
diff --git a/tests/src/com/android/TestContext.java b/tests/src/com/android/TestContext.java
index 7c3a842..720d235 100644
--- a/tests/src/com/android/TestContext.java
+++ b/tests/src/com/android/TestContext.java
@@ -61,7 +61,11 @@
     @Mock ImsManager mMockImsManager;
     @Mock UserManager mMockUserManager;
 
-    private SparseArray<PersistableBundle> mCarrierConfigs = new SparseArray<>();
+    private final SparseArray<PersistableBundle> mCarrierConfigs = new SparseArray<>();
+
+    private Intent mIntent;
+
+    private BroadcastReceiver mReceiver;
 
     private final HashSet<String> mPermissionTable = new HashSet<>();
 
@@ -105,28 +109,42 @@
     }
 
     @Override
+    public void sendBroadcast(Intent intent) {
+        mIntent = intent;
+    }
+
+    @Override
     public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+        mReceiver = receiver;
         return null;
     }
 
     @Override
     public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {
+        mReceiver = receiver;
         return null;
     }
 
     @Override
     public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
             String broadcastPermission, Handler scheduler) {
+        mReceiver = receiver;
         return null;
     }
 
     @Override
     public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
             String broadcastPermission, Handler scheduler, int flags) {
+        mReceiver = receiver;
         return null;
     }
 
     @Override
+    public void unregisterReceiver(BroadcastReceiver receiver) {
+        mReceiver = null;
+    }
+
+    @Override
     public ContentResolver getContentResolver() {
         return null;
     }
@@ -134,22 +152,22 @@
     @Override
     public Object getSystemService(String name) {
         switch (name) {
-            case (Context.CARRIER_CONFIG_SERVICE) : {
+            case Context.CARRIER_CONFIG_SERVICE: {
                 return mMockCarrierConfigManager;
             }
-            case (Context.TELECOM_SERVICE) : {
+            case Context.TELECOM_SERVICE: {
                 return mMockTelecomManager;
             }
-            case (Context.TELEPHONY_SERVICE) : {
+            case Context.TELEPHONY_SERVICE: {
                 return mMockTelephonyManager;
             }
-            case (Context.TELEPHONY_SUBSCRIPTION_SERVICE) : {
+            case Context.TELEPHONY_SUBSCRIPTION_SERVICE: {
                 return mMockSubscriptionManager;
             }
-            case(Context.TELEPHONY_IMS_SERVICE) : {
+            case Context.TELEPHONY_IMS_SERVICE: {
                 return mMockImsManager;
             }
-            case(Context.USER_SERVICE) : {
+            case Context.USER_SERVICE: {
                 return mMockUserManager;
             }
         }
@@ -170,6 +188,9 @@
         if (serviceClass == SubscriptionManager.class) {
             return Context.TELEPHONY_SUBSCRIPTION_SERVICE;
         }
+        if (serviceClass == ImsManager.class) {
+            return Context.TELEPHONY_IMS_SERVICE;
+        }
         if (serviceClass == UserManager.class) {
             return Context.USER_SERVICE;
         }
@@ -252,6 +273,14 @@
         }
     }
 
+    public Intent getBroadcast() {
+        return mIntent;
+    }
+
+    public BroadcastReceiver getBroadcastReceiver() {
+        return mReceiver;
+    }
+
     private static void logd(String s) {
         Log.d(TAG, s);
     }
diff --git a/tests/src/com/android/phone/SlicePurchaseControllerTest.java b/tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java
similarity index 75%
rename from tests/src/com/android/phone/SlicePurchaseControllerTest.java
rename to tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java
index ebcf15d..e9e23f3 100644
--- a/tests/src/com/android/phone/SlicePurchaseControllerTest.java
+++ b/tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.phone;
+package com.android.phone.slice;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -26,15 +26,19 @@
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.SharedPreferences;
 import android.os.AsyncResult;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -53,38 +57,42 @@
 import com.android.TelephonyTestBase;
 import com.android.internal.telephony.CommandsInterface;
 import com.android.internal.telephony.Phone;
-import com.android.phone.slice.PremiumNetworkEntitlementApi;
-import com.android.phone.slice.PremiumNetworkEntitlementResponse;
-import com.android.phone.slice.SlicePurchaseController;
-import com.android.phone.slice.SlicePurchaseController.SlicePurchaseControllerBroadcastReceiver;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 
+import java.time.LocalDate;
 import java.util.Collections;
 import java.util.Map;
 
 @RunWith(AndroidJUnit4.class)
 public class SlicePurchaseControllerTest extends TelephonyTestBase {
     private static final String TAG = "SlicePurchaseControllerTest";
+    private static final String DAILY_NOTIFICATION_COUNT_KEY = "daily_notification_count0";
+    private static final String MONTHLY_NOTIFICATION_COUNT_KEY = "monthly_notification_count0";
+    private static final int YEAR = 2000;
+    private static final int MONTH = 6;
+    private static final int DATE = 1;
     private static final int PHONE_ID = 0;
+    private static final int DAILY_NOTIFICATION_MAX = 3;
+    private static final int MONTHLY_NOTIFICATION_MAX = 5;
     private static final long NOTIFICATION_TIMEOUT = 1000;
     private static final long PURCHASE_CONDITION_TIMEOUT = 2000;
     private static final long NETWORK_SETUP_TIMEOUT = 3000;
     private static final long THROTTLE_TIMEOUT = 4000;
 
     @Mock Phone mPhone;
-    @Mock Context mMockedContext;
     @Mock CarrierConfigManager mCarrierConfigManager;
     @Mock CommandsInterface mCommandsInterface;
     @Mock ServiceState mServiceState;
     @Mock PremiumNetworkEntitlementApi mPremiumNetworkEntitlementApi;
+    @Mock SharedPreferences mSharedPreferences;
+    @Mock SharedPreferences.Editor mEditor;
 
     private SlicePurchaseController mSlicePurchaseController;
-    private SlicePurchaseControllerBroadcastReceiver mBroadcastReceiver;
     private PersistableBundle mBundle;
     private PremiumNetworkEntitlementResponse mEntitlementResponse;
     private Handler mHandler;
@@ -106,17 +114,34 @@
         mTestableLooper = new TestableLooper(mHandler.getLooper());
 
         doReturn(PHONE_ID).when(mPhone).getPhoneId();
-        doReturn(mMockedContext).when(mPhone).getContext();
+        doReturn(mContext).when(mPhone).getContext();
         doReturn(mServiceState).when(mPhone).getServiceState();
         mPhone.mCi = mCommandsInterface;
 
-        doReturn(Context.CARRIER_CONFIG_SERVICE).when(mMockedContext)
-                .getSystemServiceName(eq(CarrierConfigManager.class));
-        doReturn(mCarrierConfigManager).when(mMockedContext)
-                .getSystemService(eq(Context.CARRIER_CONFIG_SERVICE));
+        doReturn(mCarrierConfigManager).when(mContext)
+                .getSystemService(Context.CARRIER_CONFIG_SERVICE);
         mBundle = new PersistableBundle();
+        mBundle.putInt(
+                CarrierConfigManager.KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT,
+                DAILY_NOTIFICATION_MAX);
+        mBundle.putInt(
+                CarrierConfigManager.KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT,
+                MONTHLY_NOTIFICATION_MAX);
         doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
 
+        doReturn(mSharedPreferences).when(mContext).getSharedPreferences(anyString(), anyInt());
+        doReturn(mEditor).when(mSharedPreferences).edit();
+        doAnswer(invocation -> {
+            doReturn(invocation.getArgument(1)).when(mSharedPreferences)
+                    .getInt(eq(invocation.getArgument(0)), anyInt());
+            return null;
+        }).when(mEditor).putInt(anyString(), anyInt());
+        doAnswer(invocation -> {
+            doReturn(invocation.getArgument(1)).when(mSharedPreferences)
+                    .getString(eq(invocation.getArgument(0)), anyString());
+            return null;
+        }).when(mEditor).putString(anyString(), anyString());
+
         // create a spy to mock final PendingIntent methods
         SlicePurchaseController slicePurchaseController =
                 new SlicePurchaseController(mPhone, mHandler.getLooper());
@@ -159,7 +184,6 @@
                 new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY});
         mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING,
                 SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
         doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mPhone).getSubId();
 
         // retry to verify available
@@ -188,19 +212,65 @@
         };
         for (String url : invalidUrls) {
             mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING, url);
-            doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
             assertFalse(mSlicePurchaseController.isPremiumCapabilityAvailableForPurchase(
                     TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY));
         }
 
         mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING,
                 SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
         assertTrue(mSlicePurchaseController.isPremiumCapabilityAvailableForPurchase(
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY));
     }
 
     @Test
+    public void testUpdateNotificationCounts() {
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR, MONTH, DATE));
+        mSlicePurchaseController.updateNotificationCounts();
+
+        // change only date, month and year remain the same
+        Mockito.clearInvocations(mEditor);
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR, MONTH, DATE + 1));
+        mSlicePurchaseController.updateNotificationCounts();
+        verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(0));
+        verify(mEditor, never()).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(0));
+
+        // change only month, date and year remain the same
+        Mockito.clearInvocations(mEditor);
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR, MONTH + 1, DATE + 1));
+        mSlicePurchaseController.updateNotificationCounts();
+        verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(0));
+        verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(0));
+
+        // change only year, date and month remain the same
+        Mockito.clearInvocations(mEditor);
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR + 1, MONTH + 1, DATE + 1));
+        mSlicePurchaseController.updateNotificationCounts();
+        verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(0));
+        verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(0));
+
+        // change only month and year, date remains the same
+        Mockito.clearInvocations(mEditor);
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR + 2, MONTH + 2, DATE + 1));
+        mSlicePurchaseController.updateNotificationCounts();
+        verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(0));
+        verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(0));
+
+        // change only date and year, month remains the same
+        Mockito.clearInvocations(mEditor);
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR + 3, MONTH + 2, DATE + 2));
+        mSlicePurchaseController.updateNotificationCounts();
+        verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(0));
+        verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(0));
+
+        // change only date and month, year remains the same
+        Mockito.clearInvocations(mEditor);
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR + 3, MONTH + 3, DATE + 3));
+        mSlicePurchaseController.updateNotificationCounts();
+        verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(0));
+        verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(0));
+    }
+
+    @Test
     public void testPurchasePremiumCapabilityResultFeatureNotSupported() {
         mSlicePurchaseController.purchasePremiumCapability(
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
@@ -237,7 +307,6 @@
                 new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY});
         mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING,
                 SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
 
         mSlicePurchaseController.purchasePremiumCapability(
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
@@ -255,7 +324,6 @@
                 new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY});
         mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING,
                 SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
 
         mSlicePurchaseController.purchasePremiumCapability(
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
@@ -285,7 +353,6 @@
                 new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY});
         mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING,
                 SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
         doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mPhone).getSubId();
 
         mSlicePurchaseController.purchasePremiumCapability(
@@ -314,7 +381,6 @@
                 new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY});
         mBundle.putString(CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING,
                 SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
         doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mPhone).getSubId();
         doReturn(TelephonyManager.NETWORK_TYPE_NR).when(mServiceState).getDataNetworkType();
         doReturn(null).when(mPremiumNetworkEntitlementApi).checkEntitlementStatus(anyInt());
@@ -342,8 +408,6 @@
         // retry with provisioning response
         mEntitlementResponse.mProvisionStatus =
                 PremiumNetworkEntitlementResponse.PREMIUM_NETWORK_PROVISION_STATUS_IN_PROGRESS;
-        doReturn(mEntitlementResponse).when(mPremiumNetworkEntitlementApi)
-                .checkEntitlementStatus(anyInt());
 
         mSlicePurchaseController.purchasePremiumCapability(
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
@@ -357,12 +421,9 @@
                 PremiumNetworkEntitlementResponse.PREMIUM_NETWORK_PROVISION_STATUS_NOT_PROVISIONED;
         mEntitlementResponse.mEntitlementStatus =
                 PremiumNetworkEntitlementResponse.PREMIUM_NETWORK_ENTITLEMENT_STATUS_INCOMPATIBLE;
-        doReturn(mEntitlementResponse).when(mPremiumNetworkEntitlementApi)
-                .checkEntitlementStatus(anyInt());
         mBundle.putLong(CarrierConfigManager
                 .KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG,
                 PURCHASE_CONDITION_TIMEOUT);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
 
         mSlicePurchaseController.purchasePremiumCapability(
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
@@ -409,12 +470,13 @@
     public void testPurchasePremiumCapabilityResultSuccess() {
         sendValidPurchaseRequest();
 
+        // broadcast SUCCESS response from slice purchase application
         Intent intent = new Intent();
         intent.setAction("com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_SUCCESS");
         intent.putExtra(SlicePurchaseController.EXTRA_PHONE_ID, PHONE_ID);
         intent.putExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
-        mBroadcastReceiver.onReceive(mMockedContext, intent);
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
         mTestableLooper.processAllMessages();
         assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS, mResult);
 
@@ -443,13 +505,7 @@
     public void testPurchasePremiumCapabilityResultAlreadyPurchased() {
         testPurchasePremiumCapabilityResultSuccess();
 
-        // TODO: implement slicing config logic properly
-        NetworkSlicingConfig slicingConfig = new NetworkSlicingConfig(Collections.emptyList(),
-                Collections.singletonList(new NetworkSliceInfo.Builder()
-                        .setStatus(NetworkSliceInfo.SLICE_STATUS_ALLOWED).build()));
-        mSlicePurchaseController.obtainMessage(2 /* EVENT_SLICING_CONFIG_CHANGED */,
-                new AsyncResult(null, slicingConfig, null)).sendToTarget();
-        mTestableLooper.processAllMessages();
+        sendNetworkSlicingConfig(true);
 
         mSlicePurchaseController.purchasePremiumCapability(
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
@@ -467,10 +523,7 @@
                 mResult);
 
         // retry to verify purchase expired
-        slicingConfig = new NetworkSlicingConfig(Collections.emptyList(), Collections.emptyList());
-        mSlicePurchaseController.obtainMessage(2 /* EVENT_SLICING_CONFIG_CHANGED */,
-                new AsyncResult(null, slicingConfig, null)).sendToTarget();
-        mTestableLooper.processAllMessages();
+        sendNetworkSlicingConfig(false);
 
         testPurchasePremiumCapabilityResultSuccess();
     }
@@ -501,12 +554,13 @@
     public void testPurchasePremiumCapabilityResultUserCanceled() {
         sendValidPurchaseRequest();
 
+        // broadcast CANCELED response from slice purchase application
         Intent intent = new Intent();
         intent.setAction("com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_CANCELED");
         intent.putExtra(SlicePurchaseController.EXTRA_PHONE_ID, PHONE_ID);
         intent.putExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
-        mBroadcastReceiver.onReceive(mMockedContext, intent);
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
         mTestableLooper.processAllMessages();
         assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED, mResult);
 
@@ -528,6 +582,7 @@
     public void testPurchasePremiumCapabilityResultCarrierError() {
         sendValidPurchaseRequest();
 
+        // broadcast CARRIER_ERROR response from slice purchase application
         Intent intent = new Intent();
         intent.setAction(
                 "com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_CARRIER_ERROR");
@@ -536,7 +591,7 @@
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
         intent.putExtra(SlicePurchaseController.EXTRA_FAILURE_CODE,
                 SlicePurchaseController.FAILURE_CODE_SERVER_UNREACHABLE);
-        mBroadcastReceiver.onReceive(mMockedContext, intent);
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
         mTestableLooper.processAllMessages();
         assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR, mResult);
 
@@ -558,13 +613,14 @@
     public void testPurchasePremiumCapabilityResultRequestFailed() {
         sendValidPurchaseRequest();
 
+        // broadcast REQUEST_FAILED response from slice purchase application
         Intent intent = new Intent();
         intent.setAction(
                 "com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_REQUEST_FAILED");
         intent.putExtra(SlicePurchaseController.EXTRA_PHONE_ID, PHONE_ID);
         intent.putExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
-        mBroadcastReceiver.onReceive(mMockedContext, intent);
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
         mTestableLooper.processAllMessages();
         assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED, mResult);
 
@@ -582,7 +638,7 @@
         intent.putExtra(SlicePurchaseController.EXTRA_PHONE_ID, PHONE_ID);
         intent.putExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
-        mBroadcastReceiver.onReceive(mMockedContext, intent);
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
         mTestableLooper.processAllMessages();
         assertEquals(
                 TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION,
@@ -592,8 +648,70 @@
         testPurchasePremiumCapabilityResultSuccess();
     }
 
+    @Test
+    public void testPurchasePremiumCapabilityResultNotificationThrottled() {
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR, MONTH, DATE));
+        mSlicePurchaseController.updateNotificationCounts();
+
+        for (int count = 1; count <= DAILY_NOTIFICATION_MAX; count++) {
+            completeSuccessfulPurchase();
+            verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(count));
+            verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY), eq(count));
+        }
+
+        // retry to verify throttled
+        mSlicePurchaseController.purchasePremiumCapability(
+                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
+                mHandler.obtainMessage());
+        mTestableLooper.processAllMessages();
+        assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED, mResult);
+
+        // change the date to trigger daily reset
+        mSlicePurchaseController.setLocalDate(LocalDate.of(YEAR, MONTH, DATE + 1));
+        Mockito.clearInvocations(mEditor);
+
+        for (int count = 1; count <= (MONTHLY_NOTIFICATION_MAX - DAILY_NOTIFICATION_MAX); count++) {
+            completeSuccessfulPurchase();
+            verify(mEditor).putInt(eq(DAILY_NOTIFICATION_COUNT_KEY), eq(count));
+            verify(mEditor).putInt(eq(MONTHLY_NOTIFICATION_COUNT_KEY),
+                    eq(count + DAILY_NOTIFICATION_MAX));
+        }
+
+        // retry to verify throttled
+        mSlicePurchaseController.purchasePremiumCapability(
+                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, TAG,
+                mHandler.obtainMessage());
+        mTestableLooper.processAllMessages();
+        assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED, mResult);
+    }
+
+    private void completeSuccessfulPurchase() {
+        sendValidPurchaseRequest();
+
+        // broadcast NOTIFICATION_SHOWN response from slice purchase application
+        Intent intent = new Intent();
+        intent.setAction(
+                "com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_NOTIFICATION_SHOWN");
+        intent.putExtra(SlicePurchaseController.EXTRA_PHONE_ID, PHONE_ID);
+        intent.putExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
+                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
+        mTestableLooper.processAllMessages();
+
+        // broadcast SUCCESS response from slice purchase application
+        intent.setAction("com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_SUCCESS");
+        mContext.getBroadcastReceiver().onReceive(mContext, intent);
+        mTestableLooper.processAllMessages();
+        assertEquals(TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS, mResult);
+
+        // complete network setup
+        sendNetworkSlicingConfig(true);
+        // purchase expired
+        sendNetworkSlicingConfig(false);
+    }
+
     private void sendValidPurchaseRequest() {
-        clearInvocations(mMockedContext);
+        clearInvocations(mContext);
 
         // feature supported
         doReturn((int) TelephonyManager.NETWORK_TYPE_BITMASK_NR).when(mPhone)
@@ -614,7 +732,6 @@
         mBundle.putLong(CarrierConfigManager
                 .KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG,
                 PURCHASE_CONDITION_TIMEOUT);
-        doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
         // default data subscription
         doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mPhone).getSubId();
         // network available
@@ -622,8 +739,6 @@
         // entitlement check passed
         mEntitlementResponse.mEntitlementStatus =
                 PremiumNetworkEntitlementResponse.PREMIUM_NETWORK_ENTITLEMENT_STATUS_ENABLED;
-        doReturn(mEntitlementResponse).when(mPremiumNetworkEntitlementApi)
-                .checkEntitlementStatus(anyInt());
 
         // send purchase request
         mSlicePurchaseController.purchasePremiumCapability(
@@ -632,18 +747,23 @@
         mTestableLooper.processAllMessages();
 
         // verify that the purchase request was sent successfully
-        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mMockedContext).sendBroadcast(intentCaptor.capture());
-        Intent intent = intentCaptor.getValue();
-        assertEquals(SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP, intent.getAction());
+        verify(mContext).sendBroadcast(any(Intent.class));
+        assertEquals(SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP,
+                mContext.getBroadcast().getAction());
         assertTrue(mSlicePurchaseController.hasMessages(4 /* EVENT_PURCHASE_TIMEOUT */,
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY));
+        verify(mContext).registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class));
+    }
 
-        // capture the broadcast receiver to fake responses from the slice purchase application
-        ArgumentCaptor<SlicePurchaseControllerBroadcastReceiver> broadcastReceiverCaptor =
-                ArgumentCaptor.forClass(SlicePurchaseControllerBroadcastReceiver.class);
-        verify(mMockedContext).registerReceiver(
-                broadcastReceiverCaptor.capture(), any(IntentFilter.class));
-        mBroadcastReceiver = broadcastReceiverCaptor.getValue();
+    private void sendNetworkSlicingConfig(boolean configExists) {
+        // TODO: implement slicing config logic properly
+        NetworkSlicingConfig slicingConfig = new NetworkSlicingConfig(Collections.emptyList(),
+                configExists
+                        ? Collections.singletonList(new NetworkSliceInfo.Builder()
+                                .setStatus(NetworkSliceInfo.SLICE_STATUS_ALLOWED).build())
+                        : Collections.emptyList());
+        mSlicePurchaseController.obtainMessage(2 /* EVENT_SLICING_CONFIG_CHANGED */,
+                new AsyncResult(null, slicingConfig, null)).sendToTarget();
+        mTestableLooper.processAllMessages();
     }
 }