OSDN Git Service

Expose SubscriptionPlan API.
authorJeff Sharkey <jsharkey@android.com>
Thu, 20 Jul 2017 03:00:38 +0000 (21:00 -0600)
committerJeff Sharkey <jsharkey@android.com>
Thu, 20 Jul 2017 20:55:57 +0000 (14:55 -0600)
Clean up SubscriptionPlan, add docs, and expose as public API.

Deprecate older constants in CarrierConfigManager, and point users
towards new API.  (The goal of CarrierConfigManager is to express
key/value pairs that are identical for all users on a particular
carrier, not to configure details that vary between users.)

Since a CarrierService may itself not have access to the plan
details, allow them to "delegate" the capability to configure plans
to another package name.

Factor out new RecurrenceRule class which is used by both
NetworkPolicy and SubscriptionPlan internally.  Polish it to support
arbitrary periods, and continue to test it through NPMST.

Bug: 63391323
Test: bit FrameworksCoreTests:android.util.RecurrenceRuleTest
Test: bit FrameworksServicesTests:com.android.server.NetworkPolicyManagerServiceTest
Exempt-From-Owner-Approval: Bug 63673347
Change-Id: I93fae4a43e58e9e4c32f8bf345a257dcd607f088

17 files changed:
api/current.txt
api/system-current.txt
api/test-current.txt
core/java/android/net/INetworkPolicyManager.aidl
core/java/android/net/NetworkPolicy.java
core/java/android/net/NetworkPolicyManager.java
core/java/android/util/RecurrenceRule.java [new file with mode: 0644]
core/java/com/android/internal/util/XmlUtils.java
core/res/AndroidManifest.xml
core/tests/coretests/src/android/util/RecurrenceRuleTest.java [new file with mode: 0644]
packages/SettingsLib/src/com/android/settingslib/NetworkPolicyEditor.java
services/core/java/com/android/server/net/NetworkPolicyManagerService.java
services/tests/servicestests/src/com/android/server/NetworkPolicyManagerServiceTest.java
telephony/java/android/telephony/CarrierConfigManager.java
telephony/java/android/telephony/SubscriptionManager.java
telephony/java/android/telephony/SubscriptionPlan.java
telephony/java/com/android/internal/telephony/ICarrierConfigLoader.aidl

