Modifier to combine/split mobile network policy.

Create NetworkPolicyModifier which knows about which mobile network
policies can coexist.  Settings UI uses this modifier to drive UI and
persist policies.

Change-Id: Ib3f3841b0a74c14eefb99209dd644a2e7b7e525d
diff --git a/src/com/android/settings/DataUsageSummary.java b/src/com/android/settings/DataUsageSummary.java
index e27227f..44a86df 100644
--- a/src/com/android/settings/DataUsageSummary.java
+++ b/src/com/android/settings/DataUsageSummary.java
@@ -16,6 +16,7 @@
 
 package com.android.settings;
 
+import static android.net.NetworkPolicy.LIMIT_DISABLED;
 import static android.net.NetworkPolicyManager.computeLastCycleBoundary;
 import static android.net.NetworkPolicyManager.computeNextCycleBoundary;
 import static android.net.TrafficStats.TEMPLATE_MOBILE_3G_LOWER;
@@ -37,6 +38,8 @@
 import android.os.ServiceManager;
 import android.preference.CheckBoxPreference;
 import android.preference.Preference;
+import android.preference.SwitchPreference;
+import android.telephony.TelephonyManager;
 import android.text.format.DateUtils;
 import android.text.format.Formatter;
 import android.text.format.Time;
@@ -64,6 +67,7 @@
 import android.widget.TabWidget;
 import android.widget.TextView;
 
+import com.android.settings.net.NetworkPolicyModifier;
 import com.android.settings.widget.DataUsageChartView;
 import com.android.settings.widget.DataUsageChartView.DataUsageChartListener;
 import com.google.android.collect.Lists;
@@ -99,7 +103,7 @@
     private View mHeader;
     private LinearLayout mSwitches;
 
-    private CheckBoxPreference mDataEnabled;
+    private SwitchPreference mDataEnabled;
     private CheckBoxPreference mDisableAtLimit;
     private View mDataEnabledView;
     private View mDisableAtLimitView;
@@ -109,25 +113,28 @@
     private Spinner mCycleSpinner;
     private CycleAdapter mCycleAdapter;
 
-    private boolean mSplit4G = false;
+    // TODO: persist show wifi flag
     private boolean mShowWifi = false;
 
     private int mTemplate = TEMPLATE_INVALID;
 
-    private NetworkPolicy mPolicy;
+    private NetworkPolicyModifier mPolicyModifier;
     private NetworkStatsHistory mHistory;
 
