| /* |
| * 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.SntpClient; |
| 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.Slog; |
| |
| 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.Properties; |
| 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 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 int mPolicyPollPeriodSec; |
| private long mPolicyThreshold; |
| private int 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 int 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 static final String PROPERTIES_FILE = "/etc/gps.conf"; |
| private String mNtpServer; |
| private boolean mNtpActive; |
| |
| public ThrottleService(Context context) { |
| if (VDBG) Slog.v(TAG, "Starting ThrottleService"); |
| mContext = context; |
| |
| mNtpActive = false; |
| |
| mIface = mContext.getResources().getString(R.string.config_datause_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); |
| |
| IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); |
| mNMService = INetworkManagementService.Stub.asInterface(b); |
| |
| 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 interfaceLinkStatusChanged(String iface, boolean link) { |
| if (link) { |
| if (TextUtils.equals(iface, mIface)) { |
| mHandler.obtainMessage(mMsg).sendToTarget(); |
| } |
| } |
| } |
| |
| 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) {} |
| } |
| |
| |
| private static class SettingsObserver extends ContentObserver { |
| private int mMsg; |
| private Handler mHandler; |
| SettingsObserver(Handler handler, int msg) { |
| super(handler); |
| mHandler = handler; |
| mMsg = msg; |
| } |
| |
| void observe(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); |
| } |
| |
| @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) { |
| long bestNow = getBestTime(); |
| 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 synchronized long getResetTime(String iface) { |
| enforceAccessPermission(); |
| long resetTime = 0; |
| if (mRecorder != null) { |
| resetTime = ntpToWallTime(mRecorder.getPeriodEnd()); |
| } |
| return resetTime; |
| } |
| |
| // TODO - fetch for the iface |
| // return time in the local, system wall time, correcting for the use of ntp |
| public synchronized long getPeriodStartTime(String iface) { |
| enforceAccessPermission(); |
| long startTime = 0; |
| if (mRecorder != null) { |
| startTime = ntpToWallTime(mRecorder.getPeriodStart()); |
| } |
| return startTime; |
| } |
| //TODO - a better name? getCliffByteCountThreshold? |
| // TODO - fetch for the iface |
| public synchronized long getCliffThreshold(String iface, int cliff) { |
| enforceAccessPermission(); |
| if (cliff == 1) { |
| return mPolicyThreshold; |
| } |
| return 0; |
| } |
| // TODO - a better name? getThrottleRate? |
| // TODO - fetch for the iface |
| public synchronized int getCliffLevel(String iface, int cliff) { |
| enforceAccessPermission(); |
| if (cliff == 1) { |
| return mPolicyThrottleValue; |
| } |
| return 0; |
| } |
| |
| public String getHelpUri() { |
| enforceAccessPermission(); |
| return Settings.Secure.getString(mContext.getContentResolver(), |
| Settings.Secure.THROTTLE_HELP_URI); |
| } |
| |
| // TODO - fetch for the iface |
| public synchronized 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 synchronized int getThrottle(String iface) { |
| enforceAccessPermission(); |
| if (mThrottleIndex == 1) { |
| return mPolicyThrottleValue; |
| } |
| return 0; |
| } |
| |
| void systemReady() { |
| if (VDBG) Slog.v(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(); |
| |
| 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.observe(mContext); |
| |
| FileInputStream stream = null; |
| try { |
| Properties properties = new Properties(); |
| File file = new File(PROPERTIES_FILE); |
| stream = new FileInputStream(file); |
| properties.load(stream); |
| mNtpServer = properties.getProperty("NTP_SERVER", null); |
| } catch (IOException e) { |
| Slog.e(TAG, "Could not open GPS configuration file " + PROPERTIES_FILE); |
| } finally { |
| if (stream != null) { |
| try { |
| stream.close(); |
| } catch (Exception e) {} |
| } |
| } |
| } |
| |
| |
| 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 = 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); |
| synchronized (ThrottleService.this) { |
| mPolicyThreshold = Settings.Secure.getLong(mContext.getContentResolver(), |
| Settings.Secure.THROTTLE_THRESHOLD_BYTES, defaultThreshold); |
| mPolicyThrottleValue = Settings.Secure.getInt(mContext.getContentResolver(), |
| Settings.Secure.THROTTLE_VALUE_KBITSPS, defaultValue); |
| if (testing) { |
| mPolicyPollPeriodSec = TESTING_POLLING_PERIOD_SEC; |
| mPolicyThreshold = 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); |
| } |
| synchronized (ThrottleService.this) { |
| if (mIface == null) { |
| mPolicyThreshold = 0; |
| } |
| } |
| |
| int defaultNotificationType = mContext.getResources().getInteger( |
| R.integer.config_datause_notification_type); |
| mPolicyNotificationsAllowedMask = Settings.Secure.getInt(mContext.getContentResolver(), |
| Settings.Secure.THROTTLE_NOTIFICATION_TYPE, defaultNotificationType); |
| |
| mMaxNtpCacheAgeSec = Settings.Secure.getInt(mContext.getContentResolver(), |
| Settings.Secure.THROTTLE_MAX_NTP_CACHE_AGE_SEC, MAX_NTP_CACHE_AGE_SEC); |
| |
| if (VDBG || (mPolicyThreshold != 0)) { |
| Slog.d(TAG, "onPolicyChanged testing=" + testing +", period=" + |
| mPolicyPollPeriodSec + ", threshold=" + mPolicyThreshold + ", value=" + |
| mPolicyThrottleValue + ", resetDay=" + mPolicyResetDay + ", noteType=" + |
| mPolicyNotificationsAllowedMask + ", maxNtpCacheAge=" + mMaxNtpCacheAgeSec); |
| } |
| |
| // force updates |
| mThrottleIndex = 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; |
| |
| checkForAuthoritativeTime(); |
| |
| long incRead = 0; |
| long incWrite = 0; |
| try { |
| incRead = mNMService.getInterfaceRxCounter(mIface) - mLastRead; |
| incWrite = mNMService.getInterfaceTxCounter(mIface) - mLastWrite; |
| // 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 (RemoteException e) { |
| Slog.e(TAG, "got remoteException in 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 != 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); |
| |
| 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 == 1) { |
| try { |
| mNMService.setInterfaceThrottle(mIface, -1, -1); |
| mNMService.setInterfaceThrottle(mIface, |
| mPolicyThrottleValue, mPolicyThrottleValue); |
| } catch (Exception e) { |
| Slog.e(TAG, "error setting Throttle: " + e); |
| } |
| } |
| } |
| |
| private void checkThrottleAndPostNotification(long currentTotal) { |
| // is throttling enabled? |
| if (mPolicyThreshold == 0) { |
| clearThrottleAndNotification(); |
| return; |
| } |
| |
| // have we spoken with an ntp server yet? |
| // this is controversial, but we'd rather err towards not throttling |
| if ((mNtpServer != null) && !mNtpActive) { |
| return; |
| } |
| |
| // check if we need to throttle |
| if (currentTotal > mPolicyThreshold) { |
| if (mThrottleIndex != 1) { |
| synchronized (ThrottleService.this) { |
| mThrottleIndex = 1; |
| } |
| if (DBG) Slog.d(TAG, "Threshold " + mPolicyThreshold + " exceeded!"); |
| try { |
| mNMService.setInterfaceThrottle(mIface, |
| mPolicyThrottleValue, mPolicyThrottleValue); |
| } 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); |
| 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*mPolicyThreshold*timeUsed/(timeUsed+periodLength); |
| if ((currentTotal > warningThreshold) && (currentTotal > mPolicyThreshold/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 synchronized void clearThrottleAndNotification() { |
| if (mThrottleIndex != THROTTLE_INDEX_UNTHROTTLED) { |
| synchronized (ThrottleService.this) { |
| mThrottleIndex = 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 != 0)) { |
| Slog.d(TAG, "onResetAlarm - last period had " + mRecorder.getPeriodRx(0) + |
| " bytes read and " + mRecorder.getPeriodTx(0) + " written"); |
| } |
| |
| long now = getBestTime(); |
| |
| if (mNtpActive || (mNtpServer == null)) { |
| 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 authoritative time - not resetting period"); |
| } |
| } |
| } |
| |
| private void checkForAuthoritativeTime() { |
| if (mNtpActive || (mNtpServer == null)) return; |
| |
| // will try to get the ntp time and switch to it if found. |
| // will also cache the time so we don't fetch it repeatedly. |
| getBestTime(); |
| } |
| |
| private static final int MAX_NTP_CACHE_AGE_SEC = 60 * 60 * 24; // 1 day |
| private static final int MAX_NTP_FETCH_WAIT = 10 * 1000; |
| private int mMaxNtpCacheAgeSec = MAX_NTP_CACHE_AGE_SEC; |
| private long cachedNtp; |
| private long cachedNtpTimestamp; |
| |
| private long getBestTime() { |
| if (mNtpServer != null) { |
| if (mNtpActive) { |
| long ntpAge = SystemClock.elapsedRealtime() - cachedNtpTimestamp; |
| if (ntpAge < mMaxNtpCacheAgeSec * 1000) { |
| if (VDBG) Slog.v(TAG, "using cached time"); |
| return cachedNtp + ntpAge; |
| } |
| } |
| SntpClient client = new SntpClient(); |
| if (client.requestTime(mNtpServer, MAX_NTP_FETCH_WAIT)) { |
| cachedNtp = client.getNtpTime(); |
| cachedNtpTimestamp = SystemClock.elapsedRealtime(); |
| if (!mNtpActive) { |
| mNtpActive = true; |
| if (VDBG) Slog.d(TAG, "found Authoritative time - reseting alarm"); |
| mHandler.obtainMessage(EVENT_RESET_ALARM).sendToTarget(); |
| } |
| if (VDBG) Slog.v(TAG, "using Authoritative time: " + cachedNtp); |
| return cachedNtp; |
| } |
| } |
| long time = System.currentTimeMillis(); |
| if (VDBG) Slog.v(TAG, "using User time: " + time); |
| mNtpActive = false; |
| return time; |
| } |
| |
| // 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.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(); |
| |
| 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; |
| } |
| synchronized (mParent) { |
| String[] parsed = data.split(":"); |
| int parsedUsed = 0; |
| if (parsed.length < 6) { |
| Slog.e(TAG, "reading data file with insufficient length - ignoring"); |
| return; |
| } |
| |
| if (Integer.parseInt(parsed[parsedUsed++]) != DATA_FILE_VERSION) { |
| Slog.e(TAG, "reading data file with bad version - ignoring"); |
| return; |
| } |
| |
| mPeriodCount = Integer.parseInt(parsed[parsedUsed++]); |
| if (parsed.length != 5 + (2 * mPeriodCount)) { |
| Slog.e(TAG, "reading data file with bad length (" + parsed.length + |
| " != " + (5+(2*mPeriodCount)) + ") - ignoring"); |
| 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) { |
| 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 " + (getResetTime(mIface) - System.currentTimeMillis()) / 1000 + |
| " seconds."); |
| pw.println("Polling every " + mPolicyPollPeriodSec + " seconds"); |
| pw.println("Current Throttle Index is " + mThrottleIndex); |
| pw.println("Max NTP Cache Age is " + mMaxNtpCacheAgeSec); |
| |
| for (int i = 0; i < mRecorder.getPeriodCount(); i++) { |
| pw.println(" Period[" + i + "] - read:" + mRecorder.getPeriodRx(i) + ", written:" + |
| mRecorder.getPeriodTx(i)); |
| } |
| } |
| } |