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";
import android.net.NetworkQuotaInfo;
import android.net.NetworkState;
import android.net.NetworkTemplate;
+import android.telephony.SubscriptionPlan;
/**
* Interface that creates and modifies network policy rules.
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);
}
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) {
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;
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.
}
}
- /**
- * 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();
}
/**
<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 -->
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) {
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;
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";
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) {
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;
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;
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;
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;
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;
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;
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;
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)) {
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
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));
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;
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;
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;
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;
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;
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;
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;
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;
LocalServices.removeServiceForTest(UsageStatsManagerInternal.class);
}
+ @After
+ public void resetClock() throws Exception {
+ SubscriptionPlan.sNowOverride = -1;
+ }
+
@Test
public void testTurnRestrictBackgroundOn() throws Exception {
assertRestrictBackgroundOff(); // Sanity check.
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
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);
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));
@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(
}
@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.
// 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());
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[] {};
{
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
{
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));
{
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);
{
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);
{
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());
}
@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);
}
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) {
}
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) + "]");
}
}
}
private void setCurrentTimeMillis(long currentTimeMillis) {
+ SubscriptionPlan.sNowOverride = currentTimeMillis;
mStartTime = currentTimeMillis;
mElapsedRealtime = 0L;
}
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;
import com.android.internal.telephony.PhoneConstants;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
/**
}
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();
+ }
+ }
}
--- /dev/null
+/*
+ * 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;
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}