Add service to monitor/control the flow of data.
bug:2576057
Change-Id: Ib343c7ee1d619c6978910d9ee597db195d5aa3b6
diff --git a/Android.mk b/Android.mk
index cecc26a..10b6d67 100644
--- a/Android.mk
+++ b/Android.mk
@@ -117,6 +117,7 @@
core/java/android/hardware/ISensorService.aidl \
core/java/android/net/IConnectivityManager.aidl \
core/java/android/net/INetworkManagementEventObserver.aidl \
+ core/java/android/net/IThrottleManager.aidl \
core/java/android/os/IMessenger.aidl \
core/java/android/os/storage/IMountService.aidl \
core/java/android/os/storage/IMountServiceListener.aidl \
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 3a2aa55..30822d4 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -1398,7 +1398,7 @@
* @see android.os.Vibrator
*/
public static final String VIBRATOR_SERVICE = "vibrator";
-
+
/**
* Use with {@link #getSystemService} to retrieve a {@link
* android.app.StatusBarManager} for interacting with the status bar.
@@ -1421,6 +1421,17 @@
/**
* Use with {@link #getSystemService} to retrieve a {@link
+ * android.net.ThrottleManager} for handling management of
+ * throttling.
+ *
+ * @hide
+ * @see #getSystemService
+ * @see android.net.ThrottleManager
+ */
+ public static final String THROTTLE_SERVICE = "throttle";
+
+ /**
+ * Use with {@link #getSystemService} to retrieve a {@link
* android.net.NetworkManagementService} for handling management of
* system network services
*
diff --git a/core/java/android/net/IThrottleManager.aidl b/core/java/android/net/IThrottleManager.aidl
new file mode 100644
index 0000000..298de6e
--- /dev/null
+++ b/core/java/android/net/IThrottleManager.aidl
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010, 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.net;
+
+import android.os.IBinder;
+
+/**
+ * Interface that answers queries about data transfer amounts and throttling
+ */
+/** {@hide} */
+interface IThrottleManager
+{
+ long getByteCount(String iface, int dir, int period, int ago);
+
+ int getThrottle(String iface);
+
+ long getResetTime(String iface);
+
+ long getPeriodStartTime(String iface);
+
+ long getCliffThreshold(String iface, int cliff);
+
+ int getCliffLevel(String iface, int cliff);
+}
diff --git a/core/java/android/net/ThrottleManager.java b/core/java/android/net/ThrottleManager.java
new file mode 100644
index 0000000..0500f6f
--- /dev/null
+++ b/core/java/android/net/ThrottleManager.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2008 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.net;
+
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.os.Binder;
+import android.os.RemoteException;
+
+/**
+ * Class that handles throttling. It provides read/write numbers per interface
+ * and methods to apply throttled rates.
+ * {@hide}
+ */
+public class ThrottleManager
+{
+ /**
+ * Broadcast each polling period to indicate new data counts.
+ *
+ * Includes four extras:
+ * EXTRA_CYCLE_READ - a long of the read bytecount for the current cycle
+ * EXTRA_CYCLE_WRITE -a long of the write bytecount for the current cycle
+ * EXTRA_CYLCE_START -a long of MS for the cycle start time
+ * EXTRA_CYCLE_END -a long of MS for the cycle stop time
+ * {@hide}
+ */
+ public static final String THROTTLE_POLL_ACTION = "android.net.thrott.POLL_ACTION";
+ /**
+ * The lookup key for a long for the read bytecount for this period. Retrieve with
+ * {@link android.content.Intent#getLongExtra(String)}.
+ * {@hide}
+ */
+ public static final String EXTRA_CYCLE_READ = "cycleRead";
+ /**
+ * contains a long of the number of bytes written in the cycle
+ * {@hide}
+ */
+ public static final String EXTRA_CYCLE_WRITE = "cycleWrite";
+ /**
+ * contains a long of the number of bytes read in the cycle
+ * {@hide}
+ */
+ public static final String EXTRA_CYCLE_START = "cycleStart";
+ /**
+ * contains a long of the ms since 1970 used to init a calendar, etc for the end
+ * of the cycle
+ * {@hide}
+ */
+ public static final String EXTRA_CYCLE_END = "cycleEnd";
+
+ /**
+ * Broadcast when the thottle level changes.
+ * {@hide}
+ */
+ public static final String THROTTLE_ACTION = "android.net.thrott.THROTTLE_ACTION";
+ /**
+ * int of the current bandwidth in TODO
+ * {@hide}
+ */
+ public static final String EXTRA_THROTTLE_LEVEL = "level";
+
+ // {@hide}
+ public static final int DIRECTION_TX = 0;
+ // {@hide}
+ public static final int DIRECTION_RX = 1;
+
+ // {@hide}
+ public static final int PERIOD_CYCLE = 0;
+ // {@hide}
+ public static final int PERIOD_YEAR = 1;
+ // {@hide}
+ public static final int PERIOD_MONTH = 2;
+ // {@hide}
+ public static final int PERIOD_WEEK = 3;
+ // @hide
+ public static final int PERIOD_7DAY = 4;
+ // @hide
+ public static final int PERIOD_DAY = 5;
+ // @hide
+ public static final int PERIOD_24HOUR = 6;
+ // @hide
+ public static final int PERIOD_HOUR = 7;
+ // @hide
+ public static final int PERIOD_60MIN = 8;
+ // @hide
+ public static final int PERIOD_MINUTE = 9;
+ // @hide
+ public static final int PERIOD_60SEC = 10;
+ // @hide
+ public static final int PERIOD_SECOND = 11;
+
+ /**
+ * returns a long of the ms from the epoch to the time the current cycle ends for the
+ * named interface
+ * {@hide}
+ */
+ public long getResetTime(String iface) {
+ try {
+ return mService.getResetTime(iface);
+ } catch (RemoteException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * returns a long of the ms from the epoch to the time the current cycle started for the
+ * named interface
+ * {@hide}
+ */
+ public long getPeriodStartTime(String iface) {
+ try {
+ return mService.getPeriodStartTime(iface);
+ } catch (RemoteException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * returns a long of the byte count either read or written on the named interface
+ * for the period described. Direction is either DIRECTION_RX or DIRECTION_TX and
+ * period may only be PERIOD_CYCLE for the current cycle (other periods may be supported
+ * in the future). Ago indicates the number of periods in the past to lookup - 0 means
+ * the current period, 1 is the last one, 2 was two periods ago..
+ * {@hide}
+ */
+ public long getByteCount(String iface, int direction, int period, int ago) {
+ try {
+ return mService.getByteCount(iface, direction, period, ago);
+ } catch (RemoteException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * returns the number of bytes read+written after which a particular cliff
+ * takes effect on the named iface. Currently only cliff #0 is supported (1 step)
+ * {@hide}
+ */
+ public long getCliffThreshold(String iface, int cliff) {
+ try {
+ return mService.getCliffThreshold(iface, cliff);
+ } catch (RemoteException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * returns the thottling bandwidth (bps) for a given cliff # on the named iface.
+ * only cliff #0 is currently supported.
+ * {@hide}
+ */
+ public int getCliffLevel(String iface, int cliff) {
+ try {
+ return mService.getCliffLevel(iface, cliff);
+ } catch (RemoteException e) {
+ return -1;
+ }
+ }
+
+ private IThrottleManager mService;
+
+ /**
+ * Don't allow use of default constructor.
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ private ThrottleManager() {
+ }
+
+ /**
+ * {@hide}
+ */
+ public ThrottleManager(IThrottleManager service) {
+ if (service == null) {
+ throw new IllegalArgumentException(
+ "ThrottleManager() cannot be constructed with null service");
+ }
+ mService = service;
+ }
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index c07ac31..eb14815 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -3295,7 +3295,43 @@
* @hide
*/
public static final String DEFAULT_INSTALL_LOCATION = "default_install_location";
-
+
+ /**
+ * The bandwidth throttle polling freqency in seconds
+ * @hide
+ */
+ public static final String THROTTLE_POLLING_SEC = "throttle_polling_sec";
+
+ /**
+ * The bandwidth throttle threshold (long)
+ * @hide
+ */
+ public static final String THROTTLE_THRESHOLD = "throttle_threshold";
+
+ /**
+ * The bandwidth throttle value (kbps)
+ * @hide
+ */
+ public static final String THROTTLE_VALUE = "throttle_value";
+
+ /**
+ * The bandwidth throttle reset calendar day (1-28)
+ * @hide
+ */
+ public static final String THROTTLE_RESET_DAY = "throttle_reset_day";
+
+ /**
+ * The throttling notifications we should send
+ * @hide
+ */
+ public static final String THROTTLE_NOTIFICATION_TYPE = "throttle_notification_type";
+
+ /**
+ * The interface we throttle
+ * @hide
+ */
+ public static final String THROTTLE_IFACE = "throttle_iface";
+
/**
* @hide
*/
diff --git a/core/res/res/drawable-hdpi/stat_sys_throttle_warning.png b/core/res/res/drawable-hdpi/stat_sys_throttle_warning.png
new file mode 100644
index 0000000..c42b00c
--- /dev/null
+++ b/core/res/res/drawable-hdpi/stat_sys_throttle_warning.png
Binary files differ
diff --git a/core/res/res/drawable-hdpi/stat_sys_throttled.png b/core/res/res/drawable-hdpi/stat_sys_throttled.png
new file mode 100644
index 0000000..e43fbae
--- /dev/null
+++ b/core/res/res/drawable-hdpi/stat_sys_throttled.png
Binary files differ
diff --git a/core/res/res/drawable-mdpi/stat_sys_throttle_warning.png b/core/res/res/drawable-mdpi/stat_sys_throttle_warning.png
new file mode 100644
index 0000000..3688803
--- /dev/null
+++ b/core/res/res/drawable-mdpi/stat_sys_throttle_warning.png
Binary files differ
diff --git a/core/res/res/drawable-mdpi/stat_sys_throttled.png b/core/res/res/drawable-mdpi/stat_sys_throttled.png
new file mode 100644
index 0000000..efb64ad
--- /dev/null
+++ b/core/res/res/drawable-mdpi/stat_sys_throttled.png
Binary files differ
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 48d1ad7..36dc07c 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -2257,4 +2257,14 @@
<!-- Shown when the device is tethered -->
<string name="tethered_notification_title">Tethering active</string>
<string name="tethered_notification_message">Touch to configure</string>
+
+ <!-- Strings for throttling notification -->
+ <!-- Shown when the user is in danger of being throttled -->
+ <string name="throttle_warning_notification_title">Excessive data use warning</string>
+ <string name="throttle_warning_notification_message">If your data use pattern continues you may be subject to bandwidth restrictions - touch for more information</string>
+
+ <!-- Strings for throttling notification -->
+ <!-- Shown when the users bandwidth is reduced because of excessive data use -->
+ <string name="throttled_notification_title">Bandwidth Restricted</string>
+ <string name="throttled_notification_message">Your mobile data bandwidth is being reduced because of excessive data use - touch for more information</string>
</resources>
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 25a60a6..9d5d035 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -99,6 +99,7 @@
DockObserver dock = null;
UiModeManagerService uiMode = null;
RecognitionManagerService recognition = null;
+ ThrottleService throttle = null;
// Critical services...
try {
@@ -269,6 +270,15 @@
}
try {
+ Slog.i(TAG, "Throttle Service");
+ throttle = new ThrottleService(context);
+ ServiceManager.addService(
+ Context.THROTTLE_SERVICE, throttle);
+ } catch (Throwable e) {
+ Slog.e(TAG, "Failure starting ThrottleService", e);
+ }
+
+ try {
Slog.i(TAG, "Accessibility Manager");
ServiceManager.addService(Context.ACCESSIBILITY_SERVICE,
new AccessibilityManagerService(context));
@@ -457,6 +467,7 @@
final BatteryService batteryF = battery;
final ConnectivityService connectivityF = connectivity;
final DockObserver dockF = dock;
+ final ThrottleService throttleF = throttle;
final UiModeManagerService uiModeF = uiMode;
final AppWidgetService appWidgetF = appWidget;
final WallpaperManagerService wallpaperF = wallpaper;
@@ -488,6 +499,7 @@
if (wallpaperF != null) wallpaperF.systemReady();
if (immF != null) immF.systemReady();
if (locationF != null) locationF.systemReady();
+ if (throttleF != null) throttleF.systemReady();
}
});
diff --git a/services/java/com/android/server/ThrottleService.java b/services/java/com/android/server/ThrottleService.java
new file mode 100644
index 0000000..36931b2
--- /dev/null
+++ b/services/java/com/android/server/ThrottleService.java
@@ -0,0 +1,729 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.content.SharedPreferences;
+import android.net.IThrottleManager;
+import android.net.ThrottleManager;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.util.Slog;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Random;
+
+// TODO - add comments - reference the ThrottleManager for public API
+public class ThrottleService extends IThrottleManager.Stub {
+
+ private static final String TESTING_ENABLED_PROPERTY = "persist.throttle.testing";
+
+ private static final String TAG = "ThrottleService";
+ private static boolean DBG = true;
+ private Handler mHandler;
+ private HandlerThread mThread;
+
+ private Context mContext;
+
+ private int mPolicyPollPeriodSec;
+ private static final int DEFAULT_POLLING_PERIOD_SEC = 60 * 10;
+ private static final int TESTING_POLLING_PERIOD_SEC = 60 * 1;
+
+ private static final int TESTING_RESET_PERIOD_SEC = 60 * 3;
+
+ private static final int PERIOD_COUNT = 6;
+
+ private long mPolicyThreshold;
+ // TODO - remove testing stuff?
+ private static final long DEFAULT_TESTING_THRESHOLD = 1 * 1024 * 1024;
+ private static final long DEFAULT_THRESHOLD = 0; // off by default
+
+ private int mPolicyThrottleValue;
+ private static final int DEFAULT_THROTTLE_VALUE = 100; // 100 Kbps
+
+ private int mPolicyResetDay; // 1-28
+
+ private long mLastRead; // read byte count from last poll
+ private long mLastWrite; // write byte count from last poll
+
+ private static final String ACTION_POLL = "com.android.server.ThrottleManager.action.POLL";
+ private static int POLL_REQUEST = 0;
+ private PendingIntent mPendingPollIntent;
+ private static final String ACTION_RESET = "com.android.server.ThorottleManager.action.RESET";
+ private static int RESET_REQUEST = 1;
+ private PendingIntent mPendingResetIntent;
+
+ private INetworkManagementService mNMService;
+ private AlarmManager mAlarmManager;
+ private NotificationManager mNotificationManager;
+
+ private DataRecorder mRecorder;
+
+ private int mThrottleLevel; // 0 for none, 1 for first throttle val, 2 for next, etc
+
+ private String mPolicyIface;
+
+ private static final int NOTIFICATION_WARNING = 2;
+ private static final int NOTIFICATION_ALL = 0xFFFFFFFF;
+ private int mPolicyNotificationsAllowedMask;
+
+ private Notification mThrottlingNotification;
+ private boolean mWarningNotificationSent = false;
+
+ public ThrottleService(Context context) {
+ if (DBG) Slog.d(TAG, "Starting ThrottleService");
+ mContext = context;
+
+ mAlarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
+ Intent pollIntent = new Intent(ACTION_POLL, null);
+ mPendingPollIntent = PendingIntent.getBroadcast(mContext, POLL_REQUEST, pollIntent, 0);
+ Intent resetIntent = new Intent(ACTION_RESET, null);
+ mPendingResetIntent = PendingIntent.getBroadcast(mContext, RESET_REQUEST, resetIntent, 0);
+
+ IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE);
+ mNMService = INetworkManagementService.Stub.asInterface(b);
+
+ mNotificationManager = (NotificationManager)mContext.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ }
+
+ private void enforceAccessPermission() {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_NETWORK_STATE,
+ "ThrottleService");
+ }
+
+ public synchronized long getResetTime(String iface) {
+ enforceAccessPermission();
+ if (iface.equals(mPolicyIface) && (mRecorder != null)) mRecorder.getPeriodEnd();
+ return 0;
+ }
+ public synchronized long getPeriodStartTime(String iface) {
+ enforceAccessPermission();
+ if (iface.equals(mPolicyIface) && (mRecorder != null)) mRecorder.getPeriodStart();
+ return 0;
+ }
+ //TODO - a better name? getCliffByteCountThreshold?
+ public synchronized long getCliffThreshold(String iface, int cliff) {
+ enforceAccessPermission();
+ if ((cliff == 0) && iface.equals(mPolicyIface)) {
+ return mPolicyThreshold;
+ }
+ return 0;
+ }
+ // TODO - a better name? getThrottleRate?
+ public synchronized int getCliffLevel(String iface, int cliff) {
+ enforceAccessPermission();
+ if ((cliff == 0) && iface.equals(mPolicyIface)) {
+ return mPolicyThrottleValue;
+ }
+ return 0;
+ }
+
+ public synchronized long getByteCount(String iface, int dir, int period, int ago) {
+ enforceAccessPermission();
+ if (iface.equals(mPolicyIface) &&
+ (period == ThrottleManager.PERIOD_CYCLE) &&
+ (mRecorder != null)) {
+ if (dir == ThrottleManager.DIRECTION_TX) return mRecorder.getPeriodTx(ago);
+ if (dir == ThrottleManager.DIRECTION_RX) return mRecorder.getPeriodRx(ago);
+ }
+ return 0;
+ }
+
+ // TODO - a better name - getCurrentThrottleRate?
+ public synchronized int getThrottle(String iface) {
+ enforceAccessPermission();
+ if (iface.equals(mPolicyIface) && (mThrottleLevel == 1)) {
+ return mPolicyThrottleValue;
+ }
+ return 0;
+ }
+
+ void systemReady() {
+ if (DBG) Slog.d(TAG, "systemReady");
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mHandler.obtainMessage(EVENT_POLL_ALARM).sendToTarget();
+ }
+ }, new IntentFilter(ACTION_POLL));
+
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mHandler.obtainMessage(EVENT_RESET_ALARM).sendToTarget();
+ }
+ }, new IntentFilter(ACTION_RESET));
+
+ // use a new thread as we don't want to stall the system for file writes
+ mThread = new HandlerThread(TAG);
+ mThread.start();
+ mHandler = new MyHandler(mThread.getLooper());
+ mHandler.obtainMessage(EVENT_REBOOT_RECOVERY).sendToTarget();
+ }
+
+
+ private static final int EVENT_REBOOT_RECOVERY = 0;
+ private static final int EVENT_POLICY_CHANGED = 1;
+ private static final int EVENT_POLL_ALARM = 2;
+ private static final int EVENT_RESET_ALARM = 3;
+ private class MyHandler extends Handler {
+ public MyHandler(Looper l) {
+ super(l);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_REBOOT_RECOVERY:
+ onRebootRecovery();
+ break;
+ case EVENT_POLICY_CHANGED:
+ onPolicyChanged();
+ break;
+ case EVENT_POLL_ALARM:
+ onPollAlarm();
+ break;
+ case EVENT_RESET_ALARM:
+ onResetAlarm();
+ }
+ }
+
+ private void onRebootRecovery() {
+ if (DBG) Slog.d(TAG, "onRebootRecovery");
+ // check for sim change TODO
+ // reregister for notification of policy change
+
+ // register for roaming indication change
+ // check for roaming TODO
+
+ mRecorder = new DataRecorder(mContext, ThrottleService.this);
+
+ // get policy
+ mHandler.obtainMessage(EVENT_POLICY_CHANGED).sendToTarget();
+
+ // evaluate current conditions
+ mHandler.obtainMessage(EVENT_POLL_ALARM).sendToTarget();
+ }
+
+ private void onSimChange() {
+ // TODO
+ }
+
+ // check for new policy info (threshold limit/value/etc)
+ private void onPolicyChanged() {
+ boolean testing = SystemProperties.get(TESTING_ENABLED_PROPERTY).equals("true");
+
+ int pollingPeriod = DEFAULT_POLLING_PERIOD_SEC;
+ if (testing) pollingPeriod = TESTING_POLLING_PERIOD_SEC;
+ mPolicyPollPeriodSec = Settings.Secure.getInt(mContext.getContentResolver(),
+ Settings.Secure.THROTTLE_POLLING_SEC, pollingPeriod);
+
+ // TODO - remove testing stuff?
+ long defaultThreshold = DEFAULT_THRESHOLD;
+ if (testing) defaultThreshold = DEFAULT_TESTING_THRESHOLD;
+ synchronized (ThrottleService.this) {
+ mPolicyThreshold = Settings.Secure.getLong(mContext.getContentResolver(),
+ Settings.Secure.THROTTLE_THRESHOLD, defaultThreshold);
+ mPolicyThrottleValue = Settings.Secure.getInt(mContext.getContentResolver(),
+ Settings.Secure.THROTTLE_VALUE, DEFAULT_THROTTLE_VALUE);
+ }
+ mPolicyResetDay = Settings.Secure.getInt(mContext.getContentResolver(),
+ Settings.Secure.THROTTLE_RESET_DAY, -1);
+ if (mPolicyResetDay == -1 ||
+ ((mPolicyResetDay < 1) || (mPolicyResetDay > 28))) {
+ Random g = new Random();
+ mPolicyResetDay = 1 + g.nextInt(28); // 1-28
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.THROTTLE_RESET_DAY, mPolicyResetDay);
+ }
+ synchronized (ThrottleService.this) {
+ mPolicyIface = Settings.Secure.getString(mContext.getContentResolver(),
+ Settings.Secure.THROTTLE_IFACE);
+ // TODO - read default from resource so it's device-specific
+ if (mPolicyIface == null) mPolicyIface = "rmnet0";
+ }
+
+ mPolicyNotificationsAllowedMask = Settings.Secure.getInt(mContext.getContentResolver(),
+ Settings.Secure.THROTTLE_NOTIFICATION_TYPE, NOTIFICATION_ALL);
+
+ Slog.d(TAG, "onPolicyChanged testing=" + testing +", period=" + mPolicyPollPeriodSec +
+ ", threshold=" + mPolicyThreshold + ", value=" + mPolicyThrottleValue +
+ ", resetDay=" + mPolicyResetDay + ", noteType=" +
+ mPolicyNotificationsAllowedMask);
+
+ Calendar end = calculatePeriodEnd();
+ Calendar start = calculatePeriodStart(end);
+
+ mRecorder.setNextPeriod(start,end);
+
+ mAlarmManager.cancel(mPendingResetIntent);
+ mAlarmManager.set(AlarmManager.RTC_WAKEUP, end.getTimeInMillis(),
+ mPendingResetIntent);
+ }
+
+ private void onPollAlarm() {
+ long now = SystemClock.elapsedRealtime();
+ long next = now + mPolicyPollPeriodSec*1000;
+ long incRead = 0;
+ long incWrite = 0;
+ try {
+ incRead = mNMService.getInterfaceRxCounter(mPolicyIface) - mLastRead;
+ incWrite = mNMService.getInterfaceTxCounter(mPolicyIface) - mLastWrite;
+ } catch (RemoteException e) {
+ Slog.e(TAG, "got remoteException in onPollAlarm:" + e);
+ }
+
+ mRecorder.addData(incRead, incWrite);
+
+ long periodRx = mRecorder.getPeriodRx(0);
+ long periodTx = mRecorder.getPeriodTx(0);
+ long total = periodRx + periodTx;
+ if (DBG) {
+ Slog.d(TAG, "onPollAlarm - now =" + now + ", read =" + incRead +
+ ", written =" + incWrite + ", new total =" + total);
+ }
+ mLastRead += incRead;
+ mLastWrite += incWrite;
+
+ checkThrottleAndPostNotification(total);
+
+ Intent broadcast = new Intent(ThrottleManager.THROTTLE_POLL_ACTION);
+ broadcast.putExtra(ThrottleManager.EXTRA_CYCLE_READ, periodRx);
+ broadcast.putExtra(ThrottleManager.EXTRA_CYCLE_WRITE, periodTx);
+ broadcast.putExtra(ThrottleManager.EXTRA_CYCLE_START, mRecorder.getPeriodStart());
+ broadcast.putExtra(ThrottleManager.EXTRA_CYCLE_END, mRecorder.getPeriodEnd());
+ mContext.sendStickyBroadcast(broadcast);
+
+ mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, next, mPendingPollIntent);
+ }
+
+ private void checkThrottleAndPostNotification(long currentTotal) {
+ // are we even doing this?
+ if (mPolicyThreshold == 0)
+ return;
+
+ // check if we need to throttle
+ if (currentTotal > mPolicyThreshold) {
+ if (mThrottleLevel != 1) {
+ synchronized (ThrottleService.this) {
+ mThrottleLevel = 1;
+ }
+ if (DBG) Slog.d(TAG, "Threshold " + mPolicyThreshold + " exceeded!");
+ try {
+ mNMService.setInterfaceThrottle(mPolicyIface,
+ mPolicyThrottleValue, mPolicyThrottleValue);
+ } catch (Exception e) {
+ Slog.e(TAG, "error setting Throttle: " + e);
+ }
+
+ mNotificationManager.cancel(com.android.internal.R.drawable.
+ stat_sys_throttle_warning);
+
+ postNotification(com.android.internal.R.string.throttled_notification_title,
+ com.android.internal.R.string.throttled_notification_message,
+ com.android.internal.R.drawable.stat_sys_throttled);
+
+ Intent broadcast = new Intent(ThrottleManager.THROTTLE_ACTION);
+ broadcast.putExtra(ThrottleManager.EXTRA_THROTTLE_LEVEL, mPolicyThrottleValue);
+ mContext.sendStickyBroadcast(broadcast);
+
+ } // else already up!
+ } else {
+ if ((mPolicyNotificationsAllowedMask & NOTIFICATION_WARNING) != 0) {
+ // check if we should warn about throttle
+ if (currentTotal > (mPolicyThreshold/2) && !mWarningNotificationSent) {
+ mWarningNotificationSent = true;
+ mNotificationManager.cancel(com.android.internal.R.drawable.
+ stat_sys_throttle_warning);
+ postNotification(com.android.internal.R.string.
+ throttle_warning_notification_title,
+ com.android.internal.R.string.
+ throttle_warning_notification_message,
+ com.android.internal.R.drawable.stat_sys_throttle_warning);
+ } else {
+ mWarningNotificationSent =false;
+ }
+ }
+ }
+ }
+
+ private void postNotification(int titleInt, int messageInt, int icon) {
+ Intent intent = new Intent();
+ // TODO - fix up intent
+ intent.setClassName("com.android.settings", "com.android.settings.TetherSettings");
+ intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
+
+ PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
+
+ Resources r = Resources.getSystem();
+ CharSequence title = r.getText(titleInt);
+ CharSequence message = r.getText(messageInt);
+ if (mThrottlingNotification == null) {
+ mThrottlingNotification = new Notification();
+ mThrottlingNotification.when = 0;
+ // TODO - fixup icon
+ mThrottlingNotification.icon = icon;
+ mThrottlingNotification.defaults &= ~Notification.DEFAULT_SOUND;
+// mThrottlingNotification.flags = Notification.FLAG_ONGOING_EVENT;
+ }
+ mThrottlingNotification.tickerText = title;
+ mThrottlingNotification.setLatestEventInfo(mContext, title, message, pi);
+
+ mNotificationManager.notify(mThrottlingNotification.icon, mThrottlingNotification);
+ }
+
+
+ private synchronized void clearThrottleAndNotification() {
+ if (mThrottleLevel == 1) {
+ synchronized (ThrottleService.this) {
+ mThrottleLevel = 0;
+ }
+ try {
+ mNMService.setInterfaceThrottle(mPolicyIface, -1, -1);
+ } catch (Exception e) {
+ Slog.e(TAG, "error clearing Throttle: " + e);
+ }
+ Intent broadcast = new Intent(ThrottleManager.THROTTLE_ACTION);
+ broadcast.putExtra(ThrottleManager.EXTRA_THROTTLE_LEVEL, -1);
+ mContext.sendStickyBroadcast(broadcast);
+ }
+ mNotificationManager.cancel(com.android.internal.R.drawable.stat_sys_throttle_warning);
+ mNotificationManager.cancel(com.android.internal.R.drawable.stat_sys_throttled);
+ mWarningNotificationSent = false;
+ }
+
+ private Calendar calculatePeriodEnd() {
+ Calendar end = GregorianCalendar.getInstance();
+ int day = end.get(Calendar.DAY_OF_MONTH);
+ end.set(Calendar.DAY_OF_MONTH, mPolicyResetDay);
+ end.set(Calendar.HOUR_OF_DAY, 0);
+ end.set(Calendar.MINUTE, 0);
+ if (day >= mPolicyResetDay) {
+ int month = end.get(Calendar.MONTH);
+ if (month == Calendar.DECEMBER) {
+ end.set(Calendar.YEAR, end.get(Calendar.YEAR) + 1);
+ month = Calendar.JANUARY - 1;
+ }
+ end.set(Calendar.MONTH, month + 1);
+ }
+
+ // TODO - remove!
+ if (SystemProperties.get(TESTING_ENABLED_PROPERTY).equals("true")) {
+ end = GregorianCalendar.getInstance();
+ end.add(Calendar.SECOND, TESTING_RESET_PERIOD_SEC);
+ }
+ return end;
+ }
+ private Calendar calculatePeriodStart(Calendar end) {
+ Calendar start = (Calendar)end.clone();
+ int month = end.get(Calendar.MONTH);
+ if (end.get(Calendar.MONTH) == Calendar.JANUARY) {
+ month = Calendar.DECEMBER + 1;
+ start.set(Calendar.YEAR, start.get(Calendar.YEAR) - 1);
+ }
+ start.set(Calendar.MONTH, month - 1);
+
+ // TODO - remove!!
+ if (SystemProperties.get(TESTING_ENABLED_PROPERTY).equals("true")) {
+ start = (Calendar)end.clone();
+ start.add(Calendar.SECOND, -TESTING_RESET_PERIOD_SEC);
+ }
+ return start;
+ }
+
+ private void onResetAlarm() {
+ if (DBG) {
+ Slog.d(TAG, "onResetAlarm - last period had " + mRecorder.getPeriodRx(0) +
+ " bytes read and " + mRecorder.getPeriodTx(0) + " written");
+ }
+
+ Calendar end = calculatePeriodEnd();
+ Calendar start = calculatePeriodStart(end);
+
+ clearThrottleAndNotification();
+
+ mRecorder.setNextPeriod(start,end);
+
+ mAlarmManager.set(AlarmManager.RTC_WAKEUP, end.getTimeInMillis(),
+ mPendingResetIntent);
+ }
+ }
+
+ // records bytecount data for a given time and accumulates it into larger time windows
+ // for logging and other purposes
+ //
+ // since time can be changed (user or network action) we will have to track the time of the
+ // last recording and deal with it.
+ private static class DataRecorder {
+ long[] mPeriodRxData;
+ long[] mPeriodTxData;
+ int mCurrentPeriod;
+ int mPeriodCount;
+
+ Calendar mPeriodStart;
+ Calendar mPeriodEnd;
+
+ ThrottleService mParent;
+ Context mContext;
+ SharedPreferences mSharedPreferences;
+
+ DataRecorder(Context context, ThrottleService parent) {
+ mContext = context;
+ mParent = parent;
+
+ synchronized (mParent) {
+ mPeriodCount = 6;
+ mPeriodRxData = new long[mPeriodCount];
+ mPeriodTxData = new long[mPeriodCount];
+
+ mPeriodStart = Calendar.getInstance();
+ mPeriodEnd = Calendar.getInstance();
+
+ mSharedPreferences = mContext.getSharedPreferences("ThrottleData",
+ android.content.Context.MODE_PRIVATE);
+
+ zeroData(0);
+ retrieve();
+ }
+ }
+
+ void setNextPeriod(Calendar start, Calendar end) {
+ if (DBG) {
+ Slog.d(TAG, "setting next period to " + start.getTimeInMillis() +
+ " --until-- " + end.getTimeInMillis());
+ }
+ // if we roll back in time to a previous period, toss out the current data
+ // if we roll forward to the next period, advance to the next
+
+ if (end.before(mPeriodStart)) {
+ if (DBG) {
+ Slog.d(TAG, " old start was " + mPeriodStart.getTimeInMillis() + ", wiping");
+ }
+ synchronized (mParent) {
+ mPeriodRxData[mCurrentPeriod] = 0;
+ mPeriodTxData[mCurrentPeriod] = 0;
+ }
+ } else if(start.after(mPeriodEnd)) {
+ if (DBG) {
+ Slog.d(TAG, " old end was " + mPeriodEnd.getTimeInMillis() + ", following");
+ }
+ synchronized (mParent) {
+ ++mCurrentPeriod;
+ if (mCurrentPeriod >= mPeriodCount) mCurrentPeriod = 0;
+ mPeriodRxData[mCurrentPeriod] = 0;
+ mPeriodTxData[mCurrentPeriod] = 0;
+ }
+ } else {
+ if (DBG) Slog.d(TAG, " we fit - ammending to last period");
+ }
+ setPeriodStart(start);
+ setPeriodEnd(end);
+ record();
+ }
+
+ public long getPeriodEnd() {
+ synchronized (mParent) {
+ return mPeriodEnd.getTimeInMillis();
+ }
+ }
+
+ private void setPeriodEnd(Calendar end) {
+ synchronized (mParent) {
+ mPeriodEnd = end;
+ }
+ }
+
+ public long getPeriodStart() {
+ synchronized (mParent) {
+ return mPeriodStart.getTimeInMillis();
+ }
+ }
+
+ private void setPeriodStart(Calendar start) {
+ synchronized (mParent) {
+ mPeriodStart = start;
+ }
+ }
+
+ public int getPeriodCount() {
+ synchronized (mParent) {
+ return mPeriodCount;
+ }
+ }
+
+ private void zeroData(int field) {
+ synchronized (mParent) {
+ for(int period = 0; period<mPeriodCount; period++) {
+ mPeriodRxData[period] = 0;
+ mPeriodTxData[period] = 0;
+ }
+ mCurrentPeriod = 0;
+ }
+
+ }
+
+ // if time moves backward accumulate all read/write that's lost into the now
+ // otherwise time moved forward.
+ void addData(long bytesRead, long bytesWritten) {
+ synchronized (mParent) {
+ mPeriodRxData[mCurrentPeriod] += bytesRead;
+ mPeriodTxData[mCurrentPeriod] += bytesWritten;
+ }
+ record();
+ }
+
+ private void record() {
+ // serialize into a secure setting
+
+ // 1 int mPeriodCount
+ // 13*6 long[PERIOD_COUNT] mPeriodRxData
+ // 13*6 long[PERIOD_COUNT] mPeriodTxData
+ // 1 int mCurrentPeriod
+ // 13 long periodStartMS
+ // 13 long periodEndMS
+ // 199 chars max
+ StringBuilder builder = new StringBuilder();
+ builder.append(mPeriodCount);
+ builder.append(":");
+ for(int i=0; i < mPeriodCount; i++) {
+ builder.append(mPeriodRxData[i]);
+ builder.append(":");
+ }
+ for(int i=0; i < mPeriodCount; i++) {
+ builder.append(mPeriodTxData[i]);
+ builder.append(":");
+ }
+ builder.append(mCurrentPeriod);
+ builder.append(":");
+ builder.append(mPeriodStart.getTimeInMillis());
+ builder.append(":");
+ builder.append(mPeriodEnd.getTimeInMillis());
+ builder.append(":");
+
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+
+ editor.putString("Data", builder.toString());
+ editor.commit();
+ }
+
+ private void retrieve() {
+ String data = mSharedPreferences.getString("Data", "");
+// String data = Settings.Secure.getString(mContext.getContentResolver(),
+// Settings.Secure.THROTTLE_VALUE);
+ if (data == null || data.length() == 0) return;
+
+ synchronized (mParent) {
+ String[] parsed = data.split(":");
+ int parsedUsed = 0;
+ if (parsed.length < 6) return;
+
+ mPeriodCount = Integer.parseInt(parsed[parsedUsed++]);
+ if (parsed.length != 4 + (2 * mPeriodCount)) return;
+
+ mPeriodRxData = new long[mPeriodCount];
+ for(int i=0; i < mPeriodCount; i++) {
+ mPeriodRxData[i] = Long.parseLong(parsed[parsedUsed++]);
+ }
+ mPeriodTxData = new long[mPeriodCount];
+ for(int i=0; i < mPeriodCount; i++) {
+ mPeriodTxData[i] = Long.parseLong(parsed[parsedUsed++]);
+ }
+ mCurrentPeriod = Integer.parseInt(parsed[parsedUsed++]);
+ mPeriodStart = new GregorianCalendar();
+ mPeriodStart.setTimeInMillis(Long.parseLong(parsed[parsedUsed++]));
+ mPeriodEnd = new GregorianCalendar();
+ mPeriodEnd.setTimeInMillis(Long.parseLong(parsed[parsedUsed++]));
+ }
+ }
+
+ long getPeriodRx(int which) {
+ if (DBG) { // TODO - remove
+ Slog.d(TAG, "reading slot "+ which +" with current =" + mCurrentPeriod);
+ for(int x = 0; x<mPeriodCount; x++) {
+ Slog.d(TAG, " " + x + " = " + mPeriodRxData[x]);
+ }
+ }
+ synchronized (mParent) {
+ if (which > mPeriodCount) return 0;
+ which = mCurrentPeriod - which;
+ if (which < 0) which += mPeriodCount;
+ return mPeriodRxData[which];
+ }
+ }
+ long getPeriodTx(int which) {
+ synchronized (mParent) {
+ if (which > mPeriodCount) return 0;
+ which = mCurrentPeriod - which;
+ if (which < 0) which += mPeriodCount;
+ return mPeriodTxData[which];
+ }
+ }
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.DUMP)
+ != PackageManager.PERMISSION_GRANTED) {
+ pw.println("Permission Denial: can't dump ThrottleService " +
+ "from from pid=" + Binder.getCallingPid() + ", uid=" +
+ Binder.getCallingUid());
+ return;
+ }
+ pw.println();
+
+ pw.println("The threshold is " + mPolicyThreshold +
+ ", after which you experince throttling to " +
+ mPolicyThrottleValue + "kbps");
+ pw.println("Current period is " +
+ (mRecorder.getPeriodEnd() - mRecorder.getPeriodStart())/1000 + " seconds long " +
+ "and ends in " + (mRecorder.getPeriodEnd() - System.currentTimeMillis()) / 1000 +
+ " seconds.");
+ pw.println("Polling every " + mPolicyPollPeriodSec + " seconds");
+ for (int i = 0; i < mRecorder.getPeriodCount(); i++) {
+ pw.println(" Period[" + i + "] - read:" + mRecorder.getPeriodRx(i) + ", written:" +
+ mRecorder.getPeriodTx(i));
+ }
+ }
+}