index e149797..71f7c4a 100644 (file)
@@ -39535,7 +39535,7 @@ package android.telephony {
     method public android.os.PersistableBundle getConfigForSubId(int);
     method public void notifyConfigChangedForSubId(int);
     field public static final java.lang.String ACTION_CARRIER_CONFIG_CHANGED = "android.telephony.action.CARRIER_CONFIG_CHANGED";
-    field public static final int DATA_CYCLE_THRESHOLD_DISABLED = -2; // 0xfffffffe
+    field public static final deprecated int DATA_CYCLE_THRESHOLD_DISABLED = -2; // 0xfffffffe
     field public static final java.lang.String KEY_ADDITIONAL_CALL_SETTING_BOOL = "additional_call_setting_bool";
     field public static final java.lang.String KEY_ALLOW_ADDING_APNS_BOOL = "allow_adding_apns_bool";
     field public static final java.lang.String KEY_ALLOW_ADD_CALL_DURING_VIDEO_CALL_BOOL = "allow_add_call_during_video_call";
@@ -39577,9 +39577,10 @@ package android.telephony {
     field public static final java.lang.String KEY_CI_ACTION_ON_SYS_UPDATE_EXTRA_VAL_STRING = "ci_action_on_sys_update_extra_val_string";
     field public static final java.lang.String KEY_CI_ACTION_ON_SYS_UPDATE_INTENT_STRING = "ci_action_on_sys_update_intent_string";
     field public static final java.lang.String KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING = "config_ims_package_override_string";
+    field public static final java.lang.String KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING = "config_plans_package_override_string";
     field public static final java.lang.String KEY_CSP_ENABLED_BOOL = "csp_enabled_bool";
-    field public static final java.lang.String KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG = "data_limit_threshold_bytes_long";
-    field public static final java.lang.String KEY_DATA_WARNING_THRESHOLD_BYTES_LONG = "data_warning_threshold_bytes_long";
+    field public static final deprecated java.lang.String KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG = "data_limit_threshold_bytes_long";
+    field public static final deprecated java.lang.String KEY_DATA_WARNING_THRESHOLD_BYTES_LONG = "data_warning_threshold_bytes_long";
     field public static final java.lang.String KEY_DEFAULT_SIM_CALL_MANAGER_STRING = "default_sim_call_manager_string";
     field public static final java.lang.String KEY_DEFAULT_VM_NUMBER_STRING = "default_vm_number_string";
     field public static final java.lang.String KEY_DIAL_STRING_REPLACE_STRING_ARRAY = "dial_string_replace_string_array";
@@ -39636,7 +39637,7 @@ package android.telephony {
     field public static final java.lang.String KEY_MMS_UA_PROF_TAG_NAME_STRING = "uaProfTagName";
     field public static final java.lang.String KEY_MMS_UA_PROF_URL_STRING = "uaProfUrl";
     field public static final java.lang.String KEY_MMS_USER_AGENT_STRING = "userAgent";
-    field public static final java.lang.String KEY_MONTHLY_DATA_CYCLE_DAY_INT = "monthly_data_cycle_day_int";
+    field public static final deprecated java.lang.String KEY_MONTHLY_DATA_CYCLE_DAY_INT = "monthly_data_cycle_day_int";
     field public static final java.lang.String KEY_ONLY_SINGLE_DC_ALLOWED_INT_ARRAY = "only_single_dc_allowed_int_array";
     field public static final java.lang.String KEY_OPERATOR_SELECTION_EXPAND_BOOL = "operator_selection_expand_bool";
     field public static final java.lang.String KEY_PREFER_2G_BOOL = "prefer_2g_bool";
@@ -40148,8 +40149,10 @@ package android.telephony {
     method public static int getDefaultSmsSubscriptionId();
     method public static int getDefaultSubscriptionId();
     method public static int getDefaultVoiceSubscriptionId();
+    method public java.util.List<android.telephony.SubscriptionPlan> getSubscriptionPlans(int);
     method public boolean isNetworkRoaming(int);
     method public void removeOnSubscriptionsChangedListener(android.telephony.SubscriptionManager.OnSubscriptionsChangedListener);
+    method public void setSubscriptionPlans(int, java.util.List<android.telephony.SubscriptionPlan>);
     field public static final java.lang.String ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED = "android.telephony.action.DEFAULT_SMS_SUBSCRIPTION_CHANGED";
     field public static final java.lang.String ACTION_DEFAULT_SUBSCRIPTION_CHANGED = "android.telephony.action.DEFAULT_SUBSCRIPTION_CHANGED";
     field public static final int DATA_ROAMING_DISABLE = 0; // 0x0
@@ -40163,6 +40166,38 @@ package android.telephony {
     method public void onSubscriptionsChanged();
   }
 
+  public final class SubscriptionPlan implements android.os.Parcelable {
+    method public java.util.Iterator<android.util.Pair<java.time.ZonedDateTime, java.time.ZonedDateTime>> cycleIterator();
+    method public int describeContents();
+    method public int getDataLimitBehavior();
+    method public long getDataLimitBytes();
+    method public long getDataUsageBytes();
+    method public long getDataUsageTime();
+    method public java.lang.CharSequence getSummary();
+    method public java.lang.CharSequence getTitle();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final long BYTES_UNKNOWN = -1L; // 0xffffffffffffffffL
+    field public static final long BYTES_UNLIMITED = 9223372036854775807L; // 0x7fffffffffffffffL
+    field public static final android.os.Parcelable.Creator<android.telephony.SubscriptionPlan> CREATOR;
+    field public static final int LIMIT_BEHAVIOR_BILLED = 1; // 0x1
+    field public static final int LIMIT_BEHAVIOR_DISABLED = 0; // 0x0
+    field public static final int LIMIT_BEHAVIOR_THROTTLED = 2; // 0x2
+    field public static final int LIMIT_BEHAVIOR_UNKNOWN = -1; // 0xffffffff
+    field public static final long TIME_UNKNOWN = -1L; // 0xffffffffffffffffL
+  }
+
+  public static class SubscriptionPlan.Builder {
+    method public android.telephony.SubscriptionPlan build();
+    method public static android.telephony.SubscriptionPlan.Builder createNonrecurring(java.time.ZonedDateTime, java.time.ZonedDateTime);
+    method public static android.telephony.SubscriptionPlan.Builder createRecurringDaily(java.time.ZonedDateTime);
+    method public static android.telephony.SubscriptionPlan.Builder createRecurringMonthly(java.time.ZonedDateTime);
+    method public static android.telephony.SubscriptionPlan.Builder createRecurringWeekly(java.time.ZonedDateTime);
+    method public android.telephony.SubscriptionPlan.Builder setDataLimit(long, int);
+    method public android.telephony.SubscriptionPlan.Builder setDataUsage(long, long);
+    method public android.telephony.SubscriptionPlan.Builder setSummary(java.lang.CharSequence);
+    method public android.telephony.SubscriptionPlan.Builder setTitle(java.lang.CharSequence);
+  }
+
   public class TelephonyManager {
     method public boolean canChangeDtmfToneLength();
     method public android.telephony.TelephonyManager createForPhoneAccountHandle(android.telecom.PhoneAccountHandle);
index f7a576e..a78fb0d 100644 (file)
@@ -146,7 +146,6 @@ 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";
@@ -43082,7 +43081,7 @@ package android.telephony {
     method public void notifyConfigChangedForSubId(int);
     method public void updateConfigForPhoneId(int, java.lang.String);
     field public static final java.lang.String ACTION_CARRIER_CONFIG_CHANGED = "android.telephony.action.CARRIER_CONFIG_CHANGED";
-    field public static final int DATA_CYCLE_THRESHOLD_DISABLED = -2; // 0xfffffffe
+    field public static final deprecated int DATA_CYCLE_THRESHOLD_DISABLED = -2; // 0xfffffffe
     field public static final java.lang.String KEY_ADDITIONAL_CALL_SETTING_BOOL = "additional_call_setting_bool";
     field public static final java.lang.String KEY_ALLOW_ADDING_APNS_BOOL = "allow_adding_apns_bool";
     field public static final java.lang.String KEY_ALLOW_ADD_CALL_DURING_VIDEO_CALL_BOOL = "allow_add_call_during_video_call";
@@ -43124,9 +43123,10 @@ package android.telephony {
     field public static final java.lang.String KEY_CI_ACTION_ON_SYS_UPDATE_EXTRA_VAL_STRING = "ci_action_on_sys_update_extra_val_string";
     field public static final java.lang.String KEY_CI_ACTION_ON_SYS_UPDATE_INTENT_STRING = "ci_action_on_sys_update_intent_string";
     field public static final java.lang.String KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING = "config_ims_package_override_string";
+    field public static final java.lang.String KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING = "config_plans_package_override_string";
     field public static final java.lang.String KEY_CSP_ENABLED_BOOL = "csp_enabled_bool";
-    field public static final java.lang.String KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG = "data_limit_threshold_bytes_long";
-    field public static final java.lang.String KEY_DATA_WARNING_THRESHOLD_BYTES_LONG = "data_warning_threshold_bytes_long";
+    field public static final deprecated java.lang.String KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG = "data_limit_threshold_bytes_long";
+    field public static final deprecated java.lang.String KEY_DATA_WARNING_THRESHOLD_BYTES_LONG = "data_warning_threshold_bytes_long";
     field public static final java.lang.String KEY_DEFAULT_SIM_CALL_MANAGER_STRING = "default_sim_call_manager_string";
     field public static final java.lang.String KEY_DEFAULT_VM_NUMBER_STRING = "default_vm_number_string";
     field public static final java.lang.String KEY_DIAL_STRING_REPLACE_STRING_ARRAY = "dial_string_replace_string_array";
@@ -43183,7 +43183,7 @@ package android.telephony {
     field public static final java.lang.String KEY_MMS_UA_PROF_TAG_NAME_STRING = "uaProfTagName";
     field public static final java.lang.String KEY_MMS_UA_PROF_URL_STRING = "uaProfUrl";
     field public static final java.lang.String KEY_MMS_USER_AGENT_STRING = "userAgent";
-    field public static final java.lang.String KEY_MONTHLY_DATA_CYCLE_DAY_INT = "monthly_data_cycle_day_int";
+    field public static final deprecated java.lang.String KEY_MONTHLY_DATA_CYCLE_DAY_INT = "monthly_data_cycle_day_int";
     field public static final java.lang.String KEY_ONLY_SINGLE_DC_ALLOWED_INT_ARRAY = "only_single_dc_allowed_int_array";
     field public static final java.lang.String KEY_OPERATOR_SELECTION_EXPAND_BOOL = "operator_selection_expand_bool";
     field public static final java.lang.String KEY_PREFER_2G_BOOL = "prefer_2g_bool";
@@ -43697,8 +43697,10 @@ package android.telephony {
     method public static int getDefaultSmsSubscriptionId();
     method public static int getDefaultSubscriptionId();
     method public static int getDefaultVoiceSubscriptionId();
+    method public java.util.List<android.telephony.SubscriptionPlan> getSubscriptionPlans(int);
     method public boolean isNetworkRoaming(int);
     method public void removeOnSubscriptionsChangedListener(android.telephony.SubscriptionManager.OnSubscriptionsChangedListener);
+    method public void setSubscriptionPlans(int, java.util.List<android.telephony.SubscriptionPlan>);
     field public static final java.lang.String ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED = "android.telephony.action.DEFAULT_SMS_SUBSCRIPTION_CHANGED";
     field public static final java.lang.String ACTION_DEFAULT_SUBSCRIPTION_CHANGED = "android.telephony.action.DEFAULT_SUBSCRIPTION_CHANGED";
     field public static final int DATA_ROAMING_DISABLE = 0; // 0x0
@@ -43712,6 +43714,38 @@ package android.telephony {
     method public void onSubscriptionsChanged();
   }
 
+  public final class SubscriptionPlan implements android.os.Parcelable {
+    method public java.util.Iterator<android.util.Pair<java.time.ZonedDateTime, java.time.ZonedDateTime>> cycleIterator();
+    method public int describeContents();
+    method public int getDataLimitBehavior();
+    method public long getDataLimitBytes();
+    method public long getDataUsageBytes();
+    method public long getDataUsageTime();
+    method public java.lang.CharSequence getSummary();
+    method public java.lang.CharSequence getTitle();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final long BYTES_UNKNOWN = -1L; // 0xffffffffffffffffL
+    field public static final long BYTES_UNLIMITED = 9223372036854775807L; // 0x7fffffffffffffffL
+    field public static final android.os.Parcelable.Creator<android.telephony.SubscriptionPlan> CREATOR;
+    field public static final int LIMIT_BEHAVIOR_BILLED = 1; // 0x1
+    field public static final int LIMIT_BEHAVIOR_DISABLED = 0; // 0x0
+    field public static final int LIMIT_BEHAVIOR_THROTTLED = 2; // 0x2
+    field public static final int LIMIT_BEHAVIOR_UNKNOWN = -1; // 0xffffffff
+    field public static final long TIME_UNKNOWN = -1L; // 0xffffffffffffffffL
+  }
+
+  public static class SubscriptionPlan.Builder {
+    method public android.telephony.SubscriptionPlan build();
+    method public static android.telephony.SubscriptionPlan.Builder createNonrecurring(java.time.ZonedDateTime, java.time.ZonedDateTime);
+    method public static android.telephony.SubscriptionPlan.Builder createRecurringDaily(java.time.ZonedDateTime);
+    method public static android.telephony.SubscriptionPlan.Builder createRecurringMonthly(java.time.ZonedDateTime);
+    method public static android.telephony.SubscriptionPlan.Builder createRecurringWeekly(java.time.ZonedDateTime);
+    method public android.telephony.SubscriptionPlan.Builder setDataLimit(long, int);
+    method public android.telephony.SubscriptionPlan.Builder setDataUsage(long, long);
+    method public android.telephony.SubscriptionPlan.Builder setSummary(java.lang.CharSequence);
+    method public android.telephony.SubscriptionPlan.Builder setTitle(java.lang.CharSequence);
+  }
+
   public final class TelephonyHistogram implements android.os.Parcelable {
     ctor public TelephonyHistogram(int, int, int);
     ctor public TelephonyHistogram(android.telephony.TelephonyHistogram);
index bd93e1b..e8f0505 100644 (file)
@@ -39761,7 +39761,7 @@ package android.telephony {
     method public android.os.PersistableBundle getConfigForSubId(int);
     method public void notifyConfigChangedForSubId(int);
     field public static final java.lang.String ACTION_CARRIER_CONFIG_CHANGED = "android.telephony.action.CARRIER_CONFIG_CHANGED";
-    field public static final int DATA_CYCLE_THRESHOLD_DISABLED = -2; // 0xfffffffe
+    field public static final deprecated int DATA_CYCLE_THRESHOLD_DISABLED = -2; // 0xfffffffe
     field public static final java.lang.String KEY_ADDITIONAL_CALL_SETTING_BOOL = "additional_call_setting_bool";
     field public static final java.lang.String KEY_ALLOW_ADDING_APNS_BOOL = "allow_adding_apns_bool";
     field public static final java.lang.String KEY_ALLOW_ADD_CALL_DURING_VIDEO_CALL_BOOL = "allow_add_call_during_video_call";
@@ -39803,9 +39803,10 @@ package android.telephony {
     field public static final java.lang.String KEY_CI_ACTION_ON_SYS_UPDATE_EXTRA_VAL_STRING = "ci_action_on_sys_update_extra_val_string";
     field public static final java.lang.String KEY_CI_ACTION_ON_SYS_UPDATE_INTENT_STRING = "ci_action_on_sys_update_intent_string";
     field public static final java.lang.String KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING = "config_ims_package_override_string";
+    field public static final java.lang.String KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING = "config_plans_package_override_string";
     field public static final java.lang.String KEY_CSP_ENABLED_BOOL = "csp_enabled_bool";
-    field public static final java.lang.String KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG = "data_limit_threshold_bytes_long";
-    field public static final java.lang.String KEY_DATA_WARNING_THRESHOLD_BYTES_LONG = "data_warning_threshold_bytes_long";
+    field public static final deprecated java.lang.String KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG = "data_limit_threshold_bytes_long";
+    field public static final deprecated java.lang.String KEY_DATA_WARNING_THRESHOLD_BYTES_LONG = "data_warning_threshold_bytes_long";
     field public static final java.lang.String KEY_DEFAULT_SIM_CALL_MANAGER_STRING = "default_sim_call_manager_string";
     field public static final java.lang.String KEY_DEFAULT_VM_NUMBER_STRING = "default_vm_number_string";
     field public static final java.lang.String KEY_DIAL_STRING_REPLACE_STRING_ARRAY = "dial_string_replace_string_array";
@@ -39862,7 +39863,7 @@ package android.telephony {
     field public static final java.lang.String KEY_MMS_UA_PROF_TAG_NAME_STRING = "uaProfTagName";
     field public static final java.lang.String KEY_MMS_UA_PROF_URL_STRING = "uaProfUrl";
     field public static final java.lang.String KEY_MMS_USER_AGENT_STRING = "userAgent";
-    field public static final java.lang.String KEY_MONTHLY_DATA_CYCLE_DAY_INT = "monthly_data_cycle_day_int";
+    field public static final deprecated java.lang.String KEY_MONTHLY_DATA_CYCLE_DAY_INT = "monthly_data_cycle_day_int";
     field public static final java.lang.String KEY_ONLY_SINGLE_DC_ALLOWED_INT_ARRAY = "only_single_dc_allowed_int_array";
     field public static final java.lang.String KEY_OPERATOR_SELECTION_EXPAND_BOOL = "operator_selection_expand_bool";
     field public static final java.lang.String KEY_PREFER_2G_BOOL = "prefer_2g_bool";
@@ -40374,8 +40375,10 @@ package android.telephony {
     method public static int getDefaultSmsSubscriptionId();
     method public static int getDefaultSubscriptionId();
     method public static int getDefaultVoiceSubscriptionId();
+    method public java.util.List<android.telephony.SubscriptionPlan> getSubscriptionPlans(int);
     method public boolean isNetworkRoaming(int);
     method public void removeOnSubscriptionsChangedListener(android.telephony.SubscriptionManager.OnSubscriptionsChangedListener);
+    method public void setSubscriptionPlans(int, java.util.List<android.telephony.SubscriptionPlan>);
     field public static final java.lang.String ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED = "android.telephony.action.DEFAULT_SMS_SUBSCRIPTION_CHANGED";
     field public static final java.lang.String ACTION_DEFAULT_SUBSCRIPTION_CHANGED = "android.telephony.action.DEFAULT_SUBSCRIPTION_CHANGED";
     field public static final int DATA_ROAMING_DISABLE = 0; // 0x0
@@ -40389,6 +40392,38 @@ package android.telephony {
     method public void onSubscriptionsChanged();
   }
 
+  public final class SubscriptionPlan implements android.os.Parcelable {
+    method public java.util.Iterator<android.util.Pair<java.time.ZonedDateTime, java.time.ZonedDateTime>> cycleIterator();
+    method public int describeContents();
+    method public int getDataLimitBehavior();
+    method public long getDataLimitBytes();
+    method public long getDataUsageBytes();
+    method public long getDataUsageTime();
+    method public java.lang.CharSequence getSummary();
+    method public java.lang.CharSequence getTitle();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final long BYTES_UNKNOWN = -1L; // 0xffffffffffffffffL
+    field public static final long BYTES_UNLIMITED = 9223372036854775807L; // 0x7fffffffffffffffL
+    field public static final android.os.Parcelable.Creator<android.telephony.SubscriptionPlan> CREATOR;
+    field public static final int LIMIT_BEHAVIOR_BILLED = 1; // 0x1
+    field public static final int LIMIT_BEHAVIOR_DISABLED = 0; // 0x0
+    field public static final int LIMIT_BEHAVIOR_THROTTLED = 2; // 0x2
+    field public static final int LIMIT_BEHAVIOR_UNKNOWN = -1; // 0xffffffff
+    field public static final long TIME_UNKNOWN = -1L; // 0xffffffffffffffffL
+  }
+
+  public static class SubscriptionPlan.Builder {
+    method public android.telephony.SubscriptionPlan build();
+    method public static android.telephony.SubscriptionPlan.Builder createNonrecurring(java.time.ZonedDateTime, java.time.ZonedDateTime);
+    method public static android.telephony.SubscriptionPlan.Builder createRecurringDaily(java.time.ZonedDateTime);
+    method public static android.telephony.SubscriptionPlan.Builder createRecurringMonthly(java.time.ZonedDateTime);
+    method public static android.telephony.SubscriptionPlan.Builder createRecurringWeekly(java.time.ZonedDateTime);
+    method public android.telephony.SubscriptionPlan.Builder setDataLimit(long, int);
+    method public android.telephony.SubscriptionPlan.Builder setDataUsage(long, long);
+    method public android.telephony.SubscriptionPlan.Builder setSummary(java.lang.CharSequence);
+    method public android.telephony.SubscriptionPlan.Builder setTitle(java.lang.CharSequence);
+  }
+
   public class TelephonyManager {
     method public boolean canChangeDtmfToneLength();
     method public android.telephony.TelephonyManager createForPhoneAccountHandle(android.telecom.PhoneAccountHandle);
index 7b948a7..181e4a2 100644 (file)
@@ -71,7 +71,5 @@ interface INetworkPolicyManager {
     SubscriptionPlan[] getSubscriptionPlans(int subId, String callingPackage);
     void setSubscriptionPlans(int subId, in SubscriptionPlan[] plans, String callingPackage);
 
-    String getSubscriptionPlanOwner(int subId);
-
     void factoryReset(String subscriber);
 }
index edf9a28..5df742c 100644 (file)
 
 package android.net;
 
-import static com.android.internal.util.Preconditions.checkNotNull;
-
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.BackupUtils;
+import android.util.Pair;
+import android.util.RecurrenceRule;
+
+import com.android.internal.util.Preconditions;
 
 import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Iterator;
 import java.util.Objects;
 
 /**
@@ -35,10 +40,8 @@ import java.util.Objects;
  * @hide
  */
 public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
-    /**
-     * Current Version of the Backup Serializer.
-     */
-    private static final int BACKUP_VERSION = 1;
+    private static final int VERSION_INIT = 1;
+    private static final int VERSION_RULE = 2;
 
     public static final int CYCLE_NONE = -1;
     public static final long WARNING_DISABLED = -1;
@@ -46,8 +49,7 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
     public static final long SNOOZE_NEVER = -1;
 
     public NetworkTemplate template;
-    @Deprecated public int cycleDay = CYCLE_NONE;
-    @Deprecated public String cycleTimezone = "UTC";
+    public RecurrenceRule cycleRule;
     public long warningBytes = WARNING_DISABLED;
     public long limitBytes = LIMIT_DISABLED;
     public long lastWarningSnooze = SNOOZE_NEVER;
@@ -57,7 +59,12 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
 
     private static final long DEFAULT_MTU = 1500;
 
-    public NetworkPolicy() {
+    public static RecurrenceRule buildRule(int cycleDay, ZoneId cycleTimezone) {
+        if (cycleDay != NetworkPolicy.CYCLE_NONE) {
+            return RecurrenceRule.buildRecurringMonthly(cycleDay, cycleTimezone);
+        } else {
+            return RecurrenceRule.buildNever();
+        }
     }
 
     @Deprecated
@@ -67,12 +74,19 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
                 SNOOZE_NEVER, metered, false);
     }
 
+    @Deprecated
     public NetworkPolicy(NetworkTemplate template, int cycleDay, String cycleTimezone,
             long warningBytes, long limitBytes, long lastWarningSnooze, long lastLimitSnooze,
             boolean metered, boolean inferred) {
-        this.template = checkNotNull(template, "missing NetworkTemplate");
-        this.cycleDay = cycleDay;
-        this.cycleTimezone = checkNotNull(cycleTimezone, "missing cycleTimezone");
+        this(template, buildRule(cycleDay, ZoneId.of(cycleTimezone)), warningBytes,
+                limitBytes, lastWarningSnooze, lastLimitSnooze, metered, inferred);
+    }
+
+    public NetworkPolicy(NetworkTemplate template, RecurrenceRule cycleRule, long warningBytes,
+            long limitBytes, long lastWarningSnooze, long lastLimitSnooze, boolean metered,
+            boolean inferred) {
+        this.template = Preconditions.checkNotNull(template, "missing NetworkTemplate");
+        this.cycleRule = Preconditions.checkNotNull(cycleRule, "missing RecurrenceRule");
         this.warningBytes = warningBytes;
         this.limitBytes = limitBytes;
         this.lastWarningSnooze = lastWarningSnooze;
@@ -81,23 +95,21 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
         this.inferred = inferred;
     }
 
-    public NetworkPolicy(Parcel in) {
-        template = in.readParcelable(null);
-        cycleDay = in.readInt();
-        cycleTimezone = in.readString();
-        warningBytes = in.readLong();
-        limitBytes = in.readLong();
-        lastWarningSnooze = in.readLong();
-        lastLimitSnooze = in.readLong();
-        metered = in.readInt() != 0;
-        inferred = in.readInt() != 0;
+    private NetworkPolicy(Parcel source) {
+        template = source.readParcelable(null);
+        cycleRule = source.readParcelable(null);
+        warningBytes = source.readLong();
+        limitBytes = source.readLong();
+        lastWarningSnooze = source.readLong();
+        lastLimitSnooze = source.readLong();
+        metered = source.readInt() != 0;
+        inferred = source.readInt() != 0;
     }
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeParcelable(template, flags);
-        dest.writeInt(cycleDay);
-        dest.writeString(cycleTimezone);
+        dest.writeParcelable(cycleRule, flags);
         dest.writeLong(warningBytes);
         dest.writeLong(limitBytes);
         dest.writeLong(lastWarningSnooze);
@@ -111,6 +123,10 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
         return 0;
     }
 
+    public Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator() {
+        return cycleRule.cycleIterator();
+    }
+
     /**
      * Test if given measurement is over {@link #warningBytes}.
      */
@@ -141,7 +157,7 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
      * Test if this policy has a cycle defined, after which usage should reset.
      */
     public boolean hasCycle() {
-        return cycleDay != CYCLE_NONE;
+        return cycleRule.cycleIterator().hasNext();
     }
 
     @Override
@@ -159,7 +175,7 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
 
     @Override
     public int hashCode() {
-        return Objects.hash(template, cycleDay, cycleTimezone, warningBytes, limitBytes,
+        return Objects.hash(template, cycleRule, warningBytes, limitBytes,
                 lastWarningSnooze, lastLimitSnooze, metered, inferred);
     }
 
@@ -167,30 +183,29 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
     public boolean equals(Object obj) {
         if (obj instanceof NetworkPolicy) {
             final NetworkPolicy other = (NetworkPolicy) obj;
-            return cycleDay == other.cycleDay && warningBytes == other.warningBytes
+            return warningBytes == other.warningBytes
                     && limitBytes == other.limitBytes
                     && lastWarningSnooze == other.lastWarningSnooze
                     && lastLimitSnooze == other.lastLimitSnooze && metered == other.metered
                     && inferred == other.inferred
-                    && Objects.equals(cycleTimezone, other.cycleTimezone)
-                    && Objects.equals(template, other.template);
+                    && Objects.equals(template, other.template)
+                    && Objects.equals(cycleRule, other.cycleRule);
         }
         return false;
     }
 
     @Override
     public String toString() {
-        final StringBuilder builder = new StringBuilder("NetworkPolicy");
-        builder.append("[").append(template).append("]:");
-        builder.append(" cycleDay=").append(cycleDay);
-        builder.append(", cycleTimezone=").append(cycleTimezone);
-        builder.append(", warningBytes=").append(warningBytes);
-        builder.append(", limitBytes=").append(limitBytes);
-        builder.append(", lastWarningSnooze=").append(lastWarningSnooze);
-        builder.append(", lastLimitSnooze=").append(lastLimitSnooze);
-        builder.append(", metered=").append(metered);
-        builder.append(", inferred=").append(inferred);
-        return builder.toString();
+        return new StringBuilder("NetworkPolicy{")
+                .append("template=").append(template)
+                .append(" cycleRule=").append(cycleRule)
+                .append(" warningBytes=").append(warningBytes)
+                .append(" limitBytes=").append(limitBytes)
+                .append(" lastWarningSnooze=").append(lastWarningSnooze)
+                .append(" lastLimitSnooze=").append(lastLimitSnooze)
+                .append(" metered=").append(metered)
+                .append(" inferred=").append(inferred)
+                .append("}").toString();
     }
 
     public static final Creator<NetworkPolicy> CREATOR = new Creator<NetworkPolicy>() {
@@ -209,10 +224,9 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         DataOutputStream out = new DataOutputStream(baos);
 
-        out.writeInt(BACKUP_VERSION);
+        out.writeInt(VERSION_RULE);
         out.write(template.getBytesForBackup());
-        out.writeInt(cycleDay);
-        BackupUtils.writeString(out, cycleTimezone);
+        cycleRule.writeToStream(out);
         out.writeLong(warningBytes);
         out.writeLong(limitBytes);
         out.writeLong(lastWarningSnooze);
@@ -224,21 +238,36 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
 
     public static NetworkPolicy getNetworkPolicyFromBackup(DataInputStream in) throws IOException,
             BackupUtils.BadVersionException {
-        int version = in.readInt();
-        if (version < 1 || version > BACKUP_VERSION) {
-            throw new BackupUtils.BadVersionException("Unknown Backup Serialization Version");
+        final int version = in.readInt();
+        switch (version) {
+            case VERSION_INIT: {
+                NetworkTemplate template = NetworkTemplate.getNetworkTemplateFromBackup(in);
+                int cycleDay = in.readInt();
+                String cycleTimeZone = BackupUtils.readString(in);
+                long warningBytes = in.readLong();
+                long limitBytes = in.readLong();
+                long lastWarningSnooze = in.readLong();
+                long lastLimitSnooze = in.readLong();
+                boolean metered = in.readInt() == 1;
+                boolean inferred = in.readInt() == 1;
+                return new NetworkPolicy(template, cycleDay, cycleTimeZone, warningBytes,
+                        limitBytes, lastWarningSnooze, lastLimitSnooze, metered, inferred);
+            }
+            case VERSION_RULE: {
+                NetworkTemplate template = NetworkTemplate.getNetworkTemplateFromBackup(in);
+                RecurrenceRule cycleRule = new RecurrenceRule(in);
+                long warningBytes = in.readLong();
+                long limitBytes = in.readLong();
+                long lastWarningSnooze = in.readLong();
+                long lastLimitSnooze = in.readLong();
+                boolean metered = in.readInt() == 1;
+                boolean inferred = in.readInt() == 1;
+                return new NetworkPolicy(template, cycleRule, warningBytes,
+                        limitBytes, lastWarningSnooze, lastLimitSnooze, metered, inferred);
+            }
+            default: {
+                throw new BackupUtils.BadVersionException("Unknown backup version: " + version);
+            }
         }
-
-        NetworkTemplate template = NetworkTemplate.getNetworkTemplateFromBackup(in);
-        int cycleDay = in.readInt();
-        String cycleTimeZone = BackupUtils.readString(in);
-        long warningBytes = in.readLong();
-        long limitBytes = in.readLong();
-        long lastWarningSnooze = in.readLong();
-        long lastLimitSnooze = in.readLong();
-        boolean metered = in.readInt() == 1;
-        boolean inferred = in.readInt() == 1;
-        return new NetworkPolicy(template, cycleDay, cycleTimeZone, warningBytes, limitBytes,
-                lastWarningSnooze, lastLimitSnooze, metered, inferred);
     }
 }
index 3fe9b0d..81c49a3 100644 (file)
@@ -254,7 +254,7 @@ public class NetworkPolicyManager {
 
     /** {@hide} */
     public static Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator(NetworkPolicy policy) {
-        return SubscriptionPlan.convert(policy).cycleIterator();
+        return policy.cycleIterator();
     }
 
     /**
diff --git a/core/java/android/util/RecurrenceRule.java b/core/java/android/util/RecurrenceRule.java
new file mode 100644 (file)
index 0000000..1fe638d
--- /dev/null
@@ -0,0 +1,257 @@
+/*
+ * 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.util;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.time.Clock;
+import java.time.LocalTime;
+import java.time.Period;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Iterator;
+import java.util.Objects;
+
+/**
+ * Description of an event that should recur over time at a specific interval
+ * between two anchor points in time.
+ *
+ * @hide
+ */
+public class RecurrenceRule implements Parcelable {
+    private static final String TAG = "RecurrenceRule";
+    private static final boolean DEBUG = true;
+
+    private static final int VERSION_INIT = 0;
+
+    /** {@hide} */
+    @VisibleForTesting
+    public static Clock sClock = Clock.systemDefaultZone();
+
+    public final ZonedDateTime start;
+    public final ZonedDateTime end;
+    public final Period period;
+
+    public RecurrenceRule(ZonedDateTime start, ZonedDateTime end, Period period) {
+        this.start = start;
+        this.end = end;
+        this.period = period;
+    }
+
+    @Deprecated
+    public static RecurrenceRule buildNever() {
+        return new RecurrenceRule(null, null, null);
+    }
+
+    @Deprecated
+    public static RecurrenceRule buildRecurringMonthly(int dayOfMonth, ZoneId zone) {
+        // Assume we started last January, since it has all possible days
+        final ZonedDateTime now = ZonedDateTime.now(sClock).withZoneSameInstant(zone);
+        final ZonedDateTime start = ZonedDateTime.of(
+                now.toLocalDate().minusYears(1).withMonth(1).withDayOfMonth(dayOfMonth),
+                LocalTime.MIDNIGHT, zone);
+        return new RecurrenceRule(start, null, Period.ofMonths(1));
+    }
+
+    private RecurrenceRule(Parcel source) {
+        start = convertZonedDateTime(source.readString());
+        end = convertZonedDateTime(source.readString());
+        period = convertPeriod(source.readString());
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(convertZonedDateTime(start));
+        dest.writeString(convertZonedDateTime(end));
+        dest.writeString(convertPeriod(period));
+    }
+
+    public RecurrenceRule(DataInputStream in) throws IOException {
+        final int version = in.readInt();
+        switch (version) {
+            case VERSION_INIT:
+                start = convertZonedDateTime(BackupUtils.readString(in));
+                end = convertZonedDateTime(BackupUtils.readString(in));
+                period = convertPeriod(BackupUtils.readString(in));
+            default:
+                throw new ProtocolException("Unknown version " + version);
+        }
+    }
+
+    public void writeToStream(DataOutputStream out) throws IOException {
+        out.writeInt(VERSION_INIT);
+        BackupUtils.writeString(out, convertZonedDateTime(start));
+        BackupUtils.writeString(out, convertZonedDateTime(end));
+        BackupUtils.writeString(out, convertPeriod(period));
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder("RecurrenceRule{")
+                .append("start=").append(start)
+                .append(" end=").append(end)
+                .append(" period=").append(period)
+                .append("}").toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(start, end, period);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof RecurrenceRule) {
+            final RecurrenceRule other = (RecurrenceRule) obj;
+            return Objects.equals(start, other.start)
+                    && Objects.equals(end, other.end)
+                    && Objects.equals(period, other.period);
+        }
+        return false;
+    }
+
+    public static final Parcelable.Creator<RecurrenceRule> CREATOR = new Parcelable.Creator<RecurrenceRule>() {
+        @Override
+        public RecurrenceRule createFromParcel(Parcel source) {
+            return new RecurrenceRule(source);
+        }
+
+        @Override
+        public RecurrenceRule[] newArray(int size) {
+            return new RecurrenceRule[size];
+        }
+    };
+
+    @Deprecated
+    public boolean isMonthly() {
+        return start != null
+                && period != null
+                && period.getYears() == 0
+                && period.getMonths() == 1
+                && period.getDays() == 0;
+    }
+
+    public Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator() {
+        if (period != null) {
+            return new RecurringIterator();
+        } else {
+            return new NonrecurringIterator();
+        }
+    }
+
+    private class NonrecurringIterator implements Iterator<Pair<ZonedDateTime, ZonedDateTime>> {
+        boolean hasNext;
+
+        public NonrecurringIterator() {
+            hasNext = (start != null) && (end != null);
+        }
+
+        @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>> {
+        int i;
+        ZonedDateTime cycleStart;
+        ZonedDateTime cycleEnd;
+
+        public RecurringIterator() {
+            final ZonedDateTime anchor = (end != null) ? end
+                    : ZonedDateTime.now(sClock).withZoneSameInstant(start.getZone());
+            if (DEBUG) Log.d(TAG, "Resolving using anchor " + anchor);
+
+            updateCycle();
+
+            // Walk forwards until we find first cycle after now
+            while (anchor.toEpochSecond() > cycleEnd.toEpochSecond()) {
+                i++;
+                updateCycle();
+            }
+
+            // Walk backwards until we find first cycle before now
+            while (anchor.toEpochSecond() <= cycleStart.toEpochSecond()) {
+                i--;
+                updateCycle();
+            }
+        }
+
+        private void updateCycle() {
+            cycleStart = roundBoundaryTime(start.plus(period.multipliedBy(i)));
+            cycleEnd = roundBoundaryTime(start.plus(period.multipliedBy(i + 1)));
+        }
+
+        private ZonedDateTime roundBoundaryTime(ZonedDateTime boundary) {
+            if (isMonthly() && (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 String convertZonedDateTime(ZonedDateTime time) {
+        return time != null ? time.toString() : null;
+    }
+
+    public static ZonedDateTime convertZonedDateTime(String time) {
+        return time != null ? ZonedDateTime.parse(time) : null;
+    }
+
+    public static String convertPeriod(Period period) {
+        return period != null ? period.toString() : null;
+    }
+
+    public static Period convertPeriod(String period) {
+        return period != null ? Period.parse(period) : null;
+    }
+}
index 3d8c85e..3188d30 100644 (file)
@@ -1694,10 +1694,10 @@ public class XmlUtils {
         return in.getAttributeValue(null, name);
     }
 
-    public static void writeStringAttribute(XmlSerializer out, String name, String value)
+    public static void writeStringAttribute(XmlSerializer out, String name, CharSequence value)
             throws IOException {
         if (value != null) {
-            out.attribute(null, name, value);
+            out.attribute(null, name, value.toString());
         }
     }
 
index c052e51..4a2ef32 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 @hide @deprecated use UPDATE_DEVICE_STATS instead -->
     <permission android:name="android.permission.MODIFY_NETWORK_ACCOUNTING"
         android:protectionLevel="signature|privileged" />
diff --git a/core/tests/coretests/src/android/util/RecurrenceRuleTest.java b/core/tests/coretests/src/android/util/RecurrenceRuleTest.java
new file mode 100644 (file)
index 0000000..42b6048
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * 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.util;
+
+import android.support.test.filters.SmallTest;
+
+import junit.framework.TestCase;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.time.Period;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Iterator;
+
+@SmallTest
+public class RecurrenceRuleTest extends TestCase {
+
+    static Clock sOriginalClock;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        sOriginalClock = RecurrenceRule.sClock;
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        RecurrenceRule.sClock = sOriginalClock;
+    }
+
+    private void setClock(Instant instant) {
+        RecurrenceRule.sClock = Clock.fixed(instant, ZoneId.systemDefault());
+    }
+
+    public void testSimpleMonth() throws Exception {
+        setClock(Instant.parse("2015-11-20T10:15:30.00Z"));
+        final RecurrenceRule r = new RecurrenceRule(
+                ZonedDateTime.parse("2010-11-14T00:00:00.000Z"),
+                null,
+                Period.ofMonths(1));
+
+        assertTrue(r.isMonthly());
+
+        final Iterator<Pair<ZonedDateTime, ZonedDateTime>> it = r.cycleIterator();
+        assertTrue(it.hasNext());
+        assertEquals(Pair.create(
+                ZonedDateTime.parse("2015-11-14T00:00:00.00Z"),
+                ZonedDateTime.parse("2015-12-14T00:00:00.00Z")), it.next());
+        assertTrue(it.hasNext());
+        assertEquals(Pair.create(
+                ZonedDateTime.parse("2015-10-14T00:00:00.00Z"),
+                ZonedDateTime.parse("2015-11-14T00:00:00.00Z")), it.next());
+    }
+
+    public void testSimpleDays() throws Exception {
+        setClock(Instant.parse("2015-01-01T10:15:30.00Z"));
+        final RecurrenceRule r = new RecurrenceRule(
+                ZonedDateTime.parse("2010-11-14T00:11:00.000Z"),
+                ZonedDateTime.parse("2010-11-20T00:11:00.000Z"),
+                Period.ofDays(3));
+
+        assertFalse(r.isMonthly());
+
+        final Iterator<Pair<ZonedDateTime, ZonedDateTime>> it = r.cycleIterator();
+        assertTrue(it.hasNext());
+        assertEquals(Pair.create(
+                ZonedDateTime.parse("2010-11-17T00:11:00.00Z"),
+                ZonedDateTime.parse("2010-11-20T00:11:00.00Z")), it.next());
+        assertTrue(it.hasNext());
+        assertEquals(Pair.create(
+                ZonedDateTime.parse("2010-11-14T00:11:00.00Z"),
+                ZonedDateTime.parse("2010-11-17T00:11:00.00Z")), it.next());
+        assertFalse(it.hasNext());
+    }
+
+    public void testNotRecurring() throws Exception {
+        setClock(Instant.parse("2015-01-01T10:15:30.00Z"));
+        final RecurrenceRule r = new RecurrenceRule(
+                ZonedDateTime.parse("2010-11-14T00:11:00.000Z"),
+                ZonedDateTime.parse("2010-11-20T00:11:00.000Z"),
+                null);
+
+        assertFalse(r.isMonthly());
+
+        final Iterator<Pair<ZonedDateTime, ZonedDateTime>> it = r.cycleIterator();
+        assertTrue(it.hasNext());
+        assertEquals(Pair.create(
+                ZonedDateTime.parse("2010-11-14T00:11:00.000Z"),
+                ZonedDateTime.parse("2010-11-20T00:11:00.000Z")), it.next());
+        assertFalse(it.hasNext());
+    }
+
+    public void testNever() throws Exception {
+        setClock(Instant.parse("2015-01-01T10:15:30.00Z"));
+        final RecurrenceRule r = RecurrenceRule.buildNever();
+
+        assertFalse(r.isMonthly());
+
+        final Iterator<Pair<ZonedDateTime, ZonedDateTime>> it = r.cycleIterator();
+        assertFalse(it.hasNext());
+    }
+
+    public void testSane() throws Exception {
+        final RecurrenceRule r = new RecurrenceRule(
+                ZonedDateTime.parse("1980-01-31T00:00:00.000Z"),
+                ZonedDateTime.parse("2030-01-31T00:00:00.000Z"),
+                Period.ofMonths(1));
+
+        final Iterator<Pair<ZonedDateTime, ZonedDateTime>> it = r.cycleIterator();
+        ZonedDateTime lastStart = null;
+        int months = 0;
+        while (it.hasNext()) {
+            final Pair<ZonedDateTime, ZonedDateTime> cycle = it.next();
+
+            // Make sure cycle has reasonable length
+            final long length = cycle.second.toEpochSecond() - cycle.first.toEpochSecond();
+            assertTrue(cycle + " must be more than 4 weeks", length >= 2419200);
+            assertTrue(cycle + " must be less than 5 weeks", length <= 3024000);
+
+            // Make sure we have no gaps
+            if (lastStart != null) {
+                assertEquals(lastStart, cycle.second);
+            }
+            lastStart = cycle.first;
+            months++;
+        }
+
+        assertEquals(600, months);
+    }
+}
index c346898..b01fc85 100644 (file)
@@ -30,10 +30,12 @@ import android.net.NetworkTemplate;
 import android.net.wifi.WifiInfo;
 import android.os.AsyncTask;
 import android.text.TextUtils;
-import android.text.format.Time;
+import android.util.RecurrenceRule;
 
 import com.google.android.collect.Lists;
 
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.util.ArrayList;
 
 /**
@@ -129,35 +131,36 @@ public class NetworkPolicyEditor {
     @Deprecated
     private static NetworkPolicy buildDefaultPolicy(NetworkTemplate template) {
         // TODO: move this into framework to share with NetworkPolicyManagerService
-        final int cycleDay;
-        final String cycleTimezone;
+        final RecurrenceRule cycleRule;
         final boolean metered;
 
         if (template.getMatchRule() == MATCH_WIFI) {
-            cycleDay = CYCLE_NONE;
-            cycleTimezone = Time.TIMEZONE_UTC;
+            cycleRule = RecurrenceRule.buildNever();
             metered = false;
         } else {
-            final Time time = new Time();
-            time.setToNow();
-            cycleDay = time.monthDay;
-            cycleTimezone = time.timezone;
+            cycleRule = RecurrenceRule.buildRecurringMonthly(ZonedDateTime.now().getDayOfMonth(),
+                    ZoneId.systemDefault());
             metered = true;
         }
 
-        return new NetworkPolicy(template, cycleDay, cycleTimezone, WARNING_DISABLED,
+        return new NetworkPolicy(template, cycleRule, WARNING_DISABLED,
                 LIMIT_DISABLED, SNOOZE_NEVER, SNOOZE_NEVER, metered, true);
     }
 
+    @Deprecated
     public int getPolicyCycleDay(NetworkTemplate template) {
         final NetworkPolicy policy = getPolicy(template);
-        return (policy != null) ? policy.cycleDay : CYCLE_NONE;
+        if (policy != null && policy.cycleRule.isMonthly()) {
+            return policy.cycleRule.start.getDayOfMonth();
+        } else {
+            return CYCLE_NONE;
+        }
     }
 
+    @Deprecated
     public void setPolicyCycleDay(NetworkTemplate template, int cycleDay, String cycleTimezone) {
         final NetworkPolicy policy = getOrCreatePolicy(template);
-        policy.cycleDay = cycleDay;
-        policy.cycleTimezone = cycleTimezone;
+        policy.cycleRule = NetworkPolicy.buildRule(cycleDay, ZoneId.of(cycleTimezone));
         policy.inferred = false;
         policy.clearSnooze();
         writeAsync();
index 38f1c07..f70486a 100644 (file)
@@ -18,7 +18,6 @@ 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;
@@ -28,7 +27,6 @@ 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;
@@ -76,9 +74,11 @@ import static com.android.internal.util.Preconditions.checkNotNull;
 import static com.android.internal.util.XmlUtils.readBooleanAttribute;
 import static com.android.internal.util.XmlUtils.readIntAttribute;
 import static com.android.internal.util.XmlUtils.readLongAttribute;
+import static com.android.internal.util.XmlUtils.readStringAttribute;
 import static com.android.internal.util.XmlUtils.writeBooleanAttribute;
 import static com.android.internal.util.XmlUtils.writeIntAttribute;
 import static com.android.internal.util.XmlUtils.writeLongAttribute;
+import static com.android.internal.util.XmlUtils.writeStringAttribute;
 import static com.android.server.NetworkManagementService.LIMIT_GLOBAL_ALERT;
 import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_UPDATED;
 
@@ -157,14 +157,15 @@ import android.telephony.SubscriptionPlan;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.text.format.Formatter;
-import android.text.format.Time;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Log;
 import android.util.NtpTrustedTime;
 import android.util.Pair;
+import android.util.RecurrenceRule;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 import android.util.TrustedTime;
@@ -180,6 +181,7 @@ import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Preconditions;
 import com.android.server.DeviceIdleController;
 import com.android.server.EventLogTags;
 import com.android.server.LocalServices;
@@ -192,7 +194,6 @@ import libcore.io.IoUtils;
 import com.google.android.collect.Lists;
 
 import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
 import org.xmlpull.v1.XmlSerializer;
 
 import java.io.File;
@@ -205,6 +206,7 @@ import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.nio.charset.StandardCharsets;
+import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -257,7 +259,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
     private static final int VERSION_SWITCH_APP_ID = 8;
     private static final int VERSION_ADDED_NETWORK_ID = 9;
     private static final int VERSION_SWITCH_UID = 10;
-    private static final int VERSION_LATEST = VERSION_SWITCH_UID;
+    private static final int VERSION_ADDED_CYCLE = 11;
+    private static final int VERSION_LATEST = VERSION_ADDED_CYCLE;
 
     /**
      * Max items written to {@link #ProcStateSeqHistory}.
@@ -275,6 +278,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
 
     private static final String TAG_POLICY_LIST = "policy-list";
     private static final String TAG_NETWORK_POLICY = "network-policy";
+    private static final String TAG_SUBSCRIPTION_PLAN = "subscription-plan";
     private static final String TAG_UID_POLICY = "uid-policy";
     private static final String TAG_APP_POLICY = "app-policy";
     private static final String TAG_WHITELIST = "whitelist";
@@ -286,8 +290,11 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
     private static final String ATTR_NETWORK_TEMPLATE = "networkTemplate";
     private static final String ATTR_SUBSCRIBER_ID = "subscriberId";
     private static final String ATTR_NETWORK_ID = "networkId";
-    private static final String ATTR_CYCLE_DAY = "cycleDay";
-    private static final String ATTR_CYCLE_TIMEZONE = "cycleTimezone";
+    @Deprecated private static final String ATTR_CYCLE_DAY = "cycleDay";
+    @Deprecated private static final String ATTR_CYCLE_TIMEZONE = "cycleTimezone";
+    private static final String ATTR_CYCLE_START = "cycleStart";
+    private static final String ATTR_CYCLE_END = "cycleEnd";
+    private static final String ATTR_CYCLE_PERIOD = "cyclePeriod";
     private static final String ATTR_WARNING_BYTES = "warningBytes";
     private static final String ATTR_LIMIT_BYTES = "limitBytes";
     private static final String ATTR_LAST_SNOOZE = "lastSnooze";
@@ -298,6 +305,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
     private static final String ATTR_UID = "uid";
     private static final String ATTR_APP_ID = "appId";
     private static final String ATTR_POLICY = "policy";
+    private static final String ATTR_SUB_ID = "subId";
+    private static final String ATTR_TITLE = "title";
+    private static final String ATTR_SUMMARY = "summary";
+    private static final String ATTR_LIMIT_BEHAVIOR = "limitBehavior";
+    private static final String ATTR_USAGE_BYTES = "usageBytes";
+    private static final String ATTR_USAGE_TIME = "usageTime";
 
     private static final String ACTION_ALLOW_BACKGROUND =
             "com.android.server.net.action.ALLOW_BACKGROUND";
@@ -359,6 +372,9 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
     /** Currently active network rules for ifaces. */
     final ArrayMap<NetworkPolicy, String[]> mNetworkRules = new ArrayMap<>();
 
+    /** Defined subscription plans. */
+    final SparseArray<SubscriptionPlan[]> mSubscriptionPlans = new SparseArray<>();
+
     /** Defined UID policies. */
     @GuardedBy("mUidRulesFirstLock") final SparseIntArray mUidPolicy = new SparseIntArray();
     /** Currently derived rules for each UID. */
@@ -998,7 +1014,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
         // cycle boundary to recompute notifications.
 
         // examine stats for each active policy
-        final long currentTime = currentTimeMillis();
         for (int i = mNetworkPolicy.size()-1; i >= 0; i--) {
             final NetworkPolicy policy = mNetworkPolicy.valueAt(i);
             // ignore policies that aren't relevant to user
@@ -1273,20 +1288,27 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
                     continue;
                 }
 
-                final int cycleDay = getCycleDayFromCarrierConfig(config, policy.cycleDay);
+                final int currentCycleDay;
+                if (policy.cycleRule.isMonthly()) {
+                    currentCycleDay = policy.cycleRule.start.getDayOfMonth();
+                } else {
+                    currentCycleDay = NetworkPolicy.CYCLE_NONE;
+                }
+
+                final int cycleDay = getCycleDayFromCarrierConfig(config, currentCycleDay);
                 final long warningBytes = getWarningBytesFromCarrierConfig(config,
                         policy.warningBytes);
                 final long limitBytes = getLimitBytesFromCarrierConfig(config,
                         policy.limitBytes);
 
-                if (policy.cycleDay == cycleDay &&
+                if (currentCycleDay == cycleDay &&
                         policy.warningBytes == warningBytes &&
                         policy.limitBytes == limitBytes) {
                     continue;
                 }
 
                 policyUpdated = true;
-                policy.cycleDay = cycleDay;
+                policy.cycleRule = NetworkPolicy.buildRule(cycleDay, ZoneId.systemDefault());
                 policy.warningBytes = warningBytes;
                 policy.limitBytes = limitBytes;
 
@@ -1456,7 +1478,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
         // TODO: reset any policy-disabled networks when any policy is removed
         // completely, which is currently rare case.
 
-        final long currentTime = currentTimeMillis();
         for (int i = mNetworkPolicy.size()-1; i >= 0; i--) {
             final NetworkPolicy policy = mNetworkPolicy.valueAt(i);
             // shortcut when policy has no limit
@@ -1573,7 +1594,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
 
         // apply each policy that we found ifaces for; compute remaining data
         // based on current cycle and historical stats, and push to kernel.
-        final long currentTime = currentTimeMillis();
         for (int i = mNetworkRules.size()-1; i >= 0; i--) {
             final NetworkPolicy policy = mNetworkRules.keyAt(i);
             final String[] ifaces = mNetworkRules.valueAt(i);
@@ -1721,20 +1741,16 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
     public NetworkPolicy buildDefaultMobilePolicy(int subId, String subscriberId) {
         PersistableBundle config = mCarrierConfigManager.getConfigForSubId(subId);
 
-        // assume usage cycle starts today
-        final Time time = new Time();
-        time.setToNow();
-
-        final String cycleTimezone = time.timezone;
-
-        final int cycleDay = getCycleDayFromCarrierConfig(config, time.monthDay);
+        final int cycleDay = getCycleDayFromCarrierConfig(config,
+                ZonedDateTime.now().getDayOfMonth());
         final long warningBytes = getWarningBytesFromCarrierConfig(config,
                 getPlatformDefaultWarningBytes());
         final long limitBytes = getLimitBytesFromCarrierConfig(config,
                 getPlatformDefaultLimitBytes());
 
         final NetworkTemplate template = buildTemplateMobileAll(subscriberId);
-        final NetworkPolicy policy = new NetworkPolicy(template, cycleDay, cycleTimezone,
+        final RecurrenceRule cycleRule = NetworkPolicy.buildRule(cycleDay, ZoneId.systemDefault());
+        final NetworkPolicy policy = new NetworkPolicy(template, cycleRule,
                 warningBytes, limitBytes, SNOOZE_NEVER, SNOOZE_NEVER, true, true);
         return policy;
     }
@@ -1744,6 +1760,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
 
         // clear any existing policy and read from disk
         mNetworkPolicy.clear();
+        mSubscriptionPlans.clear();
         mUidPolicy.clear();
 
         FileInputStream fis = null;
@@ -1787,12 +1804,24 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
                         } else {
                             networkId = null;
                         }
-                        final int cycleDay = readIntAttribute(in, ATTR_CYCLE_DAY);
-                        final String cycleTimezone;
-                        if (version >= VERSION_ADDED_TIMEZONE) {
-                            cycleTimezone = in.getAttributeValue(null, ATTR_CYCLE_TIMEZONE);
+                        final RecurrenceRule cycleRule;
+                        if (version >= VERSION_ADDED_CYCLE) {
+                            final String start = readStringAttribute(in, ATTR_CYCLE_START);
+                            final String end = readStringAttribute(in, ATTR_CYCLE_END);
+                            final String period = readStringAttribute(in, ATTR_CYCLE_PERIOD);
+                            cycleRule = new RecurrenceRule(
+                                    RecurrenceRule.convertZonedDateTime(start),
+                                    RecurrenceRule.convertZonedDateTime(end),
+                                    RecurrenceRule.convertPeriod(period));
                         } else {
-                            cycleTimezone = Time.TIMEZONE_UTC;
+                            final int cycleDay = readIntAttribute(in, ATTR_CYCLE_DAY);
+                            final String cycleTimezone;
+                            if (version >= VERSION_ADDED_TIMEZONE) {
+                                cycleTimezone = in.getAttributeValue(null, ATTR_CYCLE_TIMEZONE);
+                            } else {
+                                cycleTimezone = "UTC";
+                            }
+                            cycleRule = NetworkPolicy.buildRule(cycleDay, ZoneId.of(cycleTimezone));
                         }
                         final long warningBytes = readLongAttribute(in, ATTR_WARNING_BYTES);
                         final long limitBytes = readLongAttribute(in, ATTR_LIMIT_BYTES);
@@ -1834,10 +1863,45 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
                         final NetworkTemplate template = new NetworkTemplate(networkTemplate,
                                 subscriberId, networkId);
                         if (template.isPersistable()) {
-                            mNetworkPolicy.put(template, new NetworkPolicy(template, cycleDay,
-                                    cycleTimezone, warningBytes, limitBytes, lastWarningSnooze,
+                            mNetworkPolicy.put(template, new NetworkPolicy(template, cycleRule,
+                                    warningBytes, limitBytes, lastWarningSnooze,
                                     lastLimitSnooze, metered, inferred));
                         }
+
+                    } else if (TAG_SUBSCRIPTION_PLAN.equals(tag)) {
+                        final String start = readStringAttribute(in, ATTR_CYCLE_START);
+                        final String end = readStringAttribute(in, ATTR_CYCLE_END);
+                        final String period = readStringAttribute(in, ATTR_CYCLE_PERIOD);
+                        final SubscriptionPlan.Builder builder = new SubscriptionPlan.Builder(
+                                RecurrenceRule.convertZonedDateTime(start),
+                                RecurrenceRule.convertZonedDateTime(end),
+                                RecurrenceRule.convertPeriod(period));
+                        builder.setTitle(readStringAttribute(in, ATTR_TITLE));
+                        builder.setSummary(readStringAttribute(in, ATTR_SUMMARY));
+
+                        final long limitBytes = readLongAttribute(in, ATTR_LIMIT_BYTES,
+                                SubscriptionPlan.BYTES_UNKNOWN);
+                        final int limitBehavior = readIntAttribute(in, ATTR_LIMIT_BEHAVIOR,
+                                SubscriptionPlan.LIMIT_BEHAVIOR_UNKNOWN);
+                        if (limitBytes != SubscriptionPlan.BYTES_UNKNOWN
+                                && limitBehavior != SubscriptionPlan.LIMIT_BEHAVIOR_UNKNOWN) {
+                            builder.setDataLimit(limitBytes, limitBehavior);
+                        }
+
+                        final long usageBytes = readLongAttribute(in, ATTR_USAGE_BYTES,
+                                SubscriptionPlan.BYTES_UNKNOWN);
+                        final long usageTime = readLongAttribute(in, ATTR_USAGE_TIME,
+                                SubscriptionPlan.TIME_UNKNOWN);
+                        if (usageBytes != SubscriptionPlan.BYTES_UNKNOWN
+                                && usageTime != SubscriptionPlan.TIME_UNKNOWN) {
+                            builder.setDataUsage(usageBytes, usageTime);
+                        }
+
+                        final int subId = readIntAttribute(in, ATTR_SUB_ID);
+                        final SubscriptionPlan plan = builder.build();
+                        mSubscriptionPlans.put(subId, ArrayUtils.appendElement(
+                                SubscriptionPlan.class, mSubscriptionPlans.get(subId), plan));
+
                     } else if (TAG_UID_POLICY.equals(tag)) {
                         final int uid = readIntAttribute(in, ATTR_UID);
                         final int policy = readIntAttribute(in, ATTR_POLICY);
@@ -1898,9 +1962,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
         } catch (FileNotFoundException e) {
             // missing policy is okay, probably first boot
             upgradeDefaultBackgroundDataUL();
-        } catch (IOException e) {
-            Log.wtf(TAG, "problem reading network policy", e);
-        } catch (XmlPullParserException e) {
+        } catch (Exception e) {
             Log.wtf(TAG, "problem reading network policy", e);
         } finally {
             IoUtils.closeQuietly(fis);
@@ -1994,8 +2056,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
                 if (networkId != null) {
                     out.attribute(null, ATTR_NETWORK_ID, networkId);
                 }
-                writeIntAttribute(out, ATTR_CYCLE_DAY, policy.cycleDay);
-                out.attribute(null, ATTR_CYCLE_TIMEZONE, policy.cycleTimezone);
+                writeStringAttribute(out, ATTR_CYCLE_START,
+                        RecurrenceRule.convertZonedDateTime(policy.cycleRule.start));
+                writeStringAttribute(out, ATTR_CYCLE_END,
+                        RecurrenceRule.convertZonedDateTime(policy.cycleRule.end));
+                writeStringAttribute(out, ATTR_CYCLE_PERIOD,
+                        RecurrenceRule.convertPeriod(policy.cycleRule.period));
                 writeLongAttribute(out, ATTR_WARNING_BYTES, policy.warningBytes);
                 writeLongAttribute(out, ATTR_LIMIT_BYTES, policy.limitBytes);
                 writeLongAttribute(out, ATTR_LAST_WARNING_SNOOZE, policy.lastWarningSnooze);
@@ -2005,6 +2071,32 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
                 out.endTag(null, TAG_NETWORK_POLICY);
             }
 
+            // write all known subscription plans
+            for (int i = 0; i < mSubscriptionPlans.size(); i++) {
+                final int subId = mSubscriptionPlans.keyAt(i);
+                final SubscriptionPlan[] plans = mSubscriptionPlans.valueAt(i);
+                if (ArrayUtils.isEmpty(plans)) continue;
+
+                for (SubscriptionPlan plan : plans) {
+                    out.startTag(null, TAG_SUBSCRIPTION_PLAN);
+                    writeIntAttribute(out, ATTR_SUB_ID, subId);
+                    final RecurrenceRule cycleRule = plan.getCycleRule();
+                    writeStringAttribute(out, ATTR_CYCLE_START,
+                            RecurrenceRule.convertZonedDateTime(cycleRule.start));
+                    writeStringAttribute(out, ATTR_CYCLE_END,
+                            RecurrenceRule.convertZonedDateTime(cycleRule.end));
+                    writeStringAttribute(out, ATTR_CYCLE_PERIOD,
+                            RecurrenceRule.convertPeriod(cycleRule.period));
+                    writeStringAttribute(out, ATTR_TITLE, plan.getTitle());
+                    writeStringAttribute(out, ATTR_SUMMARY, plan.getSummary());
+                    writeLongAttribute(out, ATTR_LIMIT_BYTES, plan.getDataLimitBytes());
+                    writeIntAttribute(out, ATTR_LIMIT_BEHAVIOR, plan.getDataLimitBehavior());
+                    writeLongAttribute(out, ATTR_USAGE_BYTES, plan.getDataUsageBytes());
+                    writeLongAttribute(out, ATTR_USAGE_TIME, plan.getDataUsageTime());
+                    out.endTag(null, TAG_SUBSCRIPTION_PLAN);
+                }
+            }
+
             // write all known uid policies
             for (int i = 0; i < mUidPolicy.size(); i++) {
                 final int uid = mUidPolicy.keyAt(i);
@@ -2506,27 +2598,35 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
         }
 
         final SubscriptionInfo si;
+        final PersistableBundle config;
         final long token = Binder.clearCallingIdentity();
         try {
             si = mContext.getSystemService(SubscriptionManager.class)
                     .getActiveSubscriptionInfo(subId);
+            config = mCarrierConfigManager.getConfigForSubId(subId);
         } finally {
             Binder.restoreCallingIdentity(token);
         }
 
-        // First check: does caller have carrier access?
+        // First check: is caller the CarrierService?
         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
+        // Second check: has the CarrierService delegated access?
+        if (config != null) {
+            final String overridePackage = config
+                    .getString(CarrierConfigManager.KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING, null);
+            if (!TextUtils.isEmpty(overridePackage)
+                    && Objects.equals(overridePackage, callingPackage)) {
+                return;
+            }
+        }
 
-        // 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");
+        // Third check: is caller the fallback/default CarrierService?
+        final String defaultPackage = mCarrierConfigManager.getDefaultCarrierServicePackageName();
+        if (!TextUtils.isEmpty(defaultPackage)
+                && Objects.equals(defaultPackage, callingPackage)) {
             return;
         }
 
@@ -2538,11 +2638,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
     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<>();
@@ -2550,7 +2645,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
                 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,
@@ -2562,7 +2656,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
                         .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,
@@ -2615,19 +2708,10 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
             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];
+        synchronized (mUidRulesFirstLock) {
+            synchronized (mNetworkPoliciesSecondLock) {
+                return mSubscriptionPlans.get(subId);
             }
-        } finally {
-            Binder.restoreCallingIdentity(token);
         }
     }
 
@@ -2635,22 +2719,19 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
     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();
+        for (SubscriptionPlan plan : plans) {
+            Preconditions.checkNotNull(plan);
         }
 
         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);
+            maybeRefreshTrustedTime();
+            synchronized (mUidRulesFirstLock) {
+                synchronized (mNetworkPoliciesSecondLock) {
+                    mSubscriptionPlans.put(subId, plans);
+                    // TODO: update any implicit details from newly defined plans
+                    handleNetworkPoliciesUpdateAL(false);
+                }
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -2658,14 +2739,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
     }
 
     @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 11e383c..8decb96 100644 (file)
@@ -34,7 +34,6 @@ import static android.telephony.CarrierConfigManager.DATA_CYCLE_USE_PLATFORM_DEF
 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;
-import static android.text.format.DateUtils.DAY_IN_MILLIS;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 import static android.text.format.Time.TIMEZONE_UTC;
 
@@ -92,7 +91,6 @@ 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;
@@ -108,12 +106,12 @@ 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.RecurrenceRule;
 import android.util.TrustedTime;
 
 import com.android.internal.telephony.PhoneConstants;
@@ -154,7 +152,10 @@ import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.time.Clock;
 import java.time.Instant;
+import java.time.Period;
+import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.util.Arrays;
 import java.util.Calendar;
@@ -396,7 +397,7 @@ public class NetworkPolicyManagerServiceTest {
 
     @After
     public void resetClock() throws Exception {
-        SubscriptionPlan.sNowOverride = -1;
+        RecurrenceRule.sClock = Clock.systemDefaultZone();
     }
 
     @Test
@@ -785,9 +786,9 @@ public class NetworkPolicyManagerServiceTest {
     }
 
     private static long computeLastCycleBoundary(long currentTime, NetworkPolicy policy) {
-        SubscriptionPlan.sNowOverride = currentTime;
-        final Iterator<Pair<ZonedDateTime, ZonedDateTime>> it = NetworkPolicyManager
-                .cycleIterator(policy);
+        RecurrenceRule.sClock = Clock.fixed(Instant.ofEpochMilli(currentTime),
+                ZoneId.systemDefault());
+        final Iterator<Pair<ZonedDateTime, ZonedDateTime>> it = policy.cycleIterator();
         while (it.hasNext()) {
             final Pair<ZonedDateTime, ZonedDateTime> cycle = it.next();
             if (cycle.first.toInstant().toEpochMilli() < currentTime) {
@@ -799,8 +800,9 @@ public class NetworkPolicyManagerServiceTest {
     }
 
     private static long computeNextCycleBoundary(long currentTime, NetworkPolicy policy) {
-        SubscriptionPlan.sNowOverride = currentTime;
-        return NetworkPolicyManager.cycleIterator(policy).next().second.toInstant().toEpochMilli();
+        RecurrenceRule.sClock = Clock.fixed(Instant.ofEpochMilli(currentTime),
+                ZoneId.systemDefault());
+        return policy.cycleIterator().next().second.toInstant().toEpochMilli();
     }
 
     @Test
@@ -894,38 +896,6 @@ public class NetworkPolicyManagerServiceTest {
     }
 
     @Test
-    public void testNextCycleSane() throws Exception {
-        final NetworkPolicy policy = new NetworkPolicy(
-                sTemplateWifi, 31, TIMEZONE_UTC, WARNING_DISABLED, LIMIT_DISABLED, false);
-        final LinkedHashSet<Long> seen = new LinkedHashSet<Long>();
-
-        // walk forwards, ensuring that cycle boundaries don't get stuck
-        long currentCycle = computeNextCycleBoundary(parseTime("2011-08-01T00:00:00.000Z"), policy);
-        for (int i = 0; i < 128; i++) {
-            long nextCycle = computeNextCycleBoundary(currentCycle, policy);
-            assertEqualsFuzzy(DAY_IN_MILLIS * 30, nextCycle - currentCycle, DAY_IN_MILLIS * 3);
-            assertUnique(seen, nextCycle);
-            currentCycle = nextCycle;
-        }
-    }
-
-    @Test
-    public void testLastCycleSane() throws Exception {
-        final NetworkPolicy policy = new NetworkPolicy(
-                sTemplateWifi, 31, TIMEZONE_UTC, WARNING_DISABLED, LIMIT_DISABLED, false);
-        final LinkedHashSet<Long> seen = new LinkedHashSet<Long>();
-
-        // walk backwards, ensuring that cycle boundaries look sane
-        long currentCycle = computeLastCycleBoundary(parseTime("2011-08-04T00:00:00.000Z"), policy);
-        for (int i = 0; i < 128; i++) {
-            long lastCycle = computeLastCycleBoundary(currentCycle, policy);
-            assertEqualsFuzzy(DAY_IN_MILLIS * 30, currentCycle - lastCycle, DAY_IN_MILLIS * 3);
-            assertUnique(seen, lastCycle);
-            currentCycle = lastCycle;
-        }
-    }
-
-    @Test
     public void testCycleTodayJanuary() throws Exception {
         final NetworkPolicy policy = new NetworkPolicy(
                 sTemplateWifi, 14, "US/Pacific", 1024L, 1024L, false);
@@ -946,17 +916,6 @@ public class NetworkPolicyManagerServiceTest {
     }
 
     @Test
-    public void testLastCycleBoundaryDST() throws Exception {
-        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(
-                sTemplateWifi, 3, "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;
@@ -1145,15 +1104,6 @@ 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);
@@ -1472,7 +1422,9 @@ public class NetworkPolicyManagerServiceTest {
     private NetworkPolicy buildDefaultFakeMobilePolicy() {
         NetworkPolicy p = mService.buildDefaultMobilePolicy(FAKE_SUB_ID, FAKE_SUBSCRIBER_ID);
         // set a deterministic cycle date
-        p.cycleDay = DEFAULT_CYCLE_DAY;
+        p.cycleRule = new RecurrenceRule(
+                p.cycleRule.start.withDayOfMonth(DEFAULT_CYCLE_DAY),
+                p.cycleRule.end, Period.ofMonths(1));
         return p;
     }
 
@@ -1665,7 +1617,8 @@ public class NetworkPolicyManagerServiceTest {
     }
 
     private void setCurrentTimeMillis(long currentTimeMillis) {
-        SubscriptionPlan.sNowOverride = currentTimeMillis;
+        RecurrenceRule.sClock = Clock.fixed(Instant.ofEpochMilli(currentTimeMillis),
+                ZoneId.systemDefault());
         mStartTime = currentTimeMillis;
         mElapsedRealtime = 0L;
     }
index 8b3a804..0001d42 100644 (file)
@@ -26,6 +26,7 @@ import android.content.Context;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.service.carrier.CarrierService;
 
 import com.android.ims.ImsReasonInfo;
 import com.android.internal.telephony.ICarrierConfigLoader;
@@ -262,6 +263,17 @@ public class CarrierConfigManager {
             "config_ims_package_override_string";
 
     /**
+     * Override the package that will manage {@link SubscriptionPlan}
+     * information instead of the {@link CarrierService} that defines this
+     * value.
+     *
+     * @see SubscriptionManager#getSubscriptionPlans(int)
+     * @see SubscriptionManager#setSubscriptionPlans(int, java.util.List)
+     */
+    public static final String KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING =
+            "config_plans_package_override_string";
+
+    /**
      * Override the platform's notion of a network operator being considered roaming.
      * Value is string array of SIDs to be considered roaming for 3GPP2 RATs.
      */
@@ -1379,12 +1391,16 @@ public class CarrierConfigManager {
     /**
      * The day of the month (1-31) on which the data cycle rolls over.
      * <p>
-     * If the current month does not have this day, the cycle will roll over at the start of the
-     * next month.
+     * If the current month does not have this day, the cycle will roll over at
+     * the start of the next month.
      * <p>
-     * This setting may be still overridden by explicit user choice. By default, the platform value
-     * will be used.
+     * This setting may be still overridden by explicit user choice. By default,
+     * the platform value will be used.
+     *
+     * @deprecated replaced by
+     *             {@link SubscriptionManager#setSubscriptionPlans(int, java.util.List)}
      */
+    @Deprecated
     public static final String KEY_MONTHLY_DATA_CYCLE_DAY_INT =
             "monthly_data_cycle_day_int";
 
@@ -1395,6 +1411,7 @@ public class CarrierConfigManager {
      *
      * @hide
      */
+    @Deprecated
     public static final int DATA_CYCLE_USE_PLATFORM_DEFAULT = -1;
 
     /**
@@ -1408,6 +1425,7 @@ public class CarrierConfigManager {
      * default data limit, if one exists, will be disabled. A user selected data limit will not be
      * overridden.
      */
+    @Deprecated
     public static final int DATA_CYCLE_THRESHOLD_DISABLED = -2;
 
     /**
@@ -1420,7 +1438,11 @@ public class CarrierConfigManager {
      * <p>
      * This setting may be overridden by explicit user choice. By default, the platform value
      * will be used.
+     *
+     * @deprecated replaced by
+     *             {@link SubscriptionManager#setSubscriptionPlans(int, java.util.List)}
      */
+    @Deprecated
     public static final String KEY_DATA_WARNING_THRESHOLD_BYTES_LONG =
             "data_warning_threshold_bytes_long";
 
@@ -1434,7 +1456,11 @@ public class CarrierConfigManager {
      * <p>
      * This setting may be overridden by explicit user choice. By default, the platform value
      * will be used.
+     *
+     * @deprecated replaced by
+     *             {@link SubscriptionManager#setSubscriptionPlans(int, java.util.List)}
      */
+    @Deprecated
     public static final String KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG =
             "data_limit_threshold_bytes_long";
 
@@ -1866,6 +1892,15 @@ public class CarrierConfigManager {
         }
     }
 
+    /** {@hide} */
+    public String getDefaultCarrierServicePackageName() {
+        try {
+            return getICarrierConfigLoader().getDefaultCarrierServicePackageName();
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
     /**
      * Returns a new bundle with the default value for every supported configuration variable.
      *
index 89c9134..503bf82 100644 (file)
@@ -18,8 +18,8 @@ package android.telephony;
 
 import android.annotation.NonNull;
 import android.annotation.SdkConstant;
-import android.annotation.SystemService;
 import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemService;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Configuration;
@@ -1542,7 +1542,20 @@ public class SubscriptionManager {
         return false;
     }
 
-    /** {@pending} */
+    /**
+     * Get the description of the billing relationship plan between a carrier
+     * and a specific subscriber.
+     * <p>
+     * This method is only accessible to the following narrow set of apps:
+     * <ul>
+     * <li>The carrier app for this subscriberId, as determined by
+     * {@link TelephonyManager#hasCarrierPrivileges(int)}.
+     * <li>The carrier app explicitly delegated access through
+     * {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
+     * </ul>
+     *
+     * @param subId the subscriber this relationship applies to
+     */
     public @NonNull List<SubscriptionPlan> getSubscriptionPlans(int subId) {
         final INetworkPolicyManager npm = INetworkPolicyManager.Stub
                 .asInterface(ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
@@ -1554,7 +1567,23 @@ public class SubscriptionManager {
         }
     }
 
-    /** {@pending} */
+    /**
+     * Set the description of the billing relationship plan between a carrier
+     * and a specific subscriber.
+     * <p>
+     * This method is only accessible to the following narrow set of apps:
+     * <ul>
+     * <li>The carrier app for this subscriberId, as determined by
+     * {@link TelephonyManager#hasCarrierPrivileges(int)}.
+     * <li>The carrier app explicitly delegated access through
+     * {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
+     * </ul>
+     *
+     * @param subId the subscriber this relationship applies to
+     * @param plans the list of plans. The first plan is always the primary and
+     *            most important plan. Any additional plans are secondary and
+     *            may not be displayed or used by decision making logic.
+     */
     public void setSubscriptionPlans(int subId, @NonNull List<SubscriptionPlan> plans) {
         final INetworkPolicyManager npm = INetworkPolicyManager.Stub
                 .asInterface(ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
@@ -1565,15 +1594,4 @@ public class SubscriptionManager {
             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();
-        }
-    }
 }
index da7661a..c9419c5 100644 (file)
@@ -19,45 +19,31 @@ package android.telephony;
 import android.annotation.BytesLong;
 import android.annotation.CurrentTimeMillisLong;
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 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 android.util.RecurrenceRule;
 
-import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
 
 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.Period;
 import java.time.ZonedDateTime;
-import java.time.temporal.ChronoUnit;
-import java.time.temporal.TemporalUnit;
 import java.util.Iterator;
 
-/** {@pending} */
+/**
+ * Description of a billing relationship plan between a carrier and a specific
+ * subscriber. This information is used to present more useful UI to users, such
+ * as explaining how much mobile data they have remaining, and what will happen
+ * when they run out.
+ *
+ * @see SubscriptionManager#setSubscriptionPlans(int, java.util.List)
+ * @see SubscriptionManager#getSubscriptionPlans(int)
+ */
 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,
@@ -68,51 +54,40 @@ public final class SubscriptionPlan implements Parcelable {
     @Retention(RetentionPolicy.SOURCE)
     public @interface LimitBehavior {}
 
+    /** When a resource limit is hit, the behavior is unknown. */
     public static final int LIMIT_BEHAVIOR_UNKNOWN = -1;
+    /** When a resource limit is hit, access is disabled. */
     public static final int LIMIT_BEHAVIOR_DISABLED = 0;
+    /** When a resource limit is hit, the user is billed automatically. */
     public static final int LIMIT_BEHAVIOR_BILLED = 1;
+    /** When a resource limit is hit, access is throttled to a slower rate. */
     public static final int LIMIT_BEHAVIOR_THROTTLED = 2;
 
+    /** Value indicating a number of bytes is unknown. */
     public static final long BYTES_UNKNOWN = -1;
+    /** Value indicating a number of bytes is unlimited. */
+    public static final long BYTES_UNLIMITED = Long.MAX_VALUE;
+
+    /** Value indicating a timestamp is unknown. */
     public static final long TIME_UNKNOWN = -1;
 
-    private final int type;
-    private final ZonedDateTime start;
-    private final ZonedDateTime end;
+    private final RecurrenceRule cycleRule;
     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(RecurrenceRule cycleRule) {
+        this.cycleRule = Preconditions.checkNotNull(cycleRule);
     }
 
     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;
-        }
+        cycleRule = source.readParcelable(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();
@@ -125,25 +100,10 @@ public final class SubscriptionPlan implements Parcelable {
 
     @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.writeParcelable(cycleRule, flags);
         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);
@@ -151,20 +111,15 @@ public final class SubscriptionPlan implements Parcelable {
 
     @Override
     public String toString() {
-        return new StringBuilder("SubscriptionPlan:")
-                .append(" type=").append(type)
-                .append(" start=").append(start)
-                .append(" end=").append(end)
+        return new StringBuilder("SubscriptionPlan{")
+                .append("cycleRule=").append(cycleRule)
                 .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();
+                .append("}").toString();
     }
 
     public static final Parcelable.Creator<SubscriptionPlan> CREATOR = new Parcelable.Creator<SubscriptionPlan>() {
@@ -179,296 +134,161 @@ public final class SubscriptionPlan implements Parcelable {
         }
     };
 
-    public @Type int getType() {
-        return type;
-    }
-
-    public ZonedDateTime getStart() {
-        return start;
-    }
-
-    public ZonedDateTime getEnd() {
-        return end;
+    /** {@hide} */
+    public @NonNull RecurrenceRule getCycleRule() {
+        return cycleRule;
     }
 
+    /** Return the short title of this plan. */
     public @Nullable CharSequence getTitle() {
         return title;
     }
 
+    /** Return the short summary of this plan. */
     public @Nullable CharSequence getSummary() {
         return summary;
     }
 
-    public @BytesLong long getDataWarningBytes() {
-        return dataWarningBytes;
-    }
-
+    /**
+     * Return the usage threshold at which data access changes according to
+     * {@link #getDataLimitBehavior()}.
+     */
     public @BytesLong long getDataLimitBytes() {
         return dataLimitBytes;
     }
 
+    /**
+     * Return the behavior of data access when usage reaches
+     * {@link #getDataLimitBytes()}.
+     */
     public @LimitBehavior int getDataLimitBehavior() {
         return dataLimitBehavior;
     }
 
+    /**
+     * Return a snapshot of currently known mobile data usage at
+     * {@link #getDataUsageTime()}.
+     */
     public @BytesLong long getDataUsageBytes() {
         return dataUsageBytes;
     }
 
+    /**
+     * Return the time at which {@link #getDataUsageBytes()} was valid.
+     */
     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.
+     * Return an iterator that will return all valid data usage cycles based on
+     * any recurrence rules. The iterator starts from the currently active cycle
+     * and walks backwards through time.
      */
     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;
-        }
+        return cycleRule.cycleIterator();
     }
 
+    /**
+     * Builder for a {@link SubscriptionPlan}.
+     */
     public static class Builder {
         private final SubscriptionPlan plan;
 
-        private Builder(@Type int type, ZonedDateTime start, ZonedDateTime end) {
-            plan = new SubscriptionPlan(type, start, end);
+        /** {@hide} */
+        public Builder(ZonedDateTime start, ZonedDateTime end, Period period) {
+            plan = new SubscriptionPlan(new RecurrenceRule(start, end, period));
         }
 
+        /**
+         * Start defining a {@link SubscriptionPlan} that covers a very specific
+         * window of time, and never automatically recurs.
+         */
         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);
+            return new Builder(start, end, null);
         }
 
+        /**
+         * Start defining a {@link SubscriptionPlan} that will recur
+         * automatically every month. It will always recur on the same day of a
+         * particular month. When a particular month ends before the defined
+         * recurrence day, the plan will recur on the last instant of that
+         * month.
+         */
         public static Builder createRecurringMonthly(ZonedDateTime start) {
-            return new Builder(TYPE_RECURRING_MONTHLY, start, null);
+            return new Builder(start, null, Period.ofMonths(1));
         }
 
+        /**
+         * Start defining a {@link SubscriptionPlan} that will recur
+         * automatically every week.
+         */
         public static Builder createRecurringWeekly(ZonedDateTime start) {
-            return new Builder(TYPE_RECURRING_WEEKLY, start, null);
+            return new Builder(start, null, Period.ofDays(7));
         }
 
+        /**
+         * Start defining a {@link SubscriptionPlan} that will recur
+         * automatically every day.
+         */
         public static Builder createRecurringDaily(ZonedDateTime start) {
-            return new Builder(TYPE_RECURRING_DAILY, start, null);
+            return new Builder(start, null, Period.ofDays(1));
         }
 
         public SubscriptionPlan build() {
             return plan;
         }
 
+        /** Set the short title of this plan. */
         public Builder setTitle(@Nullable CharSequence title) {
             plan.title = title;
             return this;
         }
 
+        /** Set the short summary of this plan. */
         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;
-        }
-
+        /**
+         * Set the usage threshold at which data access changes.
+         *
+         * @param dataLimitBytes the usage threshold at which data access
+         *            changes
+         * @param dataLimitBehavior the behavior of data access when usage
+         *            reaches the threshold
+         */
         public Builder setDataLimit(@BytesLong long dataLimitBytes,
                 @LimitBehavior int dataLimitBehavior) {
-            if (dataLimitBytes < BYTES_UNKNOWN) {
-                throw new IllegalArgumentException("Limit must be positive or BYTES_UNKNOWN");
+            if (dataLimitBytes < 0) {
+                throw new IllegalArgumentException("Limit bytes must be positive");
+            }
+            if (dataLimitBehavior < 0) {
+                throw new IllegalArgumentException("Limit behavior must be defined");
             }
             plan.dataLimitBytes = dataLimitBytes;
             plan.dataLimitBehavior = dataLimitBehavior;
             return this;
         }
 
-        /** {@hide} */
-        public Builder setDataLimitSnooze(@CurrentTimeMillisLong long dataLimitSnoozeTime) {
-            plan.dataLimitSnoozeTime = dataLimitSnoozeTime;
-            return this;
-        }
-
+        /**
+         * Set a snapshot of currently known mobile data usage.
+         *
+         * @param dataUsageBytes the currently known mobile data usage
+         * @param dataUsageTime the time at which this snapshot was valid
+         */
         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 < 0) {
+                throw new IllegalArgumentException("Usage bytes must be positive");
             }
-            if ((dataUsageBytes == BYTES_UNKNOWN) != (dataUsageTime == TIME_UNKNOWN)) {
-                throw new IllegalArgumentException("Must provide both usage and time or neither");
+            if (dataUsageTime < 0) {
+                throw new IllegalArgumentException("Usage time must be positive");
             }
             plan.dataUsageBytes = dataUsageBytes;
             plan.dataUsageTime = dataUsageTime;
index d77b27f..5115731 100644 (file)
@@ -28,4 +28,7 @@ interface ICarrierConfigLoader {
     void notifyConfigChangedForSubId(int subId);
 
     void updateConfigForPhoneId(int phoneId, String simState);
+
+    String getDefaultCarrierServicePackageName();
+
 }