OSDN Git Service

Iterate NetworkPolicy towards SubscriptionPlan.
authorJeff Sharkey <jsharkey@android.com>
Thu, 13 Jul 2017 22:47:32 +0000 (16:47 -0600)
committerJeff Sharkey <jsharkey@android.com>
Fri, 14 Jul 2017 22:18:40 +0000 (16:18 -0600)
Add new SubscriptionPlan API to describe the various types of carrier
data plans.  Internally the OS will only use the first plan for
driving policy, but it will blindly plumb through the details for
Settings to display any secondary plans.

As part of getting Settings ready to roll towards SubscriptionPlan,
reduce references to NetworkPolicy internal fields.  All usage cycle
details are now obtained from an Iterator which converts to
SubscriptionPlan under the hood.

Replace all data usage cycle calculation with new SubscriptionPlan
implementation, and retrofit large suite of existing tests to
exercise and verify the new logic.

Offer a debugging property that can be used to return "fake" plan
examples for testing.

Bug: 63391323
Test: bit FrameworksServicesTests:com.android.server.NetworkPolicyManagerServiceTest
Exempt-From-Owner-Approval: Bug 63673347
Change-Id: I889c653980eeb7887abdfa4f5b6986f35855ee6d

12 files changed:
api/system-current.txt
core/java/android/net/INetworkPolicyManager.aidl
core/java/android/net/NetworkPolicy.java
core/java/android/net/NetworkPolicyManager.java
core/res/AndroidManifest.xml
packages/SettingsLib/src/com/android/settingslib/NetworkPolicyEditor.java
packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java
services/core/java/com/android/server/net/NetworkPolicyManagerService.java
services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java
telephony/java/android/telephony/SubscriptionManager.java
telephony/java/android/telephony/SubscriptionPlan.aidl [new file with mode: 0755]
telephony/java/android/telephony/SubscriptionPlan.java [new file with mode: 0644]

index 268d8ec..a3610f8 100644 (file)
@@ -146,6 +146,7 @@ package android {
     field public static final java.lang.String MANAGE_CA_CERTIFICATES = "android.permission.MANAGE_CA_CERTIFICATES";
     field public static final java.lang.String MANAGE_DEVICE_ADMINS = "android.permission.MANAGE_DEVICE_ADMINS";
     field public static final java.lang.String MANAGE_DOCUMENTS = "android.permission.MANAGE_DOCUMENTS";
+    field public static final java.lang.String MANAGE_FALLBACK_SUBSCRIPTION_PLANS = "android.permission.MANAGE_FALLBACK_SUBSCRIPTION_PLANS";
     field public static final java.lang.String MANAGE_OWN_CALLS = "android.permission.MANAGE_OWN_CALLS";
     field public static final java.lang.String MANAGE_USB = "android.permission.MANAGE_USB";
     field public static final java.lang.String MANAGE_USERS = "android.permission.MANAGE_USERS";
index 7b1e61e..7b948a7 100644 (file)
@@ -21,6 +21,7 @@ import android.net.NetworkPolicy;
 import android.net.NetworkQuotaInfo;
 import android.net.NetworkState;
 import android.net.NetworkTemplate;
+import android.telephony.SubscriptionPlan;
 
 /**
  * Interface that creates and modifies network policy rules.
@@ -67,5 +68,10 @@ interface INetworkPolicyManager {
 
     NetworkQuotaInfo getNetworkQuotaInfo(in NetworkState state);
 
+    SubscriptionPlan[] getSubscriptionPlans(int subId, String callingPackage);
+    void setSubscriptionPlans(int subId, in SubscriptionPlan[] plans, String callingPackage);
+
+    String getSubscriptionPlanOwner(int subId);
+
     void factoryReset(String subscriber);
 }
index 5830667..edf9a28 100644 (file)
@@ -46,17 +46,20 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
     public static final long SNOOZE_NEVER = -1;
 
     public NetworkTemplate template;
-    public int cycleDay;
-    public String cycleTimezone;
-    public long warningBytes;
-    public long limitBytes;
-    public long lastWarningSnooze;
-    public long lastLimitSnooze;
-    @Deprecated public boolean metered;
-    public boolean inferred;
+    @Deprecated public int cycleDay = CYCLE_NONE;
+    @Deprecated public String cycleTimezone = "UTC";
+    public long warningBytes = WARNING_DISABLED;
+    public long limitBytes = LIMIT_DISABLED;
+    public long lastWarningSnooze = SNOOZE_NEVER;
+    public long lastLimitSnooze = SNOOZE_NEVER;
+    @Deprecated public boolean metered = true;
+    public boolean inferred = false;
 
     private static final long DEFAULT_MTU = 1500;
 
+    public NetworkPolicy() {
+    }
+
     @Deprecated
     public NetworkPolicy(NetworkTemplate template, int cycleDay, String cycleTimezone,
             long warningBytes, long limitBytes, boolean metered) {
index fc96004..3fe9b0d 100644 (file)
@@ -17,7 +17,6 @@
 package android.net;
 
 import static android.content.pm.PackageManager.GET_SIGNATURES;
-import static android.net.NetworkPolicy.CYCLE_NONE;
 
 import android.annotation.SystemService;
 import android.app.ActivityManager;
@@ -30,13 +29,15 @@ import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiInfo;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.telephony.SubscriptionPlan;
 import android.util.DebugUtils;
+import android.util.Pair;
 
 import com.google.android.collect.Sets;
 
-import java.util.Calendar;
+import java.time.ZonedDateTime;
 import java.util.HashSet;
-import java.util.TimeZone;
+import java.util.Iterator;
 
 /**
  * Manager for creating and modifying network policy rules.
@@ -251,73 +252,9 @@ public class NetworkPolicyManager {
         }
     }
 
-    /**
-     * Compute the last cycle boundary for the given {@link NetworkPolicy}. For
-     * example, if cycle day is 20th, and today is June 15th, it will return May
-     * 20th. When cycle day doesn't exist in current month, it snaps to the 1st
-     * of following month.
-     *
-     * @hide
-     */
-    public static long computeLastCycleBoundary(long currentTime, NetworkPolicy policy) {
-        if (policy.cycleDay == CYCLE_NONE) {
-            throw new IllegalArgumentException("Unable to compute boundary without cycleDay");
-        }
-
-        final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(policy.cycleTimezone));
-        cal.setTimeInMillis(currentTime);
-        snapToCycleDay(cal, policy.cycleDay);
-
-        if (cal.getTimeInMillis() >= currentTime) {
-            // Cycle boundary is beyond now, use last cycle boundary
-            cal.set(Calendar.DAY_OF_MONTH, 1);
-            cal.add(Calendar.MONTH, -1);
-            snapToCycleDay(cal, policy.cycleDay);
-        }
-
-        return cal.getTimeInMillis();
-    }
-
     /** {@hide} */
