| /* |
| * 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.os.UserHandle; |
| 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) {} |
| public void interfaceClassDataActivityChanged(String label, boolean active) {} |
| } |
| |
| |
| 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.removeStickyBroadcastAsUser(mPollStickyBroadcast, UserHandle.ALL); |
| } |
| } |
| |
| 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.sendBroadcastAsUser(broadcast, UserHandle.ALL); |
| } |
| |
| 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.getNetworkStatsSummaryDev(); |
| 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.sendStickyBroadcastAsUser(broadcast, UserHandle.ALL); |
| 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.sendStickyBroadcastAsUser(broadcast, UserHandle.ALL); |
| |
| } // 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.getActivityAsUser(mContext, 0, intent, 0, |
| null, UserHandle.CURRENT); |
| |
| 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.notifyAsUser(null, mThrottlingNotification.icon, |
| mThrottlingNotification, UserHandle.ALL); |
| } |
| |
| |
| 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.sendStickyBroadcastAsUser(broadcast, UserHandle.ALL); |
| mNotificationManager.cancelAsUser(null, R.drawable.stat_sys_throttled, |
| UserHandle.ALL); |
| 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)); |
| } |
| } |
| } |