Jeff Sharkey 558a232006 Data usage buckets active time, parsing ISE.
When recording data usage, measure the actual active time, since
buckets can be quite long.  Offer incrementOperationCount() version
that reads thread stats tag for caller.  Rethrow any NPE as ISE
during stats parsing, which callers already handle.

Bug: 5171812, 5184508, 5180659
Change-Id: I6da80ccc0162be68bee279529e3a23b6f98ebd87
2011-08-24 16:00:39 -07:00

1156 lines
45 KiB
Java

/*
* 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.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.net.INetworkManagementEventObserver;
import android.net.IThrottleManager;
import android.net.NetworkStats;
import android.net.ThrottleManager;
import android.os.Binder;
import android.os.Environment;
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.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.NtpTrustedTime;
import android.util.Slog;
import android.util.TrustedTime;
import com.android.internal.R;
import com.android.internal.telephony.TelephonyProperties;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
// 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 final boolean DBG = true;
private static final boolean VDBG = false;
private Handler mHandler;
private HandlerThread mThread;
private Context mContext;
private static final int INITIAL_POLL_DELAY_SEC = 90;
private static final int TESTING_POLLING_PERIOD_SEC = 60 * 1;
private static final int TESTING_RESET_PERIOD_SEC = 60 * 10;
private static final long TESTING_THRESHOLD = 1 * 1024 * 1024;
private static final long MAX_NTP_CACHE_AGE = 24 * 60 * 60 * 1000;
private long mMaxNtpCacheAge = MAX_NTP_CACHE_AGE;
private int mPolicyPollPeriodSec;
private AtomicLong mPolicyThreshold;
private AtomicInteger mPolicyThrottleValue;
private int mPolicyResetDay; // 1-28
private int mPolicyNotificationsAllowedMask;
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 String mIface;
private static final int NOTIFICATION_WARNING = 2;
private Notification mThrottlingNotification;
private boolean mWarningNotificationSent = false;
private InterfaceObserver mInterfaceObserver;
private SettingsObserver mSettingsObserver;
private AtomicInteger mThrottleIndex; // 0 for none, 1 for first throttle val, 2 for next, etc
private static final int THROTTLE_INDEX_UNINITIALIZED = -1;
private static final int THROTTLE_INDEX_UNTHROTTLED = 0;
private Intent mPollStickyBroadcast;
private TrustedTime mTime;
private static INetworkManagementService getNetworkManagementService() {
final IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE);
return INetworkManagementService.Stub.asInterface(b);
}
public ThrottleService(Context context) {
this(context, getNetworkManagementService(), NtpTrustedTime.getInstance(context),
context.getResources().getString(R.string.config_datause_iface));
}
public ThrottleService(Context context, INetworkManagementService nmService, TrustedTime time,
String iface) {
if (VDBG) Slog.v(TAG, "Starting ThrottleService");
mContext = context;
mPolicyThreshold = new AtomicLong();
mPolicyThrottleValue = new AtomicInteger();
mThrottleIndex = new AtomicInteger();
mIface = iface;
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);
mNMService = nmService;
mTime = time;
mNotificationManager = (NotificationManager)mContext.getSystemService(
Context.NOTIFICATION_SERVICE);
}
private static class InterfaceObserver extends INetworkManagementEventObserver.Stub {
private int mMsg;
private Handler mHandler;
private String mIface;
InterfaceObserver(Handler handler, int msg, String iface) {
super();
mHandler = handler;
mMsg = msg;
mIface = iface;
}
public void interfaceStatusChanged(String iface, boolean up) {
if (up) {
if (TextUtils.equals(iface, mIface)) {
mHandler.obtainMessage(mMsg).sendToTarget();
}
}
}
public void interfaceLinkStateChanged(String iface, boolean up) {
}
public void interfaceAdded(String iface) {
// TODO - an interface added in the UP state should also trigger a StatusChanged
// notification..
if (TextUtils.equals(iface, mIface)) {
mHandler.obtainMessage(mMsg).sendToTarget();
}
}
public void interfaceRemoved(String iface) {}
public void limitReached(String limitName, String iface) {}
}
private static class SettingsObserver extends ContentObserver {
private int mMsg;
private Handler mHandler;
SettingsObserver(Handler handler, int msg) {
super(handler);
mHandler = handler;
mMsg = msg;
}
void register(Context context) {
ContentResolver resolver = context.getContentResolver();
resolver.registerContentObserver(Settings.Secure.getUriFor(
Settings.Secure.THROTTLE_POLLING_SEC), false, this);
resolver.registerContentObserver(Settings.Secure.getUriFor(
Settings.Secure.THROTTLE_THRESHOLD_BYTES), false, this);
resolver.registerContentObserver(Settings.Secure.getUriFor(
Settings.Secure.THROTTLE_VALUE_KBITSPS), false, this);
resolver.registerContentObserver(Settings.Secure.getUriFor(
Settings.Secure.THROTTLE_RESET_DAY), false, this);
resolver.registerContentObserver(Settings.Secure.getUriFor(
Settings.Secure.THROTTLE_NOTIFICATION_TYPE), false, this);
resolver.registerContentObserver(Settings.Secure.getUriFor(
Settings.Secure.THROTTLE_HELP_URI), false, this);
resolver.registerContentObserver(Settings.Secure.getUriFor(
Settings.Secure.THROTTLE_MAX_NTP_CACHE_AGE_SEC), false, this);
}
void unregister(Context context) {
final ContentResolver resolver = context.getContentResolver();
resolver.unregisterContentObserver(this);
}
@Override
public void onChange(boolean selfChange) {
mHandler.obtainMessage(mMsg).sendToTarget();
}
}
private void enforceAccessPermission() {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.ACCESS_NETWORK_STATE,
"ThrottleService");
}
private long ntpToWallTime(long ntpTime) {
// get time quickly without worrying about trusted state
long bestNow = mTime.hasCache() ? mTime.currentTimeMillis()
: System.currentTimeMillis();
long localNow = System.currentTimeMillis();
return localNow + (ntpTime - bestNow);
}
// TODO - fetch for the iface
// return time in the local, system wall time, correcting for the use of ntp
public long getResetTime(String iface) {
enforceAccessPermission();
long resetTime = 0;
if (mRecorder != null) {
resetTime = mRecorder.getPeriodEnd();
}
resetTime = ntpToWallTime(resetTime);
return resetTime;
}
// TODO - fetch for the iface
// return time in the local, system wall time, correcting for the use of ntp
public long getPeriodStartTime(String iface) {
long startTime = 0;
enforceAccessPermission();
if (mRecorder != null) {
startTime = mRecorder.getPeriodStart();
}
startTime = ntpToWallTime(startTime);
return startTime;
}
//TODO - a better name? getCliffByteCountThreshold?
// TODO - fetch for the iface
public long getCliffThreshold(String iface, int cliff) {
enforceAccessPermission();
if (cliff == 1) {
return mPolicyThreshold.get();
}
return 0;
}
// TODO - a better name? getThrottleRate?
// TODO - fetch for the iface
public int getCliffLevel(String iface, int cliff) {
enforceAccessPermission();
if (cliff == 1) {
return mPolicyThrottleValue.get();
}
return 0;
}
public String getHelpUri() {
enforceAccessPermission();
return Settings.Secure.getString(mContext.getContentResolver(),
Settings.Secure.THROTTLE_HELP_URI);
}
// TODO - fetch for the iface
public long getByteCount(String iface, int dir, int period, int ago) {
enforceAccessPermission();
if ((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?
// TODO - fetch for the iface
public int getThrottle(String iface) {
enforceAccessPermission();
if (mThrottleIndex.get() == 1) {
return mPolicyThrottleValue.get();
}
return 0;
}
void systemReady() {
if (VDBG) Slog.v(TAG, "systemReady");
mContext.registerReceiver(
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
dispatchPoll();
}
}, new IntentFilter(ACTION_POLL));
mContext.registerReceiver(
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
dispatchReset();
}
}, 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();
mInterfaceObserver = new InterfaceObserver(mHandler, EVENT_IFACE_UP, mIface);
try {
mNMService.registerObserver(mInterfaceObserver);
} catch (RemoteException e) {
Slog.e(TAG, "Could not register InterfaceObserver " + e);
}
mSettingsObserver = new SettingsObserver(mHandler, EVENT_POLICY_CHANGED);
mSettingsObserver.register(mContext);
}
void shutdown() {
// TODO: eventually connect with ShutdownThread to persist stats during
// graceful shutdown.
if (mThread != null) {
mThread.quit();
}
if (mSettingsObserver != null) {
mSettingsObserver.unregister(mContext);
}
if (mPollStickyBroadcast != null) {
mContext.removeStickyBroadcast(mPollStickyBroadcast);
}
}
void dispatchPoll() {
mHandler.obtainMessage(EVENT_POLL_ALARM).sendToTarget();
}
void dispatchReset() {
mHandler.obtainMessage(EVENT_RESET_ALARM).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 static final int EVENT_IFACE_UP = 4;
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();
break;
case EVENT_IFACE_UP:
onIfaceUp();
}
}
private void onRebootRecovery() {
if (VDBG) Slog.v(TAG, "onRebootRecovery");
// check for sim change TODO
// reregister for notification of policy change
mThrottleIndex.set(THROTTLE_INDEX_UNINITIALIZED);
mRecorder = new DataRecorder(mContext, ThrottleService.this);
// get policy
mHandler.obtainMessage(EVENT_POLICY_CHANGED).sendToTarget();
// if we poll now we won't have network connectivity or even imsi access
// queue up a poll to happen in a little while - after ntp and imsi are avail
// TODO - make this callback based (ie, listen for notificaitons)
mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_POLL_ALARM),
INITIAL_POLL_DELAY_SEC * 1000);
}
// check for new policy info (threshold limit/value/etc)
private void onPolicyChanged() {
boolean testing = SystemProperties.get(TESTING_ENABLED_PROPERTY).equals("true");
int pollingPeriod = mContext.getResources().getInteger(
R.integer.config_datause_polling_period_sec);
mPolicyPollPeriodSec = Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.THROTTLE_POLLING_SEC, pollingPeriod);
// TODO - remove testing stuff?
long defaultThreshold = mContext.getResources().getInteger(
R.integer.config_datause_threshold_bytes);
int defaultValue = mContext.getResources().getInteger(
R.integer.config_datause_throttle_kbitsps);
long threshold = Settings.Secure.getLong(mContext.getContentResolver(),
Settings.Secure.THROTTLE_THRESHOLD_BYTES, defaultThreshold);
int value = Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.THROTTLE_VALUE_KBITSPS, defaultValue);
mPolicyThreshold.set(threshold);
mPolicyThrottleValue.set(value);
if (testing) {
mPolicyPollPeriodSec = TESTING_POLLING_PERIOD_SEC;
mPolicyThreshold.set(TESTING_THRESHOLD);
}
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);
}
if (mIface == null) {
mPolicyThreshold.set(0);
}
int defaultNotificationType = mContext.getResources().getInteger(
R.integer.config_datause_notification_type);
mPolicyNotificationsAllowedMask = Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.THROTTLE_NOTIFICATION_TYPE, defaultNotificationType);
final int maxNtpCacheAgeSec = Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.THROTTLE_MAX_NTP_CACHE_AGE_SEC,
(int) (MAX_NTP_CACHE_AGE / 1000));
mMaxNtpCacheAge = maxNtpCacheAgeSec * 1000;
if (VDBG || (mPolicyThreshold.get() != 0)) {
Slog.d(TAG, "onPolicyChanged testing=" + testing +", period=" +
mPolicyPollPeriodSec + ", threshold=" + mPolicyThreshold.get() +
", value=" + mPolicyThrottleValue.get() + ", resetDay=" + mPolicyResetDay +
", noteType=" + mPolicyNotificationsAllowedMask + ", mMaxNtpCacheAge=" +
mMaxNtpCacheAge);
}
// force updates
mThrottleIndex.set(THROTTLE_INDEX_UNINITIALIZED);
onResetAlarm();
onPollAlarm();
Intent broadcast = new Intent(ThrottleManager.POLICY_CHANGED_ACTION);
mContext.sendBroadcast(broadcast);
}
private void onPollAlarm() {
long now = SystemClock.elapsedRealtime();
long next = now + mPolicyPollPeriodSec * 1000;
// when trusted cache outdated, try refreshing
if (mTime.getCacheAge() > mMaxNtpCacheAge) {
if (mTime.forceRefresh()) {
if (VDBG) Slog.d(TAG, "updated trusted time, reseting alarm");
dispatchReset();
}
}
long incRead = 0;
long incWrite = 0;
try {
final NetworkStats stats = mNMService.getNetworkStatsSummary();
final int index = stats.findIndex(mIface, NetworkStats.UID_ALL,
NetworkStats.SET_DEFAULT, NetworkStats.TAG_NONE);
if (index != -1) {
final NetworkStats.Entry entry = stats.getValues(index, null);
incRead = entry.rxBytes - mLastRead;
incWrite = entry.txBytes - mLastWrite;
} else {
// missing iface, assume stats are 0
Slog.w(TAG, "unable to find stats for iface " + mIface);
}
// handle iface resets - on some device the 3g iface comes and goes and gets
// totals reset to 0. Deal with it
if ((incRead < 0) || (incWrite < 0)) {
incRead += mLastRead;
incWrite += mLastWrite;
mLastRead = 0;
mLastWrite = 0;
}
} catch (IllegalStateException e) {
Slog.e(TAG, "problem during onPollAlarm: " + e);
} catch (RemoteException e) {
Slog.e(TAG, "problem during onPollAlarm: " + e);
}
// don't count this data if we're roaming.
boolean roaming = "true".equals(
SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_ISROAMING));
if (!roaming) {
mRecorder.addData(incRead, incWrite);
}
long periodRx = mRecorder.getPeriodRx(0);
long periodTx = mRecorder.getPeriodTx(0);
long total = periodRx + periodTx;
if (VDBG || (mPolicyThreshold.get() != 0)) {
Slog.d(TAG, "onPollAlarm - roaming =" + roaming +
", 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, getPeriodStartTime(mIface));
broadcast.putExtra(ThrottleManager.EXTRA_CYCLE_END, getResetTime(mIface));
mContext.sendStickyBroadcast(broadcast);
mPollStickyBroadcast = broadcast;
mAlarmManager.cancel(mPendingPollIntent);
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, next, mPendingPollIntent);
}
private void onIfaceUp() {
// if we were throttled before, be sure and set it again - the iface went down
// (and may have disappeared all together) and these settings were lost
if (mThrottleIndex.get() == 1) {
try {
mNMService.setInterfaceThrottle(mIface, -1, -1);
mNMService.setInterfaceThrottle(mIface,
mPolicyThrottleValue.get(), mPolicyThrottleValue.get());
} catch (Exception e) {
Slog.e(TAG, "error setting Throttle: " + e);
}
}
}
private void checkThrottleAndPostNotification(long currentTotal) {
// is throttling enabled?
long threshold = mPolicyThreshold.get();
if (threshold == 0) {
clearThrottleAndNotification();
return;
}
// have we spoken with an ntp server yet?
// this is controversial, but we'd rather err towards not throttling
if (!mTime.hasCache()) {
Slog.w(TAG, "missing trusted time, skipping throttle check");
return;
}
// check if we need to throttle
if (currentTotal > threshold) {
if (mThrottleIndex.get() != 1) {
mThrottleIndex.set(1);
if (DBG) Slog.d(TAG, "Threshold " + threshold + " exceeded!");
try {
mNMService.setInterfaceThrottle(mIface,
mPolicyThrottleValue.get(), mPolicyThrottleValue.get());
} catch (Exception e) {
Slog.e(TAG, "error setting Throttle: " + e);
}
mNotificationManager.cancel(R.drawable.stat_sys_throttled);
postNotification(R.string.throttled_notification_title,
R.string.throttled_notification_message,
R.drawable.stat_sys_throttled,
Notification.FLAG_ONGOING_EVENT);
Intent broadcast = new Intent(ThrottleManager.THROTTLE_ACTION);
broadcast.putExtra(ThrottleManager.EXTRA_THROTTLE_LEVEL,
mPolicyThrottleValue.get());
mContext.sendStickyBroadcast(broadcast);
} // else already up!
} else {
clearThrottleAndNotification();
if ((mPolicyNotificationsAllowedMask & NOTIFICATION_WARNING) != 0) {
// check if we should warn about throttle
// pretend we only have 1/2 the time remaining that we actually do
// if our burn rate in the period so far would have us exceed the limit
// in that 1/2 window, warn the user.
// this gets more generous in the early to middle period and converges back
// to the limit as we move toward the period end.
// adding another factor - it must be greater than the total cap/4
// else we may get false alarms very early in the period.. in the first
// tenth of a percent of the period if we used more than a tenth of a percent
// of the cap we'd get a warning and that's not desired.
long start = mRecorder.getPeriodStart();
long end = mRecorder.getPeriodEnd();
long periodLength = end - start;
long now = System.currentTimeMillis();
long timeUsed = now - start;
long warningThreshold = 2*threshold*timeUsed/(timeUsed+periodLength);
if ((currentTotal > warningThreshold) && (currentTotal > threshold/4)) {
if (mWarningNotificationSent == false) {
mWarningNotificationSent = true;
mNotificationManager.cancel(R.drawable.stat_sys_throttled);
postNotification(R.string.throttle_warning_notification_title,
R.string.throttle_warning_notification_message,
R.drawable.stat_sys_throttled,
0);
}
} else {
if (mWarningNotificationSent == true) {
mNotificationManager.cancel(R.drawable.stat_sys_throttled);
mWarningNotificationSent =false;
}
}
}
}
}
private void postNotification(int titleInt, int messageInt, int icon, int flags) {
Intent intent = new Intent();
// TODO - fix up intent
intent.setClassName("com.android.phone", "com.android.phone.DataUsage");
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 = flags;
mThrottlingNotification.tickerText = title;
mThrottlingNotification.setLatestEventInfo(mContext, title, message, pi);
mNotificationManager.notify(mThrottlingNotification.icon, mThrottlingNotification);
}
private void clearThrottleAndNotification() {
if (mThrottleIndex.get() != THROTTLE_INDEX_UNTHROTTLED) {
mThrottleIndex.set(THROTTLE_INDEX_UNTHROTTLED);
try {
mNMService.setInterfaceThrottle(mIface, -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(R.drawable.stat_sys_throttled);
mWarningNotificationSent = false;
}
}
private Calendar calculatePeriodEnd(long now) {
Calendar end = GregorianCalendar.getInstance();
end.setTimeInMillis(now);
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);
end.set(Calendar.SECOND, 0);
end.set(Calendar.MILLISECOND, 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.setTimeInMillis(now);
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 (VDBG || (mPolicyThreshold.get() != 0)) {
Slog.d(TAG, "onResetAlarm - last period had " + mRecorder.getPeriodRx(0) +
" bytes read and " + mRecorder.getPeriodTx(0) + " written");
}
// when trusted cache outdated, try refreshing
if (mTime.getCacheAge() > mMaxNtpCacheAge) {
mTime.forceRefresh();
}
// as long as we have a trusted time cache, we always reset alarms,
// even if the refresh above failed.
if (mTime.hasCache()) {
final long now = mTime.currentTimeMillis();
Calendar end = calculatePeriodEnd(now);
Calendar start = calculatePeriodStart(end);
if (mRecorder.setNextPeriod(start, end)) {
onPollAlarm();
}
mAlarmManager.cancel(mPendingResetIntent);
long offset = end.getTimeInMillis() - now;
// use Elapsed realtime so clock changes don't fool us.
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + offset,
mPendingResetIntent);
} else {
if (VDBG) Slog.d(TAG, "no trusted time, not resetting period");
}
}
}
// 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;
String mImsi = null;
TelephonyManager mTelephonyManager;
DataRecorder(Context context, ThrottleService parent) {
mContext = context;
mParent = parent;
mTelephonyManager = (TelephonyManager)mContext.getSystemService(
Context.TELEPHONY_SERVICE);
synchronized (mParent) {
mPeriodCount = 6;
mPeriodRxData = new long[mPeriodCount];
mPeriodTxData = new long[mPeriodCount];
mPeriodStart = Calendar.getInstance();
mPeriodEnd = Calendar.getInstance();
retrieve();
}
}
boolean setNextPeriod(Calendar start, Calendar end) {
// TODO - how would we deal with a dual-IMSI device?
checkForSubscriberId();
boolean startNewPeriod = true;
if (start.equals(mPeriodStart) && end.equals(mPeriodEnd)) {
// same endpoints - keep collecting
if (VDBG) {
Slog.d(TAG, "same period (" + start.getTimeInMillis() + "," +
end.getTimeInMillis() +") - ammending data");
}
startNewPeriod = false;
} else {
if (VDBG) {
if(start.equals(mPeriodEnd) || start.after(mPeriodEnd)) {
Slog.d(TAG, "next period (" + start.getTimeInMillis() + "," +
end.getTimeInMillis() + ") - old end was " +
mPeriodEnd.getTimeInMillis() + ", following");
} else {
Slog.d(TAG, "new period (" + start.getTimeInMillis() + "," +
end.getTimeInMillis() + ") replacing old (" +
mPeriodStart.getTimeInMillis() + "," +
mPeriodEnd.getTimeInMillis() + ")");
}
}
synchronized (mParent) {
++mCurrentPeriod;
if (mCurrentPeriod >= mPeriodCount) mCurrentPeriod = 0;
mPeriodRxData[mCurrentPeriod] = 0;
mPeriodTxData[mCurrentPeriod] = 0;
}
}
setPeriodStart(start);
setPeriodEnd(end);
record();
return startNewPeriod;
}
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) {
checkForSubscriberId();
synchronized (mParent) {
mPeriodRxData[mCurrentPeriod] += bytesRead;
mPeriodTxData[mCurrentPeriod] += bytesWritten;
}
record();
}
private File getDataFile() {
File dataDir = Environment.getDataDirectory();
File throttleDir = new File(dataDir, "system/throttle");
throttleDir.mkdirs();
String mImsi = mTelephonyManager.getSubscriberId();
File dataFile;
if (mImsi == null) {
dataFile = useMRUFile(throttleDir);
if (VDBG) Slog.v(TAG, "imsi not available yet, using " + dataFile);
} else {
String imsiHash = Integer.toString(mImsi.hashCode());
dataFile = new File(throttleDir, imsiHash);
}
// touch the file so it's not LRU
dataFile.setLastModified(System.currentTimeMillis());
checkAndDeleteLRUDataFile(throttleDir);
return dataFile;
}
// TODO - get broadcast (TelephonyIntents.ACTION_SIM_STATE_CHANGED) instead of polling
private void checkForSubscriberId() {
if (mImsi != null) return;
mImsi = mTelephonyManager.getSubscriberId();
if (mImsi == null) return;
if (VDBG) Slog.d(TAG, "finally have imsi - retreiving data");
retrieve();
}
private final static int MAX_SIMS_SUPPORTED = 3;
private void checkAndDeleteLRUDataFile(File dir) {
File[] files = dir.listFiles();
if (files == null || files.length <= MAX_SIMS_SUPPORTED) return;
if (DBG) Slog.d(TAG, "Too many data files");
do {
File oldest = null;
for (File f : files) {
if ((oldest == null) || (oldest.lastModified() > f.lastModified())) {
oldest = f;
}
}
if (oldest == null) return;
if (DBG) Slog.d(TAG, " deleting " + oldest);
oldest.delete();
files = dir.listFiles();
} while (files.length > MAX_SIMS_SUPPORTED);
}
private File useMRUFile(File dir) {
File newest = null;
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
if ((newest == null) || (newest.lastModified() < f.lastModified())) {
newest = f;
}
}
}
if (newest == null) {
newest = new File(dir, "temp");
}
return newest;
}
private static final int DATA_FILE_VERSION = 1;
private void record() {
// 1 int version
// 1 int mPeriodCount
// 13*6 long[PERIOD_COUNT] mPeriodRxData
// 13*6 long[PERIOD_COUNT] mPeriodTxData
// 1 int mCurrentPeriod
// 13 long periodStartMS
// 13 long periodEndMS
// 200 chars max
StringBuilder builder = new StringBuilder();
builder.append(DATA_FILE_VERSION);
builder.append(":");
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());
BufferedWriter out = null;
try {
out = new BufferedWriter(new FileWriter(getDataFile()), 256);
out.write(builder.toString());
} catch (IOException e) {
Slog.e(TAG, "Error writing data file");
return;
} finally {
if (out != null) {
try {
out.close();
} catch (Exception e) {}
}
}
}
private void retrieve() {
// clean out any old data first. If we fail to read we don't want old stuff
zeroData(0);
File f = getDataFile();
byte[] buffer;
FileInputStream s = null;
try {
buffer = new byte[(int)f.length()];
s = new FileInputStream(f);
s.read(buffer);
} catch (IOException e) {
Slog.e(TAG, "Error reading data file");
return;
} finally {
if (s != null) {
try {
s.close();
} catch (Exception e) {}
}
}
String data = new String(buffer);
if (data == null || data.length() == 0) {
if (DBG) Slog.d(TAG, "data file empty");
return;
}
String[] parsed = data.split(":");
int parsedUsed = 0;
if (parsed.length < 6) {
Slog.e(TAG, "reading data file with insufficient length - ignoring");
return;
}
int periodCount;
long[] periodRxData;
long[] periodTxData;
int currentPeriod;
Calendar periodStart;
Calendar periodEnd;
try {
if (Integer.parseInt(parsed[parsedUsed++]) != DATA_FILE_VERSION) {
Slog.e(TAG, "reading data file with bad version - ignoring");
return;
}
periodCount = Integer.parseInt(parsed[parsedUsed++]);
if (parsed.length != 5 + (2 * periodCount)) {
Slog.e(TAG, "reading data file with bad length (" + parsed.length +
" != " + (5 + (2 * periodCount)) + ") - ignoring");
return;
}
periodRxData = new long[periodCount];
for (int i = 0; i < periodCount; i++) {
periodRxData[i] = Long.parseLong(parsed[parsedUsed++]);
}
periodTxData = new long[periodCount];
for (int i = 0; i < periodCount; i++) {
periodTxData[i] = Long.parseLong(parsed[parsedUsed++]);
}
currentPeriod = Integer.parseInt(parsed[parsedUsed++]);
periodStart = new GregorianCalendar();
periodStart.setTimeInMillis(Long.parseLong(parsed[parsedUsed++]));
periodEnd = new GregorianCalendar();
periodEnd.setTimeInMillis(Long.parseLong(parsed[parsedUsed++]));
} catch (Exception e) {
Slog.e(TAG, "Error parsing data file - ignoring");
return;
}
synchronized (mParent) {
mPeriodCount = periodCount;
mPeriodRxData = periodRxData;
mPeriodTxData = periodTxData;
mCurrentPeriod = currentPeriod;
mPeriodStart = periodStart;
mPeriodEnd = periodEnd;
}
}
long getPeriodRx(int which) {
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.get() +
", after which you experince throttling to " +
mPolicyThrottleValue.get() + "kbps");
pw.println("Current period is " +
(mRecorder.getPeriodEnd() - mRecorder.getPeriodStart())/1000 + " seconds long " +
"and ends in " + (getResetTime(mIface) - System.currentTimeMillis()) / 1000 +
" seconds.");
pw.println("Polling every " + mPolicyPollPeriodSec + " seconds");
pw.println("Current Throttle Index is " + mThrottleIndex.get());
pw.println("mMaxNtpCacheAge=" + mMaxNtpCacheAge);
for (int i = 0; i < mRecorder.getPeriodCount(); i++) {
pw.println(" Period[" + i + "] - read:" + mRecorder.getPeriodRx(i) + ", written:" +
mRecorder.getPeriodTx(i));
}
}
}