Create basic implementation for SliceStore
Create SliceStore class that checks whether premium capabilities are
available and purchasable.
Test: TelephonyManagerTest#testPurchasePremiumCapability
Bug: 244368757
Change-Id: I73cd38a5ad257b34034dbfd8b7b83bde46e354f6
diff --git a/src/com/android/phone/slicestore/SliceStore.java b/src/com/android/phone/slicestore/SliceStore.java
new file mode 100644
index 0000000..aa564c8
--- /dev/null
+++ b/src/com/android/phone/slicestore/SliceStore.java
@@ -0,0 +1,378 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.ConnectivityManager;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.telephony.AnomalyReporter;
+import android.telephony.CarrierConfigManager;
+import android.telephony.TelephonyManager;
+import android.telephony.data.NetworkSliceInfo;
+import android.telephony.data.NetworkSlicingConfig;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.telephony.Phone;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * The SliceStore controls the purchase and availability of all cellular premium capabilities.
+ * Applications can check whether premium capabilities are available by calling
+ * {@link TelephonyManager#isPremiumCapabilityAvailableForPurchase(int)}. If this returns true,
+ * 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}.
+ */
+public class SliceStore extends Handler {
+ @NonNull private static final String TAG = "SliceStore";
+ /** Purchasing the premium capability is no longer throttled. */
+ 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;
+ /** Boost was not purchased within the timeout specified by carrier configs. */
+ private static final int EVENT_PURCHASE_TIMEOUT = 4;
+
+ /** 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";
+
+ /** Map of phone ID -> SliceStore. */
+ @NonNull private static final Map<Integer, SliceStore> sInstances = new HashMap<>();
+
+ @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();
+ @Nullable private NetworkSlicingConfig mSlicingConfig;
+
+ /**
+ * Get the static SliceStore instance for the given phone.
+ *
+ * @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)
+ // that dismiss notifications and update SliceStore instance
+ int phoneId = phone.getPhoneId();
+ if (sInstances.get(phoneId) == null) {
+ sInstances.put(phoneId, new SliceStore(phone));
+ }
+ return sInstances.get(phoneId);
+ }
+
+ private SliceStore(@NonNull Phone phone) {
+ super(Looper.myLooper());
+ mPhone = phone;
+ // TODO: Create a cached value for slicing config in DataIndication and initialize here
+ mPhone.mCi.registerForSlicingConfigChanged(this, EVENT_SLICING_CONFIG_CHANGED, null);
+ }
+
+ @Override
+ public void handleMessage(@NonNull Message msg) {
+ switch (msg.what) {
+ case EVENT_PURCHASE_UNTHROTTLED: {
+ int capability = (int) msg.obj;
+ log("EVENT_PURCHASE_UNTHROTTLED: for capability "
+ + TelephonyManager.convertPremiumCapabilityToString(capability));
+ mThrottledCapabilities.setValueAt(capability, false);
+ break;
+ }
+ case EVENT_SLICING_CONFIG_CHANGED: {
+ AsyncResult ar = (AsyncResult) msg.obj;
+ NetworkSlicingConfig config = (NetworkSlicingConfig) ar.result;
+ log("EVENT_SLICING_CONFIG_CHANGED: from " + mSlicingConfig + " to " + config);
+ mSlicingConfig = config;
+ break;
+ }
+ case EVENT_DISPLAY_BOOSTER_NOTIFICATION: {
+ onDisplayBoosterNotification(msg.arg1, (Message) msg.obj);
+ break;
+ }
+ case EVENT_PURCHASE_TIMEOUT: {
+ int capability = msg.arg1;
+ log("EVENT_PURCHASE_TIMEOUT: for capability "
+ + TelephonyManager.convertPremiumCapabilityToString(capability));
+ onTimeout(capability, (Message) msg.obj);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Check whether the given premium capability is available for purchase from the carrier.
+ *
+ * @param capability The premium capability to check.
+ * @return Whether the given premium capability is available to purchase.
+ */
+ public boolean isPremiumCapabilityAvailableForPurchase(
+ @TelephonyManager.PremiumCapability int capability) {
+ if (!arePremiumCapabilitiesSupportedByDevice()) {
+ log("Premium capabilities unsupported by the device.");
+ return false;
+ }
+ if (!isPremiumCapabilitySupportedByCarrier(capability)) {
+ log("Premium capability "
+ + TelephonyManager.convertPremiumCapabilityToString(capability)
+ + " unsupported by the carrier.");
+ return false;
+ }
+ if (!arePremiumCapabilitiesEnabledByUser()) {
+ log("Premium capabilities disabled by the user.");
+ return false;
+ }
+ log("Premium capability "
+ + TelephonyManager.convertPremiumCapabilityToString(capability)
+ + " is available for purchase.");
+ return true;
+ }
+
+ /**
+ * Purchase the given premium capability from the carrier.
+ *
+ * @param capability The premium capability to purchase.
+ * @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.convertPremiumCapabilityToString(capability));
+ // Check whether the premium capability can be purchased.
+ if (!arePremiumCapabilitiesSupportedByDevice()) {
+ sendPurchaseResult(capability,
+ TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED,
+ onComplete);
+ return;
+ }
+ if (!isPremiumCapabilitySupportedByCarrier(capability)) {
+ sendPurchaseResult(capability,
+ TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED,
+ onComplete);
+ return;
+ }
+ if (!arePremiumCapabilitiesEnabledByUser()) {
+ sendPurchaseResult(capability,
+ TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED,
+ onComplete);
+ return;
+ }
+ if (mPurchasedCapabilities.get(capability) || isSlicingConfigActive(capability)) {
+ sendPurchaseResult(capability,
+ TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED,
+ onComplete);
+ return;
+ }
+ if (mThrottledCapabilities.get(capability)) {
+ sendPurchaseResult(capability,
+ TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED,
+ onComplete);
+ return;
+ }
+ if (mPhone.getServiceState().getDataNetworkType() != TelephonyManager.NETWORK_TYPE_NR) {
+ sendPurchaseResult(capability,
+ TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE,
+ onComplete);
+ return;
+ }
+ if (isNetworkCongested(capability)) {
+ throttleCapability(capability);
+ sendPurchaseResult(capability,
+ TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED,
+ onComplete);
+ return;
+ }
+ if (mPendingPurchaseCapabilities.get(capability)) {
+ sendPurchaseResult(capability,
+ TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS,
+ onComplete);
+ 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.
+ mPendingPurchaseCapabilities.put(capability, true);
+ sendMessage(obtainMessage(EVENT_DISPLAY_BOOSTER_NOTIFICATION,
+ capability, 0 /* unused */, onComplete));
+ }
+
+ private void sendPurchaseResult(@TelephonyManager.PremiumCapability int capability,
+ @TelephonyManager.PurchasePremiumCapabilityResult int result,
+ @NonNull Message onComplete) {
+ // Send the onComplete message with the purchase result.
+ log("Purchase result for capability "
+ + TelephonyManager.convertPremiumCapabilityToString(capability)
+ + ": " + TelephonyManager.convertPurchaseResultToString(result));
+ AsyncResult.forMessage(onComplete, result, null);
+ onComplete.sendToTarget();
+ }
+
+ private void throttleCapability(@TelephonyManager.PremiumCapability int capability) {
+ // Throttle subsequent requests if necessary.
+ if (!mThrottledCapabilities.get(capability)) {
+ long throttleTime = getThrottleDuration(capability);
+ if (throttleTime > 0) {
+ log("Throttle purchase requests for capability "
+ + TelephonyManager.convertPremiumCapabilityToString(capability) + " for "
+ + (throttleTime / 1000) + " seconds.");
+ mThrottledCapabilities.setValueAt(capability, true);
+ sendMessageDelayed(obtainMessage(EVENT_PURCHASE_UNTHROTTLED, capability),
+ throttleTime);
+ }
+ } else {
+ String logStr = TelephonyManager.convertPremiumCapabilityToString(capability)
+ + " is already throttled.";
+ log(logStr);
+ AnomalyReporter.reportAnomaly(UUID.fromString(UUID_CAPABILITY_THROTTLED_TWICE), logStr);
+ }
+ }
+
+ private void onDisplayBoosterNotification(@TelephonyManager.PremiumCapability int capability,
+ @NonNull Message onComplete) {
+ long timeout = getCarrierConfigs().getLong(CarrierConfigManager
+ .KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG);
+ 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
+ }
+
+ private void closeBoosterNotification(@TelephonyManager.PremiumCapability int capability) {
+ // TODO(b/245882092): Close notification; maybe cancel purchase timeout
+ }
+
+ 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);
+ }
+
+ private void onUserCanceled(@TelephonyManager.PremiumCapability int capability) {
+ // TODO(b/245882092): Process and return user canceled; throttle
+ }
+
+ private void onUserConfirmed(@TelephonyManager.PremiumCapability int capability) {
+ // TODO(b/245882092, b/245882601): Open webview listening for carrier response
+ // --> EVENT_CARRIER_SUCCESS or EVENT_CARRIER_ERROR
+ }
+
+ private void onCarrierSuccess(@TelephonyManager.PremiumCapability int capability) {
+ // TODO(b/245882601): Process and return success.
+ // Probably need to handle capability expiry as well
+ }
+
+ private void onCarrierError(@TelephonyManager.PremiumCapability int capability) {
+ // TODO(b/245882601): Process and return carrier error; throttle
+ }
+
+ @Nullable private PersistableBundle getCarrierConfigs() {
+ return mPhone.getContext().getSystemService(CarrierConfigManager.class)
+ .getConfigForSubId(mPhone.getSubId());
+ }
+
+ private long getThrottleDuration(@TelephonyManager.PurchasePremiumCapabilityResult int result) {
+ if (result == TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED
+ || result == TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT) {
+ return getCarrierConfigs().getLong(CarrierConfigManager
+ .KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG);
+ }
+ if (result == TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED
+ || result == TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR) {
+ return getCarrierConfigs().getLong(CarrierConfigManager
+ .KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG);
+ }
+ return 0;
+ }
+
+ private boolean isPremiumCapabilitySupportedByCarrier(
+ @TelephonyManager.PremiumCapability int capability) {
+ int[] supportedCapabilities = getCarrierConfigs().getIntArray(
+ CarrierConfigManager.KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY);
+ if (supportedCapabilities == null) {
+ return false;
+ }
+ return Arrays.stream(supportedCapabilities)
+ .anyMatch(supportedCapability -> supportedCapability == capability);
+ }
+
+ private boolean arePremiumCapabilitiesSupportedByDevice() {
+ // TODO: Add more checks?
+ // Maybe device resource overlay to enable/disable in addition to carrier configs
+ return (mPhone.getCachedAllowedNetworkTypesBitmask()
+ & TelephonyManager.NETWORK_TYPE_BITMASK_NR) != 0;
+ }
+
+ private boolean arePremiumCapabilitiesEnabledByUser() {
+ // TODO(b/245882396): Create and set user settings
+ return false;
+ }
+
+ private boolean isSlicingConfigActive(@TelephonyManager.PremiumCapability int capability) {
+ if (mSlicingConfig == null) {
+ return false;
+ }
+ int capabilityServiceType = getSliceServiceType(capability);
+ for (NetworkSliceInfo sliceInfo : mSlicingConfig.getSliceInfo()) {
+ // TODO: check if TrafficDescriptor has realtime capability slice
+ if (sliceInfo.getSliceServiceType() == capabilityServiceType
+ && sliceInfo.getStatus() == NetworkSliceInfo.SLICE_STATUS_ALLOWED) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private @NetworkSliceInfo.SliceServiceType int getSliceServiceType(
+ @TelephonyManager.PremiumCapability int capability) {
+ // TODO: Implement properly -- potentially need to add new slice service types?
+ return NetworkSliceInfo.SLICE_SERVICE_TYPE_NONE;
+ }
+
+ private boolean isNetworkCongested(@TelephonyManager.PremiumCapability int capability) {
+ // TODO: Implement TS43
+ return true;
+ }
+
+ private void log(String s) {
+ Log.d(TAG + "-" + mPhone.getPhoneId(), s);
+ }
+}