-    // TODO: policy service should always provide valid stub policy
-
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setHasOptionsMenu(true);
 
         mStatsService = INetworkStatsService.Stub.asInterface(
                 ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
         mPolicyService = INetworkPolicyManager.Stub.asInterface(
                 ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
+
+        final Context context = getActivity();
+        final String subscriberId = getActiveSubscriberId(context);
+        mPolicyModifier = new NetworkPolicyModifier(mPolicyService, subscriberId);
+
+        setHasOptionsMenu(true);
     }
 
     @Override
@@ -147,7 +154,7 @@
         mHeader = inflater.inflate(R.layout.data_usage_header, mListView, false);
         mListView.addHeaderView(mHeader, null, false);
 
-        mDataEnabled = new CheckBoxPreference(context);
+        mDataEnabled = new SwitchPreference(context);
         mDisableAtLimit = new CheckBoxPreference(context);
 
         // kick refresh once to force-create views
@@ -185,6 +192,9 @@
     public void onResume() {
         super.onResume();
 
+        // read current policy state from service
+        mPolicyModifier.read();
+
         // this kicks off chain reaction which creates tabs, binds the body to
         // selected network, and binds chart, cycles and detail list.
         updateTabs();
@@ -196,13 +206,18 @@
     }
 
     @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        // TODO: persist checked-ness of options to restore tabs later
+    public void onPrepareOptionsMenu(Menu menu) {
+        final MenuItem split4g = menu.findItem(R.id.action_split_4g);
+        split4g.setChecked(mPolicyModifier.isMobilePolicySplit());
+    }
 
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case R.id.action_split_4g: {
-                mSplit4G = !item.isChecked();
-                item.setChecked(mSplit4G);
+                final boolean mobileSplit = !item.isChecked();
+                mPolicyModifier.setMobilePolicySplit(mobileSplit);
+                item.setChecked(mPolicyModifier.isMobilePolicySplit());
                 updateTabs();
                 return true;
             }
@@ -217,24 +232,23 @@
     }
 
     /**
-     * Rebuild all tabs based on {@link #mSplit4G} and {@link #mShowWifi},
-     * hiding the tabs entirely when applicable. Selects first tab, and kicks
-     * off a full rebind of body contents.
+     * Rebuild all tabs based on {@link NetworkPolicyModifier} and
+     * {@link #mShowWifi}, hiding the tabs entirely when applicable. Selects
+     * first tab, and kicks off a full rebind of body contents.
      */
     private void updateTabs() {
-        // TODO: persist/restore if user wants mobile split, or wifi visibility
-
-        final boolean tabsVisible = mSplit4G || mShowWifi;
+        final boolean mobileSplit = mPolicyModifier.isMobilePolicySplit();
+        final boolean tabsVisible = mobileSplit || mShowWifi;
         mTabWidget.setVisibility(tabsVisible ? View.VISIBLE : View.GONE);
         mTabHost.clearAllTabs();
 
-        if (mSplit4G) {
+        if (mobileSplit) {
             mTabHost.addTab(buildTabSpec(TAB_3G, R.string.data_usage_tab_3g));
             mTabHost.addTab(buildTabSpec(TAB_4G, R.string.data_usage_tab_4g));
         }
 
         if (mShowWifi) {
-            if (!mSplit4G) {
+            if (!mobileSplit) {
                 mTabHost.addTab(buildTabSpec(TAB_MOBILE, R.string.data_usage_tab_mobile));
             }
             mTabHost.addTab(buildTabSpec(TAB_WIFI, R.string.data_usage_tab_wifi));
@@ -323,8 +337,7 @@
         mDataEnabled.setChecked(true);
 
         try {
-            // load policy and stats for current template
-            mPolicy = mPolicyService.getNetworkPolicy(mTemplate, null);
+            // load stats for current template
             mHistory = mStatsService.getHistoryForNetwork(mTemplate);
         } catch (RemoteException e) {
             // since we can't do much without policy or history, and we don't
@@ -332,20 +345,10 @@
             throw new RuntimeException("problem reading network policy or stats", e);
         }
 
-        // TODO: eventually service will always provide stub policy
-        if (mPolicy == null) {
-            mPolicy = new NetworkPolicy(1, 4 * GB_IN_BYTES, -1);
-        }
-
         // bind chart to historical stats
-        mChart.bindNetworkPolicy(mPolicy);
         mChart.bindNetworkStats(mHistory);
 
-        // generate cycle list based on policy and available history
-        updateCycleList();
-
-        // reflect policy limit in checkbox
-        mDisableAtLimit.setChecked(mPolicy.limitBytes != -1);
+        updatePolicy();
 
         // force scroll to top of body
         mListView.smoothScrollToPosition(0);
@@ -355,6 +358,24 @@
     }
 
     /**
+     * Update chart sweeps and cycle list to reflect {@link NetworkPolicy} for
+     * current {@link #mTemplate}.
+     */
+    private void updatePolicy() {
+        final NetworkPolicy policy = mPolicyModifier.getPolicy(mTemplate);
+
+        // reflect policy limit in checkbox
+        mDisableAtLimit.setChecked(policy != null && policy.limitBytes != LIMIT_DISABLED);
+        mChart.bindNetworkPolicy(policy);
+
+        // generate cycle list based on policy and available history
+        updateCycleList(policy);
+
+        // kick preference views so they rebind from changes above
+        refreshPreferenceViews();
+    }
+
+    /**
      * Return full time bounds (earliest and latest time recorded) of the given
      * {@link NetworkStatsHistory}.
      */
@@ -376,7 +397,7 @@
      * and available {@link NetworkStatsHistory} data. Always selects the newest
      * item, updating the inspection range on {@link #mChart}.
      */
-    private void updateCycleList() {
+    private void updateCycleList(NetworkPolicy policy) {
         mCycleAdapter.clear();
 
         final Context context = mCycleSpinner.getContext();
@@ -385,28 +406,36 @@
         final long historyStart = bounds[0];
         final long historyEnd = bounds[1];
 
-        // find the next cycle boundary
-        long cycleEnd = computeNextCycleBoundary(historyEnd, mPolicy);
+        if (policy != null) {
+            // find the next cycle boundary
+            long cycleEnd = computeNextCycleBoundary(historyEnd, policy);
 
-        int guardCount = 0;
+            int guardCount = 0;
 
-        // walk backwards, generating all valid cycle ranges
-        while (cycleEnd > historyStart) {
-            final long cycleStart = computeLastCycleBoundary(cycleEnd, mPolicy);
-            Log.d(TAG, "generating cs=" + cycleStart + " to ce=" + cycleEnd + " waiting for hs="
-                    + historyStart);
-            mCycleAdapter.add(new CycleItem(context, cycleStart, cycleEnd));
-            cycleEnd = cycleStart;
+            // walk backwards, generating all valid cycle ranges
+            while (cycleEnd > historyStart) {
+                final long cycleStart = computeLastCycleBoundary(cycleEnd, policy);
+                Log.d(TAG, "generating cs=" + cycleStart + " to ce=" + cycleEnd + " waiting for hs="
+                        + historyStart);
+                mCycleAdapter.add(new CycleItem(context, cycleStart, cycleEnd));
+                cycleEnd = cycleStart;
 
-            // TODO: remove this guard once we have better testing
-            if (guardCount++ > 50) {
-                Log.wtf(TAG, "stuck generating ranges for bounds=" + Arrays.toString(bounds)
-                        + " and policy=" + mPolicy);
+                // TODO: remove this guard once we have better testing
+                if (guardCount++ > 50) {
+                    Log.wtf(TAG, "stuck generating ranges for bounds=" + Arrays.toString(bounds)
+                            + " and policy=" + policy);
+                }
             }
-        }
 
-        // one last cycle entry to change date
-        mCycleAdapter.add(new CycleChangeItem(context));
+            // one last cycle entry to modify policy cycle day
+            mCycleAdapter.add(new CycleChangeItem(context));
+
+        } else {
+            // no valid cycle; show all data
+            // TODO: offer simple ranges like "last week" etc
+            mCycleAdapter.add(new CycleItem(context, historyStart, historyEnd));
+
+        }
 
         // force pick the current cycle (first item)
         mCycleSpinner.setSelection(0);
@@ -438,11 +467,11 @@
             mDisableAtLimit.setChecked(disableAtLimit);
             refreshPreferenceViews();
 
-            // TODO: push updated policy to service
+            // TODO: create policy if none exists
             // TODO: show interstitial warning dialog to user
-            final long limitBytes = disableAtLimit ? 5 * GB_IN_BYTES : -1;
-            mPolicy = new NetworkPolicy(mPolicy.cycleDay, mPolicy.warningBytes, limitBytes);
-            mChart.bindNetworkPolicy(mPolicy);
+            final long limitBytes = disableAtLimit ? 5 * GB_IN_BYTES : LIMIT_DISABLED;
+            mPolicyModifier.setPolicyLimitBytes(mTemplate, limitBytes);
+            updatePolicy();
         }
     };
 
@@ -500,6 +529,12 @@
         }
     }
 
+    private static String getActiveSubscriberId(Context context) {
+        final TelephonyManager telephony = (TelephonyManager) context.getSystemService(
+                Context.TELEPHONY_SERVICE);
+        return telephony.getSubscriberId();
+    }
+
     private DataUsageChartListener mChartListener = new DataUsageChartListener() {
         /** {@inheritDoc} */
         public void onInspectRangeChanged() {
@@ -508,26 +543,20 @@
         }
 
         /** {@inheritDoc} */
-        public void onLimitsChanged() {
-            if (LOGD) Log.d(TAG, "onLimitsChanged()");
-
-            // redefine policy and persist into service
-            // TODO: kick this onto background thread, since service touches disk
-
-            // TODO: remove this mPolicy null check, since later service will
-            // always define baseline value.
-            final int cycleDay = mPolicy != null ? mPolicy.cycleDay : 1;
+        public void onWarningChanged() {
+            if (LOGD) Log.d(TAG, "onWarningChanged()");
             final long warningBytes = mChart.getWarningBytes();
-            final long limitBytes = mDisableAtLimit.isChecked() ? -1 : mChart.getLimitBytes();
+            mPolicyModifier.setPolicyWarningBytes(mTemplate, warningBytes);
+            updatePolicy();
+        }
 
-            mPolicy = new NetworkPolicy(cycleDay, warningBytes, limitBytes);
-            if (LOGD) Log.d(TAG, "persisting policy=" + mPolicy);
-
-            try {
-                mPolicyService.setNetworkPolicy(mTemplate, null, mPolicy);
-            } catch (RemoteException e) {
-                Log.w(TAG, "problem persisting policy", e);
-            }
+        /** {@inheritDoc} */
+        public void onLimitChanged() {
+            if (LOGD) Log.d(TAG, "onLimitChanged()");
+            final long limitBytes = mDisableAtLimit.isChecked() ? mChart.getLimitBytes()
+                    : LIMIT_DISABLED;
+            mPolicyModifier.setPolicyLimitBytes(mTemplate, limitBytes);
+            updatePolicy();
         }
     };
 
@@ -605,7 +634,7 @@
         public void bindStats(NetworkStats stats) {
             mItems.clear();
 
-            for (int i = 0; i < stats.length(); i++) {
+            for (int i = 0; i < stats.size; i++) {
                 final AppUsageItem item = new AppUsageItem();
                 item.uid = stats.uid[i];
                 item.total = stats.rx[i] + stats.tx[i];
diff --git a/src/com/android/settings/net/NetworkPolicyModifier.java b/src/com/android/settings/net/NetworkPolicyModifier.java
new file mode 100644
index 0000000..1d8aca3
--- /dev/null
+++ b/src/com/android/settings/net/NetworkPolicyModifier.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2011 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.settings.net;
+
+import static android.net.TrafficStats.TEMPLATE_MOBILE_3G_LOWER;
+import static android.net.TrafficStats.TEMPLATE_MOBILE_4G;
+import static android.net.TrafficStats.TEMPLATE_MOBILE_ALL;
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.net.INetworkPolicyManager;
+import android.net.NetworkPolicy;
+import android.os.AsyncTask;
+import android.os.RemoteException;
+
+import com.android.internal.util.Objects;
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class to modify list of {@link NetworkPolicy}. Specifically knows
+ * about which policies can coexist.
+ */
+public class NetworkPolicyModifier {
+
+    private INetworkPolicyManager mPolicyService;
+    private String mSubscriberId;
+
+    private ArrayList<NetworkPolicy> mPolicies = Lists.newArrayList();
+
+    public NetworkPolicyModifier(INetworkPolicyManager policyService, String subscriberId) {
+        mPolicyService = checkNotNull(policyService);
+        mSubscriberId = subscriberId;
+    }
+
+    public void read() {
+        try {
+            final NetworkPolicy[] policies = mPolicyService.getNetworkPolicies();
+            mPolicies.clear();
+            for (NetworkPolicy policy : policies) {
+                mPolicies.add(policy);
+            }
+        } catch (RemoteException e) {
+            throw new RuntimeException("problem reading policies", e);
+        }
+    }
+
+    public void writeAsync() {
+        // TODO: consider making more robust by passing through service
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                write();
+                return null;
+            }
+        }.execute();
+    }
+
+    public void write() {
+        try {
+            final NetworkPolicy[] policies = mPolicies.toArray(new NetworkPolicy[mPolicies.size()]);
+            mPolicyService.setNetworkPolicies(policies);
+        } catch (RemoteException e) {
+            throw new RuntimeException("problem reading policies", e);
+        }
+    }
+
+    public NetworkPolicy getPolicy(int networkTemplate) {
+        for (NetworkPolicy policy : mPolicies) {
+            if (policy.networkTemplate == networkTemplate
+                    && Objects.equal(policy.subscriberId, mSubscriberId)) {
+                return policy;
+            }
+        }
+        return null;
+    }
+
+    public void setPolicyCycleDay(int networkTemplate, int cycleDay) {
+        getPolicy(networkTemplate).cycleDay = cycleDay;
+        writeAsync();
+    }
+
+    public void setPolicyWarningBytes(int networkTemplate, long warningBytes) {
+        getPolicy(networkTemplate).warningBytes = warningBytes;
+        writeAsync();
+    }
+
+    public void setPolicyLimitBytes(int networkTemplate, long limitBytes) {
+        getPolicy(networkTemplate).limitBytes = limitBytes;
+        writeAsync();
+    }
+
+    public boolean isMobilePolicySplit() {
+        return getPolicy(TEMPLATE_MOBILE_3G_LOWER) != null && getPolicy(TEMPLATE_MOBILE_4G) != null;
+    }
+
+    public void setMobilePolicySplit(boolean split) {
+        final boolean beforeSplit = isMobilePolicySplit();
+        if (split == beforeSplit) {
+            // already in requested state; skip
+            return;
+
+        } else if (beforeSplit && !split) {
+            // combine, picking most restrictive policy
+            final NetworkPolicy policy3g = getPolicy(TEMPLATE_MOBILE_3G_LOWER);
+            final NetworkPolicy policy4g = getPolicy(TEMPLATE_MOBILE_4G);
+
+            final NetworkPolicy restrictive = policy3g.compareTo(policy4g) < 0 ? policy3g
+                    : policy4g;
+            mPolicies.remove(policy3g);
+            mPolicies.remove(policy4g);
+            mPolicies.add(new NetworkPolicy(TEMPLATE_MOBILE_ALL, restrictive.subscriberId,
+                    restrictive.cycleDay, restrictive.warningBytes, restrictive.limitBytes));
+            writeAsync();
+
+        } else if (!beforeSplit && split) {
+            // duplicate existing policy into two rules
+            final NetworkPolicy policyAll = getPolicy(TEMPLATE_MOBILE_ALL);
+            mPolicies.remove(policyAll);
+            mPolicies.add(
+                    new NetworkPolicy(TEMPLATE_MOBILE_3G_LOWER, policyAll.subscriberId,
+                            policyAll.cycleDay, policyAll.warningBytes, policyAll.limitBytes));
+            mPolicies.add(
+                    new NetworkPolicy(TEMPLATE_MOBILE_4G, policyAll.subscriberId,
+                            policyAll.cycleDay, policyAll.warningBytes, policyAll.limitBytes));
+            writeAsync();
+
+        }
+    }
+
+}
diff --git a/src/com/android/settings/widget/DataUsageChartView.java b/src/com/android/settings/widget/DataUsageChartView.java
index defa953..6a702d0 100644
--- a/src/com/android/settings/widget/DataUsageChartView.java
+++ b/src/com/android/settings/widget/DataUsageChartView.java
@@ -21,6 +21,7 @@
 import android.net.NetworkPolicy;
 import android.net.NetworkStatsHistory;
 import android.text.format.DateUtils;
+import android.view.View;
 
 import com.android.settings.widget.ChartSweepView.OnSweepListener;
 
@@ -44,7 +45,8 @@
 
     public interface DataUsageChartListener {
         public void onInspectRangeChanged();
-        public void onLimitsChanged();
+        public void onWarningChanged();
+        public void onLimitChanged();
     }
 
     private DataUsageChartListener mListener;
@@ -78,6 +80,9 @@
 
         mSeries.bindSweepRange(mSweepTime1, mSweepTime2);
 
+        mSweepDataWarn.addOnSweepListener(mWarningListener);
+        mSweepDataLimit.addOnSweepListener(mLimitListener);
+
         mSweepTime1.addOnSweepListener(mSweepListener);
         mSweepTime2.addOnSweepListener(mSweepListener);
 
@@ -92,15 +97,29 @@
     }
 
     public void bindNetworkPolicy(NetworkPolicy policy) {
-        if (policy.limitBytes != -1) {
+        if (policy == null) {
+            mSweepDataLimit.setVisibility(View.INVISIBLE);
+            mSweepDataWarn.setVisibility(View.INVISIBLE);
+            return;
+        }
+
+        if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) {
+            mSweepDataLimit.setVisibility(View.VISIBLE);
             mSweepDataLimit.setValue(policy.limitBytes);
             mSweepDataLimit.setEnabled(true);
         } else {
+            // TODO: set limit default based on axis maximum
+            mSweepDataLimit.setVisibility(View.VISIBLE);
             mSweepDataLimit.setValue(5 * GB_IN_BYTES);
             mSweepDataLimit.setEnabled(false);
         }
 
-        mSweepDataWarn.setValue(policy.warningBytes);
+        if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) {
+            mSweepDataWarn.setVisibility(View.VISIBLE);
+            mSweepDataWarn.setValue(policy.warningBytes);
+        } else {
+            mSweepDataWarn.setVisibility(View.INVISIBLE);
+        }
     }
 
     private OnSweepListener mSweepListener = new OnSweepListener() {
@@ -115,6 +134,22 @@
         }
     };
 
+    private OnSweepListener mWarningListener = new OnSweepListener() {
+        public void onSweep(ChartSweepView sweep, boolean sweepDone) {
+            if (sweepDone && mListener != null) {
+                mListener.onWarningChanged();
+            }
+        }
+    };
+
+    private OnSweepListener mLimitListener = new OnSweepListener() {
+        public void onSweep(ChartSweepView sweep, boolean sweepDone) {
+            if (sweepDone && mListener != null) {
+                mListener.onLimitChanged();
+            }
+        }
+    };
+
     /**
      * Return current inspection range (start and end time) based on internal
      * {@link ChartSweepView} positions.