/*
 * 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));
        }
    }
}