-    public static long computeNextCycleBoundary(long currentTime, NetworkPolicy policy) {
-        if (policy.cycleDay == CYCLE_NONE) {
-            throw new IllegalArgumentException("Unable to compute boundary without cycleDay");
-        }
-
-        final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(policy.cycleTimezone));
-        cal.setTimeInMillis(currentTime);
-        snapToCycleDay(cal, policy.cycleDay);
-
-        if (cal.getTimeInMillis() <= currentTime) {
-            // Cycle boundary is before now, use next cycle boundary
-            cal.set(Calendar.DAY_OF_MONTH, 1);
-            cal.add(Calendar.MONTH, 1);
-            snapToCycleDay(cal, policy.cycleDay);
-        }
-
-        return cal.getTimeInMillis();
-    }
-
-    /**
-     * Snap to the cycle day for the current month given; when cycle day doesn't
-     * exist, it snaps to last second of current month.
-     *
-     * @hide
-     */
-    public static void snapToCycleDay(Calendar cal, int cycleDay) {
-        cal.set(Calendar.HOUR_OF_DAY, 0);
-        cal.set(Calendar.MINUTE, 0);
-        cal.set(Calendar.SECOND, 0);
-        if (cycleDay > cal.getActualMaximum(Calendar.DAY_OF_MONTH)) {
-            cal.add(Calendar.MONTH, 1);
-            cal.set(Calendar.DAY_OF_MONTH, 1);
-            cal.set(Calendar.HOUR_OF_DAY, 0);
-            cal.set(Calendar.MINUTE, 0);
-            cal.set(Calendar.SECOND, 0);
-            cal.add(Calendar.SECOND, -1);
-        } else {
-            cal.set(Calendar.DAY_OF_MONTH, cycleDay);
-        }
+    public static Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator(NetworkPolicy policy) {
+        return SubscriptionPlan.convert(policy).cycleIterator();
     }
 
     /**
index 8ac3642..6e4bcef 100644 (file)
     <permission android:name="android.permission.MANAGE_NETWORK_POLICY"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi Allows an application to manage fallback subscription plans.
+         Note that another app providing plans for an explicit HNI will always
+         take precidence over these fallback plans. @hide -->
+    <permission android:name="android.permission.MANAGE_FALLBACK_SUBSCRIPTION_PLANS"
+        android:protectionLevel="signature|privileged" />
+
     <!-- @SystemApi Allows an application to account its network traffic against other UIDs. Used
          by system services like download manager and media server. Not for use by
          third party apps. @hide -->
index 3640bfa..c346898 100644 (file)
@@ -151,7 +151,7 @@ public class NetworkPolicyEditor {
 
     public int getPolicyCycleDay(NetworkTemplate template) {
         final NetworkPolicy policy = getPolicy(template);
-        return (policy != null) ? policy.cycleDay : -1;
+        return (policy != null) ? policy.cycleDay : CYCLE_NONE;
     }
 
     public void setPolicyCycleDay(NetworkTemplate template, int cycleDay, String cycleTimezone) {
index b69232c..ed3696c 100644 (file)
 
 package com.android.settingslib.net;
 
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.NetworkStatsHistory.FIELD_RX_BYTES;
+import static android.net.NetworkStatsHistory.FIELD_TX_BYTES;
+import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.telephony.TelephonyManager.SIM_STATE_READY;
+import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH;
+import static android.text.format.DateUtils.FORMAT_SHOW_DATE;
+
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.INetworkStatsService;
@@ -29,22 +37,15 @@ import android.os.ServiceManager;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.format.DateUtils;
-import android.text.format.Time;
 import android.util.Log;
+import android.util.Pair;
 
 import com.android.internal.R;
 
+import java.time.ZonedDateTime;
 import java.util.Date;
 import java.util.Locale;
 
-import static android.net.ConnectivityManager.TYPE_MOBILE;
-import static android.net.NetworkStatsHistory.FIELD_RX_BYTES;
-import static android.net.NetworkStatsHistory.FIELD_TX_BYTES;
-import static android.telephony.TelephonyManager.SIM_STATE_READY;
-import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH;
-import static android.text.format.DateUtils.FORMAT_SHOW_DATE;
-import static android.net.TrafficStats.MB_IN_BYTES;
-
 public class DataUsageController {
 
     private static final String TAG = "DataUsageController";
@@ -107,13 +108,6 @@ public class DataUsageController {
         return null;
     }
 
-    private static Time addMonth(Time t, int months) {
-        final Time rt = new Time(t);
-        rt.set(t.monthDay, t.month + months, t.year);
-        rt.normalize(false);
-        return rt;
-    }
-
     public DataUsageInfo getDataUsageInfo() {
         final String subscriberId = getActiveSubscriberId(mContext);
         if (subscriberId == null) {
@@ -140,22 +134,11 @@ public class DataUsageController {
             final NetworkStatsHistory history = session.getHistoryForNetwork(template, FIELDS);
             final long now = System.currentTimeMillis();
             final long start, end;
-            if (policy != null && policy.cycleDay > 0) {
-                // period = determined from cycleDay
-                if (DEBUG) Log.d(TAG, "Cycle day=" + policy.cycleDay + " tz="
-                        + policy.cycleTimezone);
-                final Time nowTime = new Time(policy.cycleTimezone);
-                nowTime.setToNow();
-                final Time policyTime = new Time(nowTime);
-                policyTime.set(policy.cycleDay, policyTime.month, policyTime.year);
-                policyTime.normalize(false);
-                if (nowTime.after(policyTime)) {
-                    start = policyTime.toMillis(false);
-                    end = addMonth(policyTime, 1).toMillis(false);
-                } else {
-                    start = addMonth(policyTime, -1).toMillis(false);
-                    end = policyTime.toMillis(false);
-                }
+            if (policy != null) {
+                final Pair<ZonedDateTime, ZonedDateTime> cycle = NetworkPolicyManager
+                        .cycleIterator(policy).next();
+                start = cycle.first.toInstant().toEpochMilli();
+                end = cycle.second.toInstant().toEpochMilli();
             } else {
                 // period = last 4 wks
                 end = now;
index 8b8811e..9b032e2 100644 (file)
@@ -18,6 +18,7 @@ package com.android.server.net;
 
 import static android.Manifest.permission.ACCESS_NETWORK_STATE;
 import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
+import static android.Manifest.permission.MANAGE_FALLBACK_SUBSCRIPTION_PLANS;
 import static android.Manifest.permission.MANAGE_NETWORK_POLICY;
 import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
 import static android.Manifest.permission.READ_PHONE_STATE;
@@ -27,6 +28,7 @@ import static android.content.Intent.ACTION_UID_REMOVED;
 import static android.content.Intent.ACTION_USER_ADDED;
 import static android.content.Intent.ACTION_USER_REMOVED;
 import static android.content.Intent.EXTRA_UID;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
@@ -53,7 +55,6 @@ import static android.net.NetworkPolicyManager.RULE_NONE;
 import static android.net.NetworkPolicyManager.RULE_REJECT_ALL;
 import static android.net.NetworkPolicyManager.RULE_REJECT_METERED;
 import static android.net.NetworkPolicyManager.RULE_TEMPORARY_ALLOW_METERED;
-import static android.net.NetworkPolicyManager.computeLastCycleBoundary;
 import static android.net.NetworkPolicyManager.isProcStateAllowedWhileIdleOrPowerSaveMode;
 import static android.net.NetworkPolicyManager.isProcStateAllowedWhileOnRestrictBackground;
 import static android.net.NetworkPolicyManager.resolveNetworkId;
@@ -118,9 +119,11 @@ import android.net.INetworkStatsService;
 import android.net.LinkProperties;
 import android.net.NetworkIdentity;
 import android.net.NetworkPolicy;
+import android.net.NetworkPolicyManager;
 import android.net.NetworkQuotaInfo;
 import android.net.NetworkState;
 import android.net.NetworkTemplate;
+import android.net.TrafficStats;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiManager;
 import android.os.Binder;
@@ -141,12 +144,15 @@ import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
 import android.os.ShellCallback;
+import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
 import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionPlan;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.text.format.Formatter;
@@ -198,6 +204,7 @@ import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.nio.charset.StandardCharsets;
+import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
@@ -997,8 +1004,10 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
             if (!isTemplateRelevant(policy.template)) continue;
             if (!policy.hasCycle()) continue;
 
-            final long start = computeLastCycleBoundary(currentTime, policy);
-            final long end = currentTime;
+            final Pair<ZonedDateTime, ZonedDateTime> cycle = NetworkPolicyManager
+                    .cycleIterator(policy).next();
+            final long start = cycle.first.toInstant().toEpochMilli();
+            final long end = cycle.second.toInstant().toEpochMilli();
             final long totalBytes = getTotalBytes(policy.template, start, end);
 
             if (policy.isOverLimit(totalBytes)) {
@@ -1455,8 +1464,10 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
                 continue;
             }
 
-            final long start = computeLastCycleBoundary(currentTime, policy);
-            final long end = currentTime;
+            final Pair<ZonedDateTime, ZonedDateTime> cycle = NetworkPolicyManager
+                    .cycleIterator(policy).next();
+            final long start = cycle.first.toInstant().toEpochMilli();
+            final long end = cycle.second.toInstant().toEpochMilli();
             final long totalBytes = getTotalBytes(policy.template, start, end);
 
             // disable data connection when over limit and not snoozed
@@ -1566,15 +1577,11 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
             final NetworkPolicy policy = mNetworkRules.keyAt(i);
             final String[] ifaces = mNetworkRules.valueAt(i);
 
-            final long start;
-            final long totalBytes;
-            if (policy.hasCycle()) {
-                start = computeLastCycleBoundary(currentTime, policy);
-                totalBytes = getTotalBytes(policy.template, start, currentTime);
-            } else {
-                start = Long.MAX_VALUE;
-                totalBytes = 0;
-            }
+            final Pair<ZonedDateTime, ZonedDateTime> cycle = NetworkPolicyManager
+                    .cycleIterator(policy).next();
+            final long start = cycle.first.toInstant().toEpochMilli();
+            final long end = cycle.second.toInstant().toEpochMilli();
+            final long totalBytes = getTotalBytes(policy.template, start, end);
 
             if (LOGD) {
                 Slog.d(TAG, "applying policy " + policy + " to ifaces " + Arrays.toString(ifaces));
@@ -2479,6 +2486,178 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
         return new NetworkQuotaInfo();
     }
 
+    private void enforceSubscriptionPlanAccess(int subId, int callingUid, String callingPackage) {
+        // Verify they're not lying about package name
+        mAppOps.checkPackage(callingUid, callingPackage);
+
+        // Verify they have phone permission from user
+        mContext.enforceCallingOrSelfPermission(READ_PHONE_STATE, TAG);
+        if (mAppOps.checkOp(AppOpsManager.OP_READ_PHONE_STATE, callingUid,
+                callingPackage) != AppOpsManager.MODE_ALLOWED) {
+            throw new SecurityException(
+                    "Calling package " + callingPackage + " does not hold " + READ_PHONE_STATE);
+        }
+
+        final SubscriptionInfo si;
+        final long token = Binder.clearCallingIdentity();
+        try {
+            si = mContext.getSystemService(SubscriptionManager.class)
+                    .getActiveSubscriptionInfo(subId);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+
+        // First check: does caller have carrier access?
+        if (si.isEmbedded() && si.canManageSubscription(mContext, callingPackage)) {
+            Slog.v(TAG, "Granting access because " + callingPackage + " is carrier");
+            return;
+        }
+
+        // Second check: was caller first to claim this HNI?
+        // TODO: extend to support external data sources
+
+        // Final check: does caller have fallback permission?
+        if (mContext.checkCallingOrSelfPermission(
+                MANAGE_FALLBACK_SUBSCRIPTION_PLANS) == PERMISSION_GRANTED) {
+            Slog.v(TAG, "Granting access because " + callingPackage + " is fallback");
+            return;
+        }
+
+        throw new SecurityException("Calling package " + callingPackage
+                + " has no access to subscription plans for " + subId);
+    }
+
+    @Override
+    public SubscriptionPlan[] getSubscriptionPlans(int subId, String callingPackage) {
+        enforceSubscriptionPlanAccess(subId, Binder.getCallingUid(), callingPackage);
+
+        // TODO: extend to support external data sources
+        if (!"com.android.settings".equals(callingPackage)) {
+            throw new UnsupportedOperationException();
+        }
+
+        final String fake = SystemProperties.get("fw.fake_plan");
+        if (!TextUtils.isEmpty(fake)) {
+            final List<SubscriptionPlan> plans = new ArrayList<>();
+            if ("month_hard".equals(fake)) {
+                plans.add(SubscriptionPlan.Builder
+                        .createRecurringMonthly(ZonedDateTime.parse("2007-03-14T00:00:00.000Z"))
+                        .setTitle("G-Mobile")
+                        .setDataWarning(2 * TrafficStats.GB_IN_BYTES)
+                        .setDataLimit(5 * TrafficStats.GB_IN_BYTES,
+                                SubscriptionPlan.LIMIT_BEHAVIOR_BILLED)
+                        .setDataUsage(1 * TrafficStats.GB_IN_BYTES,
+                                ZonedDateTime.now().minusHours(36).toInstant().toEpochMilli())
+                        .build());
+            } else if ("month_soft".equals(fake)) {
+                plans.add(SubscriptionPlan.Builder
+                        .createRecurringMonthly(ZonedDateTime.parse("2007-03-14T00:00:00.000Z"))
+                        .setTitle("G-Mobile is the carriers name who this plan belongs to")
+                        .setSummary("Crazy unlimited bandwidth plan with incredibly long title "
+                                + "that should be cut off to prevent UI from looking terrible")
+                        .setDataWarning(2 * TrafficStats.GB_IN_BYTES)
+                        .setDataLimit(5 * TrafficStats.GB_IN_BYTES,
+                                SubscriptionPlan.LIMIT_BEHAVIOR_THROTTLED)
+                        .setDataUsage(1 * TrafficStats.GB_IN_BYTES,
+                                ZonedDateTime.now().minusHours(1).toInstant().toEpochMilli())
+                        .build());
+            } else if ("month_none".equals(fake)) {
+                plans.add(SubscriptionPlan.Builder
+                        .createRecurringMonthly(ZonedDateTime.parse("2007-03-14T00:00:00.000Z"))
+                        .setTitle("G-Mobile")
+                        .build());
+            } else if ("prepaid".equals(fake)) {
+                plans.add(SubscriptionPlan.Builder
+                        .createNonrecurring(ZonedDateTime.now().minusDays(20),
+                                ZonedDateTime.now().plusDays(10))
+                        .setTitle("G-Mobile")
+                        .setDataLimit(512 * TrafficStats.MB_IN_BYTES,
+                                SubscriptionPlan.LIMIT_BEHAVIOR_DISABLED)
+                        .setDataUsage(100 * TrafficStats.MB_IN_BYTES,
+                                ZonedDateTime.now().minusHours(3).toInstant().toEpochMilli())
+                        .build());
+            } else if ("prepaid_crazy".equals(fake)) {
+                plans.add(SubscriptionPlan.Builder
+                        .createNonrecurring(ZonedDateTime.now().minusDays(20),
+                                ZonedDateTime.now().plusDays(10))
+                        .setTitle("G-Mobile Anytime")
+                        .setDataLimit(512 * TrafficStats.MB_IN_BYTES,
+                                SubscriptionPlan.LIMIT_BEHAVIOR_DISABLED)
+                        .setDataUsage(100 * TrafficStats.MB_IN_BYTES,
+                                ZonedDateTime.now().minusHours(3).toInstant().toEpochMilli())
+                        .build());
+                plans.add(SubscriptionPlan.Builder
+                        .createNonrecurring(ZonedDateTime.now().minusDays(10),
+                                ZonedDateTime.now().plusDays(20))
+                        .setTitle("G-Mobile Nickel Nights")
+                        .setSummary("5¢/GB between 1-5AM")
+                        .setDataUsage(15 * TrafficStats.MB_IN_BYTES,
+                                ZonedDateTime.now().minusHours(30).toInstant().toEpochMilli())
+                        .build());
+                plans.add(SubscriptionPlan.Builder
+                        .createNonrecurring(ZonedDateTime.now().minusDays(10),
+                                ZonedDateTime.now().plusDays(20))
+                        .setTitle("G-Mobile Bonus 3G")
+                        .setSummary("Unlimited 3G data")
+                        .setDataLimit(5 * TrafficStats.GB_IN_BYTES,
+                                SubscriptionPlan.LIMIT_BEHAVIOR_THROTTLED)
+                        .setDataUsage(300 * TrafficStats.MB_IN_BYTES,
+                                ZonedDateTime.now().minusHours(1).toInstant().toEpochMilli())
+                        .build());
+            }
+            return plans.toArray(new SubscriptionPlan[plans.size()]);
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            final TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+            final NetworkTemplate template = NetworkTemplate
+                    .buildTemplateMobileAll(tm.getSubscriberId(subId));
+            final NetworkPolicy policy = mNetworkPolicy.get(template);
+            if (policy != null) {
+                return new SubscriptionPlan[] { SubscriptionPlan.convert(policy) };
+            } else {
+                return new SubscriptionPlan[0];
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public void setSubscriptionPlans(int subId, SubscriptionPlan[] plans, String callingPackage) {
+        enforceSubscriptionPlanAccess(subId, Binder.getCallingUid(), callingPackage);
+
+        // TODO: extend to support external data sources
+        if (!"com.android.settings".equals(callingPackage)) {
+            throw new UnsupportedOperationException();
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            final TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+            final NetworkTemplate template = NetworkTemplate
+                    .buildTemplateMobileAll(tm.getSubscriberId(subId));
+            if (ArrayUtils.isEmpty(plans)) {
+                mNetworkPolicy.remove(template);
+            } else {
+                final NetworkPolicy policy = SubscriptionPlan.convert(plans[0]);
+                policy.template = template;
+                mNetworkPolicy.put(template, policy);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public String getSubscriptionPlanOwner(int subId) {
+        mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
+
+        // TODO: extend to support external data sources
+        return "com.android.settings";
+    }
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return;
index ad8303a..11e383c 100644 (file)
@@ -24,15 +24,13 @@ import static android.net.NetworkPolicy.WARNING_DISABLED;
 import static android.net.NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND;
 import static android.net.NetworkPolicyManager.POLICY_NONE;
 import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND;
-import static android.net.NetworkPolicyManager.computeLastCycleBoundary;
-import static android.net.NetworkPolicyManager.computeNextCycleBoundary;
 import static android.net.NetworkPolicyManager.uidPoliciesToString;
 import static android.net.NetworkTemplate.buildTemplateMobileAll;
 import static android.net.TrafficStats.KB_IN_BYTES;
 import static android.net.TrafficStats.MB_IN_BYTES;
 import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
-import static android.telephony.CarrierConfigManager.DATA_CYCLE_USE_PLATFORM_DEFAULT;
 import static android.telephony.CarrierConfigManager.DATA_CYCLE_THRESHOLD_DISABLED;
+import static android.telephony.CarrierConfigManager.DATA_CYCLE_USE_PLATFORM_DEFAULT;
 import static android.telephony.CarrierConfigManager.KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG;
 import static android.telephony.CarrierConfigManager.KEY_DATA_WARNING_THRESHOLD_BYTES_LONG;
 import static android.telephony.CarrierConfigManager.KEY_MONTHLY_DATA_CYCLE_DAY_INT;
@@ -41,10 +39,10 @@ import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 import static android.text.format.Time.TIMEZONE_UTC;
 
 import static com.android.server.net.NetworkPolicyManagerService.MAX_PROC_STATE_SEQ_HISTORY;
-import static com.android.server.net.NetworkPolicyManagerService.ProcStateSeqHistory;
 import static com.android.server.net.NetworkPolicyManagerService.TYPE_LIMIT;
 import static com.android.server.net.NetworkPolicyManagerService.TYPE_LIMIT_SNOOZED;
 import static com.android.server.net.NetworkPolicyManagerService.TYPE_WARNING;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -53,22 +51,19 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyLong;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isA;
-import static org.mockito.Matchers.isNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -97,6 +92,7 @@ import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
 import android.net.NetworkInfo.DetailedState;
 import android.net.NetworkPolicy;
+import android.net.NetworkPolicyManager;
 import android.net.NetworkState;
 import android.net.NetworkStats;
 import android.net.NetworkTemplate;
@@ -112,18 +108,21 @@ import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionPlan;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.text.format.Time;
 import android.util.Log;
+import android.util.Pair;
 import android.util.TrustedTime;
 
-import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.internal.util.test.BroadcastInterceptingContext.FutureIntent;
 import com.android.server.net.NetworkPolicyManagerInternal;
 import com.android.server.net.NetworkPolicyManagerService;
+import com.android.server.net.NetworkPolicyManagerService.ProcStateSeqHistory;
 
 import libcore.io.IoUtils;
 import libcore.io.Streams;
@@ -132,7 +131,6 @@ import com.google.common.util.concurrent.AbstractFuture;
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.MethodRule;
@@ -156,8 +154,11 @@ import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.time.Instant;
+import java.time.ZonedDateTime;
 import java.util.Arrays;
 import java.util.Calendar;
+import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
@@ -393,6 +394,11 @@ public class NetworkPolicyManagerServiceTest {
         LocalServices.removeServiceForTest(UsageStatsManagerInternal.class);
     }
 
+    @After
+    public void resetClock() throws Exception {
+        SubscriptionPlan.sNowOverride = -1;
+    }
+
     @Test
     public void testTurnRestrictBackgroundOn() throws Exception {
         assertRestrictBackgroundOff(); // Sanity check.
@@ -778,6 +784,25 @@ public class NetworkPolicyManagerServiceTest {
         assertTrue(mService.isUidForeground(UID_B));
     }
 
+    private static long computeLastCycleBoundary(long currentTime, NetworkPolicy policy) {
+        SubscriptionPlan.sNowOverride = currentTime;
+        final Iterator<Pair<ZonedDateTime, ZonedDateTime>> it = NetworkPolicyManager
+                .cycleIterator(policy);
+        while (it.hasNext()) {
+            final Pair<ZonedDateTime, ZonedDateTime> cycle = it.next();
+            if (cycle.first.toInstant().toEpochMilli() < currentTime) {
+                return cycle.first.toInstant().toEpochMilli();
+            }
+        }
+        throw new IllegalStateException(
+                "Failed to find current cycle for " + policy + " at " + currentTime);
+    }
+
+    private static long computeNextCycleBoundary(long currentTime, NetworkPolicy policy) {
+        SubscriptionPlan.sNowOverride = currentTime;
+        return NetworkPolicyManager.cycleIterator(policy).next().second.toInstant().toEpochMilli();
+    }
+
     @Test
     public void testLastCycleBoundaryThisMonth() throws Exception {
         // assume cycle day of "5th", which should be in same month
@@ -818,7 +843,7 @@ public class NetworkPolicyManagerServiceTest {
     public void testLastCycleBoundaryLastMonthFebruary() throws Exception {
         // assume cycle day of "30th" in february, which should clamp
         final long currentTime = parseTime("2007-03-14T00:00:00.000Z");
-        final long expectedCycle = parseTime("2007-02-28T23:59:59.000Z");
+        final long expectedCycle = parseTime("2007-02-28T23:59:59.999Z");
 
         final NetworkPolicy policy = new NetworkPolicy(
                 sTemplateWifi, 30, TIMEZONE_UTC, 1024L, 1024L, false);
@@ -842,9 +867,9 @@ public class NetworkPolicyManagerServiceTest {
 
         assertTimeEquals(parseTime("2007-01-29T00:00:00.000Z"),
                 computeNextCycleBoundary(parseTime("2007-01-14T00:00:00.000Z"), policy));
-        assertTimeEquals(parseTime("2007-02-28T23:59:59.000Z"),
+        assertTimeEquals(parseTime("2007-02-28T23:59:59.999Z"),
                 computeNextCycleBoundary(parseTime("2007-02-14T00:00:00.000Z"), policy));
-        assertTimeEquals(parseTime("2007-02-28T23:59:59.000Z"),
+        assertTimeEquals(parseTime("2007-02-28T23:59:59.999Z"),
                 computeLastCycleBoundary(parseTime("2007-03-14T00:00:00.000Z"), policy));
         assertTimeEquals(parseTime("2007-03-29T00:00:00.000Z"),
                 computeNextCycleBoundary(parseTime("2007-03-14T00:00:00.000Z"), policy));
@@ -922,7 +947,7 @@ public class NetworkPolicyManagerServiceTest {
 
     @Test
     public void testLastCycleBoundaryDST() throws Exception {
-        final long currentTime = parseTime("1989-01-02T07:30:00.000");
+        final long currentTime = parseTime("1989-01-02T07:30:00.000Z");
         final long expectedCycle = parseTime("1988-12-03T02:00:00.000Z");
 
         final NetworkPolicy policy = new NetworkPolicy(
@@ -932,26 +957,16 @@ public class NetworkPolicyManagerServiceTest {
     }
 
     @Test
-    public void testLastCycleBoundaryJanuaryDST() throws Exception {
-        final long currentTime = parseTime("1989-01-26T21:00:00.000Z");
-        final long expectedCycle = parseTime("1989-01-01T01:59:59.000Z");
-
-        final NetworkPolicy policy = new NetworkPolicy(
-                sTemplateWifi, 32, "America/Argentina/Buenos_Aires", 1024L, 1024L, false);
-        final long actualCycle = computeLastCycleBoundary(currentTime, policy);
-        assertTimeEquals(expectedCycle, actualCycle);
-    }
-
-    @Test
     public void testNetworkPolicyAppliedCycleLastMonth() throws Exception {
         NetworkState[] state = null;
         NetworkStats stats = null;
 
-        final long TIME_FEB_15 = 1171497600000L;
-        final long TIME_MAR_10 = 1173484800000L;
         final int CYCLE_DAY = 15;
+        final long NOW = parseTime("2007-03-10T00:00Z");
+        final long CYCLE_START = parseTime("2007-02-15T00:00Z");
+        final long CYCLE_END = parseTime("2007-03-15T00:00Z");
 
-        setCurrentTimeMillis(TIME_MAR_10);
+        setCurrentTimeMillis(NOW);
 
         // first, pretend that wifi network comes online. no policy active,
         // which means we shouldn't push limit to interface.
@@ -971,7 +986,7 @@ public class NetworkPolicyManagerServiceTest {
         // pretend that 512 bytes total have happened
         stats = new NetworkStats(getElapsedRealtime(), 1)
                 .addIfaceValues(TEST_IFACE, 256L, 2L, 256L, 2L);
-        when(mStatsService.getNetworkTotalBytes(sTemplateWifi, TIME_FEB_15, TIME_MAR_10))
+        when(mStatsService.getNetworkTotalBytes(sTemplateWifi, CYCLE_START, CYCLE_END))
                 .thenReturn(stats.getTotalBytes());
 
         mPolicyListener.expect().onMeteredIfacesChanged(any());
@@ -991,11 +1006,12 @@ public class NetworkPolicyManagerServiceTest {
         NetworkStats stats = null;
         Future<String> tagFuture = null;
 
-        final long TIME_FEB_15 = 1171497600000L;
-        final long TIME_MAR_10 = 1173484800000L;
         final int CYCLE_DAY = 15;
+        final long NOW = parseTime("2007-03-10T00:00Z");
+        final long CYCLE_START = parseTime("2007-02-15T00:00Z");
+        final long CYCLE_END = parseTime("2007-03-15T00:00Z");
 
-        setCurrentTimeMillis(TIME_MAR_10);
+        setCurrentTimeMillis(NOW);
 
         // assign wifi policy
         state = new NetworkState[] {};
@@ -1005,8 +1021,8 @@ public class NetworkPolicyManagerServiceTest {
         {
             expectCurrentTime();
             when(mConnManager.getAllNetworkState()).thenReturn(state);
-            when(mStatsService.getNetworkTotalBytes(sTemplateWifi, TIME_FEB_15,
-                    currentTimeMillis())).thenReturn(stats.getTotalBytes());
+            when(mStatsService.getNetworkTotalBytes(sTemplateWifi, CYCLE_START,
+                    CYCLE_END)).thenReturn(stats.getTotalBytes());
 
             mPolicyListener.expect().onMeteredIfacesChanged(any());
             setNetworkPolicies(new NetworkPolicy(sTemplateWifi, CYCLE_DAY, TIMEZONE_UTC, 1
@@ -1024,8 +1040,8 @@ public class NetworkPolicyManagerServiceTest {
         {
             expectCurrentTime();
             when(mConnManager.getAllNetworkState()).thenReturn(state);
-            when(mStatsService.getNetworkTotalBytes(sTemplateWifi, TIME_FEB_15,
-                    currentTimeMillis())).thenReturn(stats.getTotalBytes());
+            when(mStatsService.getNetworkTotalBytes(sTemplateWifi, CYCLE_START,
+                    CYCLE_END)).thenReturn(stats.getTotalBytes());
 
             mPolicyListener.expect().onMeteredIfacesChanged(any());
             mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION));
@@ -1043,8 +1059,8 @@ public class NetworkPolicyManagerServiceTest {
 
         {
             expectCurrentTime();
-            when(mStatsService.getNetworkTotalBytes(sTemplateWifi, TIME_FEB_15,
-                    currentTimeMillis())).thenReturn(stats.getTotalBytes());
+            when(mStatsService.getNetworkTotalBytes(sTemplateWifi, CYCLE_START,
+                    CYCLE_END)).thenReturn(stats.getTotalBytes());
             tagFuture = expectEnqueueNotification();
 
             mNetworkObserver.limitReached(null, TEST_IFACE);
@@ -1061,8 +1077,8 @@ public class NetworkPolicyManagerServiceTest {
 
         {
             expectCurrentTime();
-            when(mStatsService.getNetworkTotalBytes(sTemplateWifi, TIME_FEB_15,
-                    currentTimeMillis())).thenReturn(stats.getTotalBytes());
+            when(mStatsService.getNetworkTotalBytes(sTemplateWifi, CYCLE_START,
+                    CYCLE_END)).thenReturn(stats.getTotalBytes());
             tagFuture = expectEnqueueNotification();
 
             mNetworkObserver.limitReached(null, TEST_IFACE);
@@ -1077,8 +1093,8 @@ public class NetworkPolicyManagerServiceTest {
         {
             expectCurrentTime();
             when(mConnManager.getAllNetworkState()).thenReturn(state);
-            when(mStatsService.getNetworkTotalBytes(sTemplateWifi, TIME_FEB_15,
-                    currentTimeMillis())).thenReturn(stats.getTotalBytes());
+            when(mStatsService.getNetworkTotalBytes(sTemplateWifi, CYCLE_START,
+                    CYCLE_END)).thenReturn(stats.getTotalBytes());
             tagFuture = expectEnqueueNotification();
 
             mPolicyListener.expect().onMeteredIfacesChanged(any());
@@ -1129,6 +1145,15 @@ public class NetworkPolicyManagerServiceTest {
     }
 
     @Test
+    public void testConversion() throws Exception {
+        NetworkTemplate template = NetworkTemplate.buildTemplateMobileWildcard();
+        NetworkPolicy before = new NetworkPolicy(template, 12, "Israel", 123, 456, true);
+        NetworkPolicy after = SubscriptionPlan.convert(SubscriptionPlan.convert(before));
+        after.template = before.template;
+        assertEquals(before, after);
+    }
+
+    @Test
     public void testOnUidStateChanged_notifyAMS() throws Exception {
         final long procStateSeq = 222;
         callOnUidStateChanged(UID_A, ActivityManager.PROCESS_STATE_SERVICE, procStateSeq);
@@ -1470,9 +1495,7 @@ public class NetworkPolicyManagerServiceTest {
     }
 
     private static long parseTime(String time) {
-        final Time result = new Time();
-        result.parse3339(time);
-        return result.toMillis(true);
+        return ZonedDateTime.parse(time).toInstant().toEpochMilli();
     }
 
     private void setNetworkPolicies(NetworkPolicy... policies) {
@@ -1559,16 +1582,15 @@ public class NetworkPolicyManagerServiceTest {
     }
 
     private static String formatTime(long millis) {
-        final Time time = new Time(Time.TIMEZONE_UTC);
-        time.set(millis);
-        return time.format3339(false);
+        return Instant.ofEpochMilli(millis) + " [" + millis + "]";
     }
 
     private static void assertEqualsFuzzy(long expected, long actual, long fuzzy) {
         final long low = expected - fuzzy;
         final long high = expected + fuzzy;
         if (actual < low || actual > high) {
-            fail("value " + actual + " is outside [" + low + "," + high + "]");
+            fail("value " + formatTime(actual) + " is outside [" + formatTime(low) + ","
+                    + formatTime(high) + "]");
         }
     }
 
@@ -1643,6 +1665,7 @@ public class NetworkPolicyManagerServiceTest {
     }
 
     private void setCurrentTimeMillis(long currentTimeMillis) {
+        SubscriptionPlan.sNowOverride = currentTimeMillis;
         mStartTime = currentTimeMillis;
         mElapsedRealtime = 0L;
     }
index 0d1764b..89c9134 100644 (file)
@@ -24,6 +24,7 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.net.INetworkPolicyManager;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.Message;
@@ -37,6 +38,7 @@ import com.android.internal.telephony.ITelephonyRegistry;
 import com.android.internal.telephony.PhoneConstants;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -1539,4 +1541,39 @@ public class SubscriptionManager {
         }
         return false;
     }
+
+    /** {@pending} */
+    public @NonNull List<SubscriptionPlan> getSubscriptionPlans(int subId) {
+        final INetworkPolicyManager npm = INetworkPolicyManager.Stub
+                .asInterface(ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
+        try {
+            return Arrays.asList(npm.getSubscriptionPlans(subId,
+                    mContext.getOpPackageName()));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@pending} */
+    public void setSubscriptionPlans(int subId, @NonNull List<SubscriptionPlan> plans) {
+        final INetworkPolicyManager npm = INetworkPolicyManager.Stub
+                .asInterface(ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
+        try {
+            npm.setSubscriptionPlans(subId, plans.toArray(new SubscriptionPlan[plans.size()]),
+                    mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    public String getSubscriptionPlanOwner(int subId) {
+        final INetworkPolicyManager npm = INetworkPolicyManager.Stub
+                .asInterface(ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
+        try {
+            return npm.getSubscriptionPlanOwner(subId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/telephony/java/android/telephony/SubscriptionPlan.aidl b/telephony/java/android/telephony/SubscriptionPlan.aidl
new file mode 100755 (executable)
index 0000000..655df3a
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2017 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 android.telephony;
+
+parcelable SubscriptionPlan;
diff --git a/telephony/java/android/telephony/SubscriptionPlan.java b/telephony/java/android/telephony/SubscriptionPlan.java
new file mode 100644 (file)
index 0000000..da7661a
--- /dev/null
@@ -0,0 +1,478 @@
+/*
+ * Copyright (C) 2017 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 android.telephony;
+
+import android.annotation.BytesLong;
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.net.NetworkPolicy;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
+import java.util.Iterator;
+
+/** {@pending} */
+public final class SubscriptionPlan implements Parcelable {
+    private static final String TAG = "SubscriptionPlan";
+    private static final boolean DEBUG = false;
+
+    /** {@hide} */
+    @IntDef(prefix = "TYPE_", value = {
+            TYPE_NONRECURRING,
+            TYPE_RECURRING_WEEKLY,
+            TYPE_RECURRING_MONTHLY,
+            TYPE_RECURRING_DAILY,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Type {}
+
+    public static final int TYPE_NONRECURRING = 0;
+    public static final int TYPE_RECURRING_MONTHLY = 1;
+    public static final int TYPE_RECURRING_WEEKLY = 2;
+    public static final int TYPE_RECURRING_DAILY = 3;
+
+    /** {@hide} */
+    @IntDef(prefix = "LIMIT_BEHAVIOR_", value = {
+            LIMIT_BEHAVIOR_UNKNOWN,
+            LIMIT_BEHAVIOR_DISABLED,
+            LIMIT_BEHAVIOR_BILLED,
+            LIMIT_BEHAVIOR_THROTTLED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface LimitBehavior {}
+
+    public static final int LIMIT_BEHAVIOR_UNKNOWN = -1;
+    public static final int LIMIT_BEHAVIOR_DISABLED = 0;
+    public static final int LIMIT_BEHAVIOR_BILLED = 1;
+    public static final int LIMIT_BEHAVIOR_THROTTLED = 2;
+
+    public static final long BYTES_UNKNOWN = -1;
+    public static final long TIME_UNKNOWN = -1;
+
+    private final int type;
+    private final ZonedDateTime start;
+    private final ZonedDateTime end;
+    private CharSequence title;
+    private CharSequence summary;
+    private long dataWarningBytes = BYTES_UNKNOWN;
+    private long dataWarningSnoozeTime = TIME_UNKNOWN;
+    private long dataLimitBytes = BYTES_UNKNOWN;
+    private long dataLimitSnoozeTime = TIME_UNKNOWN;
+    private int dataLimitBehavior = LIMIT_BEHAVIOR_UNKNOWN;
+    private long dataUsageBytes = BYTES_UNKNOWN;
+    private long dataUsageTime = TIME_UNKNOWN;
+
+    private SubscriptionPlan(@Type int type, ZonedDateTime start, ZonedDateTime end) {
+        this.type = type;
+        this.start = start;
+        this.end = end;
+    }
+
+    private SubscriptionPlan(Parcel source) {
+        type = source.readInt();
+        if (source.readInt() != 0) {
+            start = ZonedDateTime.parse(source.readString());
+        } else {
+            start = null;
+        }
+        if (source.readInt() != 0) {
+            end = ZonedDateTime.parse(source.readString());
+        } else {
+            end = null;
+        }
+        title = source.readCharSequence();
+        summary = source.readCharSequence();
+        dataWarningBytes = source.readLong();
+        dataWarningSnoozeTime = source.readLong();
+        dataLimitBytes = source.readLong();
+        dataLimitSnoozeTime = source.readLong();
+        dataLimitBehavior = source.readInt();
+        dataUsageBytes = source.readLong();
+        dataUsageTime = source.readLong();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(type);
+        if (start != null) {
+            dest.writeInt(1);
+            dest.writeString(start.toString());
+        } else {
+            dest.writeInt(0);
+        }
+        if (end != null) {
+            dest.writeInt(1);
+            dest.writeString(end.toString());
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeCharSequence(title);
+        dest.writeCharSequence(summary);
+        dest.writeLong(dataWarningBytes);
+        dest.writeLong(dataWarningSnoozeTime);
+        dest.writeLong(dataLimitBytes);
+        dest.writeLong(dataLimitSnoozeTime);
+        dest.writeInt(dataLimitBehavior);
+        dest.writeLong(dataUsageBytes);
+        dest.writeLong(dataUsageTime);
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder("SubscriptionPlan:")
+                .append(" type=").append(type)
+                .append(" start=").append(start)
+                .append(" end=").append(end)
+                .append(" title=").append(title)
+                .append(" summary=").append(summary)
+                .append(" dataWarningBytes=").append(dataWarningBytes)
+                .append(" dataWarningSnoozeTime=").append(dataWarningSnoozeTime)
+                .append(" dataLimitBytes=").append(dataLimitBytes)
+                .append(" dataLimitSnoozeTime=").append(dataLimitSnoozeTime)
+                .append(" dataLimitBehavior=").append(dataLimitBehavior)
+                .append(" dataUsageBytes=").append(dataUsageBytes)
+                .append(" dataUsageTime=").append(dataUsageTime)
+                .toString();
+    }
+
+    public static final Parcelable.Creator<SubscriptionPlan> CREATOR = new Parcelable.Creator<SubscriptionPlan>() {
+        @Override
+        public SubscriptionPlan createFromParcel(Parcel source) {
+            return new SubscriptionPlan(source);
+        }
+
+        @Override
+        public SubscriptionPlan[] newArray(int size) {
+            return new SubscriptionPlan[size];
+        }
+    };
+
+    public @Type int getType() {
+        return type;
+    }
+
+    public ZonedDateTime getStart() {
+        return start;
+    }
+
+    public ZonedDateTime getEnd() {
+        return end;
+    }
+
+    public @Nullable CharSequence getTitle() {
+        return title;
+    }
+
+    public @Nullable CharSequence getSummary() {
+        return summary;
+    }
+
+    public @BytesLong long getDataWarningBytes() {
+        return dataWarningBytes;
+    }
+
+    public @BytesLong long getDataLimitBytes() {
+        return dataLimitBytes;
+    }
+
+    public @LimitBehavior int getDataLimitBehavior() {
+        return dataLimitBehavior;
+    }
+
+    public @BytesLong long getDataUsageBytes() {
+        return dataUsageBytes;
+    }
+
+    public @CurrentTimeMillisLong long getDataUsageTime() {
+        return dataUsageTime;
+    }
+
+    /** {@hide} */
+    @VisibleForTesting
+    public static long sNowOverride = -1;
+
+    private static ZonedDateTime now(ZoneId zone) {
+        return (sNowOverride != -1)
+                ? ZonedDateTime.ofInstant(Instant.ofEpochMilli(sNowOverride), zone)
+                : ZonedDateTime.now(zone);
+    }
+
+    /** {@hide} */
+    public static SubscriptionPlan convert(NetworkPolicy policy) {
+        final ZoneId zone = ZoneId.of(policy.cycleTimezone);
+        final ZonedDateTime now = now(zone);
+        final Builder builder;
+        if (policy.cycleDay != NetworkPolicy.CYCLE_NONE) {
+            // Assume we started last January, since it has all possible days
+            ZonedDateTime start = ZonedDateTime.of(
+                    now.toLocalDate().minusYears(1).withMonth(1).withDayOfMonth(policy.cycleDay),
+                    LocalTime.MIDNIGHT, zone);
+            builder = Builder.createRecurringMonthly(start);
+        } else {
+            Log.w(TAG, "Cycle not defined; assuming last 4 weeks non-recurring");
+            ZonedDateTime end = now;
+            ZonedDateTime start = end.minusWeeks(4);
+            builder = Builder.createNonrecurring(start, end);
+        }
+        if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) {
+            builder.setDataWarning(policy.warningBytes);
+        }
+        if (policy.lastWarningSnooze != NetworkPolicy.SNOOZE_NEVER) {
+            builder.setDataWarningSnooze(policy.lastWarningSnooze);
+        }
+        if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) {
+            builder.setDataLimit(policy.limitBytes, LIMIT_BEHAVIOR_DISABLED);
+        }
+        if (policy.lastLimitSnooze != NetworkPolicy.SNOOZE_NEVER) {
+            builder.setDataLimitSnooze(policy.lastLimitSnooze);
+        }
+        return builder.build();
+    }
+
+    /** {@hide} */
+    public static NetworkPolicy convert(SubscriptionPlan plan) {
+        final NetworkPolicy policy = new NetworkPolicy();
+        switch (plan.type) {
+            case TYPE_RECURRING_MONTHLY:
+                policy.cycleDay = plan.start.getDayOfMonth();
+                policy.cycleTimezone = plan.start.getZone().getId();
+                break;
+            default:
+                policy.cycleDay = NetworkPolicy.CYCLE_NONE;
+                policy.cycleTimezone = "UTC";
+                break;
+        }
+        policy.warningBytes = plan.dataWarningBytes;
+        policy.limitBytes = plan.dataLimitBytes;
+        policy.lastWarningSnooze = plan.dataWarningSnoozeTime;
+        policy.lastLimitSnooze = plan.dataLimitSnoozeTime;
+        policy.metered = true;
+        policy.inferred = false;
+        return policy;
+    }
+
+    /** {@hide} */
+    public TemporalUnit getTemporalUnit() {
+        switch (type) {
+            case TYPE_RECURRING_DAILY: return ChronoUnit.DAYS;
+            case TYPE_RECURRING_WEEKLY: return ChronoUnit.WEEKS;
+            case TYPE_RECURRING_MONTHLY: return ChronoUnit.MONTHS;
+            default: throw new IllegalArgumentException();
+        }
+    }
+
+    /**
+     * Return an iterator that returns data usage cycles.
+     * <p>
+     * For recurring plans, it starts at the currently active cycle, and then
+     * walks backwards in time through each previous cycle, back to the defined
+     * starting point and no further.
+     * <p>
+     * For non-recurring plans, it returns one single cycle.
+     */
+    public Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator() {
+        switch (type) {
+            case TYPE_NONRECURRING:
+                return new NonrecurringIterator();
+            case TYPE_RECURRING_WEEKLY:
+            case TYPE_RECURRING_MONTHLY:
+            case TYPE_RECURRING_DAILY:
+                return new RecurringIterator();
+            default:
+                throw new IllegalStateException("Unknown type: " + type);
+        }
+    }
+
+    private class NonrecurringIterator implements Iterator<Pair<ZonedDateTime, ZonedDateTime>> {
+        boolean hasNext = true;
+
+        @Override
+        public boolean hasNext() {
+            return hasNext;
+        }
+
+        @Override
+        public Pair<ZonedDateTime, ZonedDateTime> next() {
+            hasNext = false;
+            return new Pair<>(start, end);
+        }
+    }
+
+    private class RecurringIterator implements Iterator<Pair<ZonedDateTime, ZonedDateTime>> {
+        TemporalUnit unit;
+        long i;
+        ZonedDateTime cycleStart;
+        ZonedDateTime cycleEnd;
+
+        public RecurringIterator() {
+            final ZonedDateTime now = now(start.getZone());
+            if (DEBUG) Log.d(TAG, "Resolving using now " + now);
+
+            unit = getTemporalUnit();
+            i = unit.between(start, now);
+            updateCycle();
+
+            // Walk forwards until we find first cycle after now
+            while (cycleEnd.toEpochSecond() <= now.toEpochSecond()) {
+                i++;
+                updateCycle();
+            }
+
+            // Walk backwards until we find first cycle before now
+            while (cycleStart.toEpochSecond() > now.toEpochSecond()) {
+                i--;
+                updateCycle();
+            }
+        }
+
+        private void updateCycle() {
+            cycleStart = roundBoundaryTime(start.plus(i, unit));
+            cycleEnd = roundBoundaryTime(start.plus(i + 1, unit));
+        }
+
+        private ZonedDateTime roundBoundaryTime(ZonedDateTime boundary) {
+            if ((type == TYPE_RECURRING_MONTHLY)
+                    && (boundary.getDayOfMonth() < start.getDayOfMonth())) {
+                // When forced to end a monthly cycle early, we want to count
+                // that entire day against the boundary.
+                return ZonedDateTime.of(boundary.toLocalDate(), LocalTime.MAX, start.getZone());
+            } else {
+                return boundary;
+            }
+        }
+
+        @Override
+        public boolean hasNext() {
+            return cycleStart.toEpochSecond() >= start.toEpochSecond();
+        }
+
+        @Override
+        public Pair<ZonedDateTime, ZonedDateTime> next() {
+            if (DEBUG) Log.d(TAG, "Cycle " + i + " from " + cycleStart + " to " + cycleEnd);
+            Pair<ZonedDateTime, ZonedDateTime> p = new Pair<>(cycleStart, cycleEnd);
+            i--;
+            updateCycle();
+            return p;
+        }
+    }
+
+    public static class Builder {
+        private final SubscriptionPlan plan;
+
+        private Builder(@Type int type, ZonedDateTime start, ZonedDateTime end) {
+            plan = new SubscriptionPlan(type, start, end);
+        }
+
+        public static Builder createNonrecurring(ZonedDateTime start, ZonedDateTime end) {
+            if (!end.isAfter(start)) {
+                throw new IllegalArgumentException(
+                        "End " + end + " isn't after start " + start);
+            }
+            return new Builder(TYPE_NONRECURRING, start, end);
+        }
+
+        public static Builder createRecurringMonthly(ZonedDateTime start) {
+            return new Builder(TYPE_RECURRING_MONTHLY, start, null);
+        }
+
+        public static Builder createRecurringWeekly(ZonedDateTime start) {
+            return new Builder(TYPE_RECURRING_WEEKLY, start, null);
+        }
+
+        public static Builder createRecurringDaily(ZonedDateTime start) {
+            return new Builder(TYPE_RECURRING_DAILY, start, null);
+        }
+
+        public SubscriptionPlan build() {
+            return plan;
+        }
+
+        public Builder setTitle(@Nullable CharSequence title) {
+            plan.title = title;
+            return this;
+        }
+
+        public Builder setSummary(@Nullable CharSequence summary) {
+            plan.summary = summary;
+            return this;
+        }
+
+        public Builder setDataWarning(@BytesLong long dataWarningBytes) {
+            if (dataWarningBytes < BYTES_UNKNOWN) {
+                throw new IllegalArgumentException("Warning must be positive or BYTES_UNKNOWN");
+            }
+            plan.dataWarningBytes = dataWarningBytes;
+            return this;
+        }
+
+        /** {@hide} */
+        public Builder setDataWarningSnooze(@CurrentTimeMillisLong long dataWarningSnoozeTime) {
+            plan.dataWarningSnoozeTime = dataWarningSnoozeTime;
+            return this;
+        }
+
+        public Builder setDataLimit(@BytesLong long dataLimitBytes,
+                @LimitBehavior int dataLimitBehavior) {
+            if (dataLimitBytes < BYTES_UNKNOWN) {
+                throw new IllegalArgumentException("Limit must be positive or BYTES_UNKNOWN");
+            }
+            plan.dataLimitBytes = dataLimitBytes;
+            plan.dataLimitBehavior = dataLimitBehavior;
+            return this;
+        }
+
+        /** {@hide} */
+        public Builder setDataLimitSnooze(@CurrentTimeMillisLong long dataLimitSnoozeTime) {
+            plan.dataLimitSnoozeTime = dataLimitSnoozeTime;
+            return this;
+        }
+
+        public Builder setDataUsage(@BytesLong long dataUsageBytes,
+                @CurrentTimeMillisLong long dataUsageTime) {
+            if (dataUsageBytes < BYTES_UNKNOWN) {
+                throw new IllegalArgumentException("Usage must be positive or BYTES_UNKNOWN");
+            }
+            if (dataUsageTime < TIME_UNKNOWN) {
+                throw new IllegalArgumentException("Time must be positive or TIME_UNKNOWN");
+            }
+            if ((dataUsageBytes == BYTES_UNKNOWN) != (dataUsageTime == TIME_UNKNOWN)) {
+                throw new IllegalArgumentException("Must provide both usage and time or neither");
+            }
+            plan.dataUsageBytes = dataUsageBytes;
+            plan.dataUsageTime = dataUsageTime;
+            return this;
+        }
+    }
+}