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