Merge "Data usage UI fixes; sweeps, combined history."
diff --git a/res/layout/data_usage_chart.xml b/res/layout/data_usage_chart.xml
index 7f63f3f..2e0cbdd 100644
--- a/res/layout/data_usage_chart.xml
+++ b/res/layout/data_usage_chart.xml
@@ -14,7 +14,7 @@
      limitations under the License.
 -->
 
-<com.android.settings.widget.DataUsageChartView
+<com.android.settings.widget.ChartDataUsageView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:settings="http://schemas.android.com/apk/res/com.android.settings"
     android:id="@+id/chart"
@@ -56,28 +56,6 @@
         settings:fillColorSecondary="#60ba7f3e" />
 
     <com.android.settings.widget.ChartSweepView
-        android:id="@+id/sweep_warning"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        settings:sweepDrawable="@drawable/data_sweep_warning"
-        settings:followAxis="vertical"
-        settings:neighborMargin="40dip"
-        settings:labelSize="60dip"
-        settings:labelTemplate="@string/data_usage_sweep_warning"
-        settings:labelColor="#f7931d" />
-
-    <com.android.settings.widget.ChartSweepView
-        android:id="@+id/sweep_limit"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        settings:sweepDrawable="@drawable/data_sweep_limit"
-        settings:followAxis="vertical"
-        settings:neighborMargin="40dip"
-        settings:labelSize="60dip"
-        settings:labelTemplate="@string/data_usage_sweep_limit"
-        settings:labelColor="#c01a2c" />
-
-    <com.android.settings.widget.ChartSweepView
         android:id="@+id/sweep_left"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
@@ -93,4 +71,26 @@
         settings:followAxis="horizontal"
         settings:neighborMargin="5dip" />
 
-</com.android.settings.widget.DataUsageChartView>
+    <com.android.settings.widget.ChartSweepView
+        android:id="@+id/sweep_warning"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        settings:sweepDrawable="@drawable/data_sweep_warning"
+        settings:followAxis="vertical"
+        settings:neighborMargin="5dip"
+        settings:labelSize="60dip"
+        settings:labelTemplate="@string/data_usage_sweep_warning"
+        settings:labelColor="#f7931d" />
+
+    <com.android.settings.widget.ChartSweepView
+        android:id="@+id/sweep_limit"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        settings:sweepDrawable="@drawable/data_sweep_limit"
+        settings:followAxis="vertical"
+        settings:neighborMargin="5dip"
+        settings:labelSize="60dip"
+        settings:labelTemplate="@string/data_usage_sweep_limit"
+        settings:labelColor="#c01a2c" />
+
+</com.android.settings.widget.ChartDataUsageView>
diff --git a/src/com/android/settings/DataUsageSummary.java b/src/com/android/settings/DataUsageSummary.java
index a6170c7..eb74788 100644
--- a/src/com/android/settings/DataUsageSummary.java
+++ b/src/com/android/settings/DataUsageSummary.java
@@ -122,12 +122,13 @@
 import com.android.internal.telephony.Phone;
 import com.android.settings.net.NetworkPolicyEditor;
 import com.android.settings.net.SummaryForAllUidLoader;
-import com.android.settings.widget.DataUsageChartView;
-import com.android.settings.widget.DataUsageChartView.DataUsageChartListener;
+import com.android.settings.widget.ChartDataUsageView;
+import com.android.settings.widget.ChartDataUsageView.DataUsageChartListener;
 import com.android.settings.widget.PieChartView;
 import com.google.android.collect.Lists;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Locale;
 
@@ -195,7 +196,7 @@
     private Spinner mCycleSpinner;
     private CycleAdapter mCycleAdapter;
 
-    private DataUsageChartView mChart;
+    private ChartDataUsageView mChart;
     private TextView mUsageSummary;
     private TextView mEmpty;
 
@@ -216,8 +217,7 @@
 
     private NetworkTemplate mTemplate = null;
 
-    private static final int UID_NONE = -1;
-    private int mUid = UID_NONE;
+    private int[] mAppDetailUids = null;
 
     private Intent mAppSettingsIntent;
 
@@ -307,7 +307,7 @@
         mCycleSpinner.setAdapter(mCycleAdapter);
         mCycleSpinner.setOnItemSelectedListener(mCycleListener);
 
-        mChart = (DataUsageChartView) mHeader.findViewById(R.id.chart);
+        mChart = (ChartDataUsageView) mHeader.findViewById(R.id.chart);
         mChart.setListener(mChartListener);
 
         {
@@ -611,8 +611,9 @@
             mTemplate = buildTemplateMobile4g(getActiveSubscriberId(context));
 
         } else if (TAB_WIFI.equals(currentTab)) {
+            // wifi doesn't have any controls
             mDataEnabledView.setVisibility(View.GONE);
-            setPreferenceTitle(mDisableAtLimitView, R.string.data_usage_disable_wifi_limit);
+            mDisableAtLimitView.setVisibility(View.GONE);
             mTemplate = buildTemplateWifi();
 
         } else if (TAB_ETHERNET.equals(currentTab)) {
@@ -649,12 +650,16 @@
     }
 
     private boolean isAppDetailMode() {
-        return mUid != UID_NONE;
+        return mAppDetailUids != null;
+    }
+
+    private int getAppDetailPrimaryUid() {
+        return mAppDetailUids[0];
     }
 
     /**
-     * Update UID details panels to match {@link #mUid}, showing or hiding them
-     * depending on {@link #isAppDetailMode()}.
+     * Update UID details panels to match {@link #mAppDetailUids}, showing or
+     * hiding them depending on {@link #isAppDetailMode()}.
      */
     private void updateAppDetail() {
         final Context context = getActivity();
@@ -681,7 +686,8 @@
         mChart.bindNetworkPolicy(null);
 
         // show icon and all labels appearing under this app
-        final UidDetail detail = resolveDetailForUid(context, mUid);
+        final int primaryUid = getAppDetailPrimaryUid();
+        final UidDetail detail = resolveDetailForUid(context, primaryUid);
         mAppIcon.setImageDrawable(detail.icon);
 
         mAppTitles.removeAllViews();
@@ -695,7 +701,7 @@
 
         // enable settings button when package provides it
         // TODO: target torwards entire UID instead of just first package
-        final String[] packageNames = pm.getPackagesForUid(mUid);
+        final String[] packageNames = pm.getPackagesForUid(primaryUid);
         if (packageNames != null && packageNames.length > 0) {
             mAppSettingsIntent = new Intent(Intent.ACTION_MANAGE_NETWORK_USAGE);
             mAppSettingsIntent.setPackage(packageNames[0]);
@@ -709,12 +715,40 @@
             mAppSettings.setEnabled(false);
         }
 
+        updateDetailHistory();
+        updateDetailData();
+
+        if (NetworkPolicyManager.isUidValidForPolicy(context, primaryUid)
+                && !getRestrictBackground() && isBandwidthControlEnabled()) {
+            setPreferenceTitle(mAppRestrictView, R.string.data_usage_app_restrict_background);
+            setPreferenceSummary(mAppRestrictView,
+                    getString(R.string.data_usage_app_restrict_background_summary,
+                            buildLimitedNetworksList()));
+
+            mAppRestrictView.setVisibility(View.VISIBLE);
+            mAppRestrict.setChecked(getAppRestrictBackground());
+
+        } else {
+            mAppRestrictView.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * Update {@link #mDetailHistory} and related values based on
+     * {@link #mAppDetailUids}.
+     */
+    private void updateDetailHistory() {
         try {
+            mDetailHistoryDefault = null;
+            mDetailHistoryForeground = null;
+
             // load stats for current uid and template
-            mDetailHistoryDefault = mStatsService.getHistoryForUid(
-                    mTemplate, mUid, SET_DEFAULT, TAG_NONE, FIELD_RX_BYTES | FIELD_TX_BYTES);
-            mDetailHistoryForeground = mStatsService.getHistoryForUid(
-                    mTemplate, mUid, SET_FOREGROUND, TAG_NONE, FIELD_RX_BYTES | FIELD_TX_BYTES);
+            for (int uid : mAppDetailUids) {
+                mDetailHistoryDefault = collectHistoryForUid(
+                        uid, SET_DEFAULT, mDetailHistoryDefault);
+                mDetailHistoryForeground = collectHistoryForUid(
+                        uid, SET_FOREGROUND, mDetailHistoryForeground);
+            }
         } catch (RemoteException e) {
             // since we can't do much without history, and we don't want to
             // leave with half-baked UI, we bail hard.
@@ -727,23 +761,24 @@
 
         // bind chart to historical stats
         mChart.bindDetailNetworkStats(mDetailHistory);
+    }
 
-        updateDetailData();
+    /**
+     * Collect {@link NetworkStatsHistory} for the requested UID, combining with
+     * an existing {@link NetworkStatsHistory} if provided.
+     */
+    private NetworkStatsHistory collectHistoryForUid(
+            int uid, int set, NetworkStatsHistory existing)
+            throws RemoteException {
+        final NetworkStatsHistory history = mStatsService.getHistoryForUid(
+                mTemplate, uid, set, TAG_NONE, FIELD_RX_BYTES | FIELD_TX_BYTES);
 
-        if (NetworkPolicyManager.isUidValidForPolicy(context, mUid) && !getRestrictBackground()
-                && isBandwidthControlEnabled()) {
-            setPreferenceTitle(mAppRestrictView, R.string.data_usage_app_restrict_background);
-            setPreferenceSummary(mAppRestrictView,
-                    getString(R.string.data_usage_app_restrict_background_summary,
-                            buildLimitedNetworksList()));
-
-            mAppRestrictView.setVisibility(View.VISIBLE);
-            mAppRestrict.setChecked(getAppRestrictBackground());
-
+        if (existing != null) {
+            existing.recordEntireHistory(history);
+            return existing;
         } else {
-            mAppRestrictView.setVisibility(View.GONE);
+            return history;
         }
-
     }
 
     private void setPolicyCycleDay(int cycleDay) {
@@ -764,8 +799,8 @@
         updatePolicy(false);
     }
 
-    private boolean isNetworkPolicyModifiable() {
-        return isBandwidthControlEnabled() && mDataEnabled.isChecked();
+    private boolean isNetworkPolicyModifiable(NetworkPolicy policy) {
+        return policy != null && isBandwidthControlEnabled() && mDataEnabled.isChecked();
     }
 
     private boolean isBandwidthControlEnabled() {
@@ -810,9 +845,10 @@
     }
 
     private boolean getAppRestrictBackground() {
+        final int primaryUid = getAppDetailPrimaryUid();
         final int uidPolicy;
         try {
-            uidPolicy = mPolicyService.getUidPolicy(mUid);
+            uidPolicy = mPolicyService.getUidPolicy(primaryUid);
         } catch (RemoteException e) {
             // since we can't do much without policy, we bail hard.
             throw new RuntimeException("problem reading network policy", e);
@@ -823,9 +859,10 @@
 
     private void setAppRestrictBackground(boolean restrictBackground) {
         if (LOGD) Log.d(TAG, "setAppRestrictBackground()");
+        final int primaryUid = getAppDetailPrimaryUid();
         try {
-            mPolicyService.setUidPolicy(
-                    mUid, restrictBackground ? POLICY_REJECT_METERED_BACKGROUND : POLICY_NONE);
+            mPolicyService.setUidPolicy(primaryUid,
+                    restrictBackground ? POLICY_REJECT_METERED_BACKGROUND : POLICY_NONE);
         } catch (RemoteException e) {
             throw new RuntimeException("unable to save policy", e);
         }
@@ -851,8 +888,8 @@
             }
         }
 
-        final NetworkPolicy policy = mPolicyEditor.getPolicy(mTemplate, true);
-        if (isNetworkPolicyModifiable()) {
+        final NetworkPolicy policy = mPolicyEditor.getPolicy(mTemplate);
+        if (isNetworkPolicyModifiable(policy)) {
             mDisableAtLimitView.setVisibility(View.VISIBLE);
             mDisableAtLimit.setChecked(policy != null && policy.limitBytes != LIMIT_DISABLED);
             if (!isAppDetailMode()) {
@@ -912,19 +949,28 @@
             }
 
             // one last cycle entry to modify policy cycle day
-            mCycleAdapter.setChangePossible(isNetworkPolicyModifiable());
+            mCycleAdapter.setChangePossible(isNetworkPolicyModifiable(policy));
         }
 
         if (!hasCycles) {
-            // no valid cycles; show all data
-            // TODO: offer simple ranges like "last week" etc
-            mCycleAdapter.add(new CycleItem(context, historyStart, historyEnd));
+            // no policy defined cycles; show entry for each four-week period
+            long cycleEnd = historyEnd;
+            while (cycleEnd > historyStart) {
+                final long cycleStart = cycleEnd - (DateUtils.WEEK_IN_MILLIS * 4);
+                mCycleAdapter.add(new CycleItem(context, cycleStart, cycleEnd));
+                cycleEnd = cycleStart;
+            }
+
             mCycleAdapter.setChangePossible(false);
         }
 
         // force pick the current cycle (first item)
-        mCycleSpinner.setSelection(0);
-        mCycleListener.onItemSelected(mCycleSpinner, null, 0, 0);
+        if (mCycleAdapter.getCount() > 0) {
+            mCycleSpinner.setSelection(0);
+            mCycleListener.onItemSelected(mCycleSpinner, null, 0, 0);
+        } else {
+            updateDetailData();
+        }
     }
 
     private OnCheckedChangeListener mDataEnabledListener = new OnCheckedChangeListener() {
@@ -985,8 +1031,10 @@
     private OnItemClickListener mListListener = new OnItemClickListener() {
         /** {@inheritDoc} */
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            final Context context = view.getContext();
             final AppUsageItem app = (AppUsageItem) parent.getItemAtPosition(position);
-            AppDetailsFragment.show(DataUsageSummary.this, app.uid);
+            final UidDetail detail = resolveDetailForUid(context, app.uids[0]);
+            AppDetailsFragment.show(DataUsageSummary.this, app.uids, detail.label);
         }
     };
 
@@ -1065,6 +1113,7 @@
             entry = mHistory.getValues(start, end, now, null);
 
             // kick off loader for detailed stats
+            // TODO: delay loader until animation is finished
             getLoaderManager().restartLoader(LOADER_SUMMARY,
                     SummaryForAllUidLoader.buildArgs(mTemplate, start, end), mSummaryForAllUid);
         }
@@ -1222,9 +1271,20 @@
     }
 
     private static class AppUsageItem implements Comparable<AppUsageItem> {
-        public int uid;
+        public int[] uids;
         public long total;
 
+        public AppUsageItem(int uid) {
+            uids = new int[] { uid };
+        }
+
+        public void addUid(int uid) {
+            if (contains(uids, uid)) return;
+            final int length = uids.length;
+            uids = Arrays.copyOf(uids, length + 1);
+            uids[length] = uid;
+        }
+
         /** {@inheritDoc} */
         public int compareTo(AppUsageItem another) {
             return Long.compare(another.total, total);
@@ -1244,9 +1304,7 @@
         public void bindStats(NetworkStats stats) {
             mItems.clear();
 
-            final AppUsageItem systemItem = new AppUsageItem();
-            systemItem.uid = android.os.Process.SYSTEM_UID;
-
+            final AppUsageItem systemItem = new AppUsageItem(android.os.Process.SYSTEM_UID);
             final SparseArray<AppUsageItem> knownUids = new SparseArray<AppUsageItem>();
 
             NetworkStats.Entry entry = null;
@@ -1260,8 +1318,7 @@
                 if (isApp || uid == TrafficStats.UID_REMOVED) {
                     AppUsageItem item = knownUids.get(uid);
                     if (item == null) {
-                        item = new AppUsageItem();
-                        item.uid = uid;
+                        item = new AppUsageItem(uid);
                         knownUids.put(uid, item);
                         mItems.add(item);
                     }
@@ -1269,6 +1326,7 @@
                     item.total += entry.rxBytes + entry.txBytes;
                 } else {
                     systemItem.total += entry.rxBytes + entry.txBytes;
+                    systemItem.addUid(uid);
                 }
             }
 
@@ -1293,7 +1351,7 @@
 
         @Override
         public long getItemId(int position) {
-            return mItems.get(position).uid;
+            return mItems.get(position).uids[0];
         }
 
         @Override
@@ -1312,7 +1370,7 @@
                     android.R.id.progress);
 
             final AppUsageItem item = mItems.get(position);
-            final UidDetail detail = resolveDetailForUid(context, item.uid);
+            final UidDetail detail = resolveDetailForUid(context, item.uids[0]);
 
             icon.setImageDrawable(detail.icon);
             title.setText(detail.label);
@@ -1323,7 +1381,6 @@
 
             return convertView;
         }
-
     }
 
     /**
@@ -1331,11 +1388,11 @@
      * {@link DataUsageSummary}.
      */
     public static class AppDetailsFragment extends Fragment {
-        private static final String EXTRA_UID = "uid";
+        private static final String EXTRA_UIDS = "uids";
 
-        public static void show(DataUsageSummary parent, int uid) {
+        public static void show(DataUsageSummary parent, int[] uids, CharSequence label) {
             final Bundle args = new Bundle();
-            args.putInt(EXTRA_UID, uid);
+            args.putIntArray(EXTRA_UIDS, uids);
 
             final AppDetailsFragment fragment = new AppDetailsFragment();
             fragment.setArguments(args);
@@ -1344,6 +1401,7 @@
             final FragmentTransaction ft = parent.getFragmentManager().beginTransaction();
             ft.add(fragment, TAG_APP_DETAILS);
             ft.addToBackStack(TAG_APP_DETAILS);
+            ft.setBreadCrumbTitle(label);
             ft.commit();
         }
 
@@ -1351,7 +1409,7 @@
         public void onStart() {
             super.onStart();
             final DataUsageSummary target = (DataUsageSummary) getTargetFragment();
-            target.mUid = getArguments().getInt(EXTRA_UID);
+            target.mAppDetailUids = getArguments().getIntArray(EXTRA_UIDS);
             target.updateBody();
         }
 
@@ -1359,7 +1417,7 @@
         public void onStop() {
             super.onStop();
             final DataUsageSummary target = (DataUsageSummary) getTargetFragment();
-            target.mUid = UID_NONE;
+            target.mAppDetailUids = null;
             target.updateBody();
         }
     }
@@ -1441,7 +1499,7 @@
         private static final String EXTRA_CYCLE_DAY = "cycleDay";
 
         public static void show(DataUsageSummary parent) {
-            final NetworkPolicy policy = parent.mPolicyEditor.getPolicy(parent.mTemplate, false);
+            final NetworkPolicy policy = parent.mPolicyEditor.getPolicy(parent.mTemplate);
             final Bundle args = new Bundle();
             args.putInt(CycleEditorFragment.EXTRA_CYCLE_DAY, policy.cycleDay);
 
@@ -1807,4 +1865,13 @@
         summary.setVisibility(View.VISIBLE);
         summary.setText(string);
     }
+
+    private static boolean contains(int[] haystack, int needle) {
+        for (int value : haystack) {
+            if (value == needle) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/src/com/android/settings/net/NetworkPolicyEditor.java b/src/com/android/settings/net/NetworkPolicyEditor.java
index 723c5cc..161e6ee 100644
--- a/src/com/android/settings/net/NetworkPolicyEditor.java
+++ b/src/com/android/settings/net/NetworkPolicyEditor.java
@@ -21,6 +21,7 @@
 import static android.net.NetworkPolicy.WARNING_DISABLED;
 import static android.net.NetworkTemplate.MATCH_MOBILE_3G_LOWER;
 import static android.net.NetworkTemplate.MATCH_MOBILE_4G;
+import static android.net.NetworkTemplate.MATCH_WIFI;
 import static android.net.NetworkTemplate.buildTemplateMobile3gLower;
 import static android.net.NetworkTemplate.buildTemplateMobile4g;
 import static android.net.NetworkTemplate.buildTemplateMobileAll;
@@ -40,7 +41,7 @@
 
 /**
  * Utility class to modify list of {@link NetworkPolicy}. Specifically knows
- * about which policies can coexist.
+ * about which policies can coexist. Not thread safe.
  */
 public class NetworkPolicyEditor {
     // TODO: be more robust when missing policies from service
@@ -53,64 +54,80 @@
     }
 
     public void read() {
+        final NetworkPolicy[] policies;
         try {
-            final NetworkPolicy[] policies = mPolicyService.getNetworkPolicies();
-            mPolicies.clear();
-            for (NetworkPolicy policy : policies) {
-                // TODO: find better place to clamp these
-                if (policy.limitBytes < -1) {
-                    policy.limitBytes = LIMIT_DISABLED;
-                }
-                if (policy.warningBytes < -1) {
-                    policy.warningBytes = WARNING_DISABLED;
-                }
-
-                mPolicies.add(policy);
-            }
+            policies = mPolicyService.getNetworkPolicies();
         } catch (RemoteException e) {
             throw new RuntimeException("problem reading policies", e);
         }
+
+        boolean modified = false;
+        mPolicies.clear();
+        for (NetworkPolicy policy : policies) {
+            // TODO: find better place to clamp these
+            if (policy.limitBytes < -1) {
+                policy.limitBytes = LIMIT_DISABLED;
+                modified = true;
+            }
+            if (policy.warningBytes < -1) {
+                policy.warningBytes = WARNING_DISABLED;
+                modified = true;
+            }
+
+            // drop any WIFI policies that were defined
+            if (policy.template.getMatchRule() == MATCH_WIFI) {
+                modified = true;
+                continue;
+            }
+
+            mPolicies.add(policy);
+        }
+
+        // when we cleaned policies above, write back changes
+        if (modified) writeAsync();
     }
 
     public void writeAsync() {
         // TODO: consider making more robust by passing through service
+        final NetworkPolicy[] policies = mPolicies.toArray(new NetworkPolicy[mPolicies.size()]);
         new AsyncTask<Void, Void, Void>() {
             @Override
             protected Void doInBackground(Void... params) {
-                write();
+                write(policies);
                 return null;
             }
         }.execute();
     }
 
-    public void write() {
+    public void write(NetworkPolicy[] policies) {
         try {
-            final NetworkPolicy[] policies = mPolicies.toArray(new NetworkPolicy[mPolicies.size()]);
             mPolicyService.setNetworkPolicies(policies);
         } catch (RemoteException e) {
-            throw new RuntimeException("problem reading policies", e);
+            throw new RuntimeException("problem writing policies", e);
         }
     }
 
     public boolean hasLimitedPolicy(NetworkTemplate template) {
-        final NetworkPolicy policy = getPolicy(template, false);
+        final NetworkPolicy policy = getPolicy(template);
         return policy != null && policy.limitBytes != LIMIT_DISABLED;
     }
 
-    public NetworkPolicy getPolicy(NetworkTemplate template, boolean createDefault) {
+    public NetworkPolicy getOrCreatePolicy(NetworkTemplate template) {
+        NetworkPolicy policy = getPolicy(template);
+        if (policy == null) {
+            policy = buildDefaultPolicy(template);
+            mPolicies.add(policy);
+        }
+        return policy;
+    }
+
+    public NetworkPolicy getPolicy(NetworkTemplate template) {
         for (NetworkPolicy policy : mPolicies) {
             if (policy.template.equals(template)) {
                 return policy;
             }
         }
-
-        if (createDefault) {
-            final NetworkPolicy policy = buildDefaultPolicy(template);
-            mPolicies.add(policy);
-            return policy;
-        } else {
-            return null;
-        }
+        return null;
     }
 
     private static NetworkPolicy buildDefaultPolicy(NetworkTemplate template) {
@@ -124,21 +141,21 @@
     }
 
     public void setPolicyCycleDay(NetworkTemplate template, int cycleDay) {
-        final NetworkPolicy policy = getPolicy(template, true);
+        final NetworkPolicy policy = getOrCreatePolicy(template);
         policy.cycleDay = cycleDay;
         policy.lastSnooze = SNOOZE_NEVER;
         writeAsync();
     }
 
     public void setPolicyWarningBytes(NetworkTemplate template, long warningBytes) {
-        final NetworkPolicy policy = getPolicy(template, true);
+        final NetworkPolicy policy = getOrCreatePolicy(template);
         policy.warningBytes = warningBytes;
         policy.lastSnooze = SNOOZE_NEVER;
         writeAsync();
     }
 
     public void setPolicyLimitBytes(NetworkTemplate template, long limitBytes) {
-        final NetworkPolicy policy = getPolicy(template, true);
+        final NetworkPolicy policy = getOrCreatePolicy(template);
         policy.limitBytes = limitBytes;
         policy.lastSnooze = SNOOZE_NEVER;
         writeAsync();
@@ -176,8 +193,8 @@
 
         } else if (beforeSplit && !split) {
             // combine, picking most restrictive policy
-            final NetworkPolicy policy3g = getPolicy(template3g, false);
-            final NetworkPolicy policy4g = getPolicy(template4g, false);
+            final NetworkPolicy policy3g = getPolicy(template3g);
+            final NetworkPolicy policy4g = getPolicy(template4g);
 
             final NetworkPolicy restrictive = policy3g.compareTo(policy4g) < 0 ? policy3g
                     : policy4g;
@@ -190,7 +207,7 @@
 
         } else if (!beforeSplit && split) {
             // duplicate existing policy into two rules
-            final NetworkPolicy policyAll = getPolicy(templateAll, false);
+            final NetworkPolicy policyAll = getPolicy(templateAll);
             mPolicies.remove(policyAll);
             mPolicies.add(
                     new NetworkPolicy(template3g, policyAll.cycleDay, policyAll.warningBytes,
diff --git a/src/com/android/settings/widget/DataUsageChartView.java b/src/com/android/settings/widget/ChartDataUsageView.java
similarity index 90%
rename from src/com/android/settings/widget/DataUsageChartView.java
rename to src/com/android/settings/widget/ChartDataUsageView.java
index f6ae5a0..9554368 100644
--- a/src/com/android/settings/widget/DataUsageChartView.java
+++ b/src/com/android/settings/widget/ChartDataUsageView.java
@@ -27,6 +27,7 @@
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.util.AttributeSet;
+import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.View;
 
@@ -37,7 +38,7 @@
  * Specific {@link ChartView} that displays {@link ChartNetworkSeriesView} along
  * with {@link ChartSweepView} for inspection ranges and warning/limits.
  */
-public class DataUsageChartView extends ChartView {
+public class ChartDataUsageView extends ChartView {
 
     private static final long KB_IN_BYTES = 1024;
     private static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
@@ -46,6 +47,8 @@
     private static final int MSG_UPDATE_AXIS = 100;
     private static final long DELAY_MILLIS = 250;
 
+    private static final boolean LIMIT_SWEEPS_TO_VALID_DATA = false;
+
     private ChartGridView mGrid;
     private ChartNetworkSeriesView mSeries;
     private ChartNetworkSeriesView mDetailSeries;
@@ -70,15 +73,15 @@
 
     private DataUsageChartListener mListener;
 
-    public DataUsageChartView(Context context) {
+    public ChartDataUsageView(Context context) {
         this(context, null, 0);
     }
 
-    public DataUsageChartView(Context context, AttributeSet attrs) {
+    public ChartDataUsageView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
 
-    public DataUsageChartView(Context context, AttributeSet attrs, int defStyle) {
+    public ChartDataUsageView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         init(new TimeAxis(), new InvertedChartAxis(new DataAxis()));
 
@@ -186,6 +189,7 @@
 
         updateVertAxisBounds(null);
         requestLayout();
+        invalidate();
     }
 
     /**
@@ -194,7 +198,8 @@
      */
     private void updateVertAxisBounds(ChartSweepView activeSweep) {
         final long max = mVertMax;
-        final long newMax;
+
+        long newMax = 0;
         if (activeSweep != null) {
             final int adjustAxis = activeSweep.shouldAdjustAxis();
             if (adjustAxis > 0) {
@@ -206,14 +211,14 @@
             } else {
                 newMax = max;
             }
-
-        } else {
-            // try showing all known data and policy
-            final long maxSweep = Math.max(mSweepWarning.getValue(), mSweepLimit.getValue());
-            final long maxVisible = Math.max(mSeries.getMaxVisible(), maxSweep) * 12 / 10;
-            newMax = Math.max(maxVisible, 2 * GB_IN_BYTES);
         }
 
+        // always show known data and policy lines
+        final long maxSweep = Math.max(mSweepWarning.getValue(), mSweepLimit.getValue());
+        final long maxVisible = Math.max(mSeries.getMaxVisible(), maxSweep) * 12 / 10;
+        final long maxDefault = Math.max(maxVisible, 2 * GB_IN_BYTES);
+        newMax = Math.max(maxDefault, newMax);
+
         // only invalidate when vertMax actually changed
         if (newMax != mVertMax) {
             mVertMax = newMax;
@@ -231,6 +236,16 @@
             if (activeSweep != null) {
                 activeSweep.updateValueFromPosition();
             }
+
+            // layout other sweeps to match changed axis
+            // TODO: find cleaner way of doing this, such as requesting full
+            // layout and making activeSweep discard its tracking MotionEvent.
+            if (mSweepLimit != activeSweep) {
+                layoutSweep(mSweepLimit);
+            }
+            if (mSweepWarning != activeSweep) {
+                layoutSweep(mSweepWarning);
+            }
         }
     }
 
@@ -346,9 +361,14 @@
         final long validStart = Math.max(visibleStart, getStatsStart());
         final long validEnd = Math.min(visibleEnd, getStatsEnd());
 
-        // prevent time sweeps from leaving valid data
-        mSweepLeft.setValidRange(validStart, validEnd);
-        mSweepRight.setValidRange(validStart, validEnd);
+        if (LIMIT_SWEEPS_TO_VALID_DATA) {
+            // prevent time sweeps from leaving valid data
+            mSweepLeft.setValidRange(validStart, validEnd);
+            mSweepRight.setValidRange(validStart, validEnd);
+        } else {
+            mSweepLeft.setValidRange(visibleStart, visibleEnd);
+            mSweepRight.setValidRange(visibleStart, visibleEnd);
+        }
 
         // default sweeps to last week of data
         final long halfRange = (visibleEnd + visibleStart) / 2;
@@ -424,7 +444,7 @@
             final int tickCount = (int) ((mMax - mMin) / TICK_INTERVAL);
             final float[] tickPoints = new float[tickCount];
             for (int i = 0; i < tickCount; i++) {
-                tickPoints[i] = convertToPoint(mMax - (TICK_INTERVAL * i));
+                tickPoints[i] = convertToPoint(mMax - (TICK_INTERVAL * (i + 1)));
             }
             return tickPoints;
         }
@@ -501,7 +521,14 @@
         /** {@inheritDoc} */
         public float[] getTickPoints() {
             final long range = mMax - mMin;
-            final long tickJump = 256 * MB_IN_BYTES;
+            final long tickJump;
+            if (range < 6 * GB_IN_BYTES) {
+                tickJump = 256 * MB_IN_BYTES;
+            } else if (range < 12 * GB_IN_BYTES) {
+                tickJump = 512 * MB_IN_BYTES;
+            } else {
+                tickJump = 1 * GB_IN_BYTES;
+            }
 
             final int tickCount = (int) (range / tickJump);
             final float[] tickPoints = new float[tickCount];
diff --git a/src/com/android/settings/widget/ChartSweepView.java b/src/com/android/settings/widget/ChartSweepView.java
index 81aeb84..0d91a76 100644
--- a/src/com/android/settings/widget/ChartSweepView.java
+++ b/src/com/android/settings/widget/ChartSweepView.java
@@ -21,6 +21,7 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
+import android.graphics.Paint.Style;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -42,8 +43,14 @@
  */
 public class ChartSweepView extends View {
 
+    private static final boolean DRAW_OUTLINE = false;
+
     private Drawable mSweep;
     private Rect mSweepPadding = new Rect();
+
+    /** Offset of content inside this view. */
+    private Point mContentOffset = new Point();
+    /** Offset of {@link #mSweep} inside this view. */
     private Point mSweepOffset = new Point();
 
     private Rect mMargins = new Rect();
@@ -66,6 +73,8 @@
     private ChartSweepView mValidAfterDynamic;
     private ChartSweepView mValidBeforeDynamic;
 
+    private Paint mOutlinePaint = new Paint();
+
     public static final int HORIZONTAL = 0;
     public static final int VERTICAL = 1;
 
@@ -98,6 +107,10 @@
         setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0));
         setLabelColor(a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE));
 
+        mOutlinePaint.setColor(Color.RED);
+        mOutlinePaint.setStrokeWidth(1f);
+        mOutlinePaint.setStyle(Style.STROKE);
+
         a.recycle();
 
         setWillNotDraw(false);
@@ -123,11 +136,11 @@
         if (mFollowAxis == VERTICAL) {
             final float targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
                     - mSweepPadding.bottom;
-            return mSweepPadding.top + (targetHeight / 2);
+            return mSweepPadding.top + (targetHeight / 2) + mSweepOffset.y;
         } else {
             final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
                     - mSweepPadding.right;
-            return mSweepPadding.left + (targetWidth / 2);
+            return mSweepPadding.left + (targetWidth / 2) + mSweepOffset.x;
         }
     }
 
@@ -195,6 +208,7 @@
             paint.density = getResources().getDisplayMetrics().density;
             paint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
             paint.setColor(mLabelColor);
+            paint.setShadowLayer(4 * paint.density, 0, 0, Color.BLACK);
 
             mLabelTemplate = new SpannableStringBuilder(template);
             mLabelLayout = new DynamicLayout(
@@ -283,6 +297,26 @@
         mValidBeforeDynamic = validBefore;
     }
 
+    /**
+     * Test if given {@link MotionEvent} is closer to another
+     * {@link ChartSweepView} compared to ourselves.
+     */
+    public boolean isTouchCloserTo(MotionEvent eventInParent, ChartSweepView another) {
+        if (another == null) return false;
+
+        if (mFollowAxis == HORIZONTAL) {
+            final float selfDist = Math.abs(eventInParent.getX() - (getX() + getTargetInset()));
+            final float anotherDist = Math.abs(
+                    eventInParent.getX() - (another.getX() + another.getTargetInset()));
+            return anotherDist < selfDist;
+        } else {
+            final float selfDist = Math.abs(eventInParent.getY() - (getY() + getTargetInset()));
+            final float anotherDist = Math.abs(
+                    eventInParent.getY() - (another.getY() + another.getTargetInset()));
+            return anotherDist < selfDist;
+        }
+    }
+
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         if (!isEnabled()) return false;
@@ -294,9 +328,18 @@
                 // only start tracking when in sweet spot
                 final boolean accept;
                 if (mFollowAxis == VERTICAL) {
-                    accept = event.getX() > getWidth() - (mSweepPadding.right * 2);
+                    accept = event.getX() > getWidth() - (mSweepPadding.right * 3);
                 } else {
-                    accept = event.getY() > getHeight() - (mSweepPadding.bottom * 2);
+                    accept = event.getY() > getHeight() - (mSweepPadding.bottom * 3);
+                }
+
+                final MotionEvent eventInParent = event.copy();
+                eventInParent.offsetLocation(getLeft(), getTop());
+
+                // ignore event when closer to a neighbor
+                if (isTouchCloserTo(eventInParent, mValidAfterDynamic)
+                        || isTouchCloserTo(eventInParent, mValidBeforeDynamic)) {
+                    return false;
                 }
 
                 if (accept) {
@@ -460,6 +503,7 @@
             final int templateHeight = mLabelLayout.getHeight();
 
             mSweepOffset.x = 0;
+            mSweepOffset.y = 0;
             mSweepOffset.y = (int) ((templateHeight / 2) - getTargetInset());
             setMeasuredDimension(mSweep.getIntrinsicWidth(), Math.max(sweepHeight, templateHeight));
 
@@ -485,6 +529,23 @@
             mMargins.bottom = mSweepPadding.bottom;
         }
 
+        mContentOffset.x = 0;
+        mContentOffset.y = 0;
+
+        // make touch target area larger
+        if (mFollowAxis == HORIZONTAL) {
+            final int widthBefore = getMeasuredWidth();
+            final int widthAfter = widthBefore * 3;
+            setMeasuredDimension(widthAfter, getMeasuredHeight());
+            mContentOffset.offset((widthAfter - widthBefore) / 2, 0);
+        } else {
+            final int heightBefore = getMeasuredHeight();
+            final int heightAfter = heightBefore * 3;
+            setMeasuredDimension(getMeasuredWidth(), heightAfter);
+            mContentOffset.offset(0, (heightAfter - heightBefore) / 2);
+        }
+
+        mSweepOffset.offset(mContentOffset.x, mContentOffset.y);
         mMargins.offset(-mSweepOffset.x, -mSweepOffset.y);
     }
 
@@ -493,9 +554,43 @@
         final int width = getWidth();
         final int height = getHeight();
 
+        if (DRAW_OUTLINE) {
+            canvas.drawRect(0, 0, width, height, mOutlinePaint);
+        }
+
+        // when overlapping with neighbor, split difference and push label
+        float margin;
+        float labelOffset = 0;
+        if (mFollowAxis == VERTICAL) {
+            if (mValidAfterDynamic != null) {
+                margin = getLabelTop(mValidAfterDynamic) - getLabelBottom(this);
+                if (margin < 0) {
+                    labelOffset = margin / 2;
+                }
+            } else if (mValidBeforeDynamic != null) {
+                margin = getLabelTop(this) - getLabelBottom(mValidBeforeDynamic);
+                if (margin < 0) {
+                    labelOffset = -margin / 2;
+                }
+            }
+        } else {
+            // TODO: implement horizontal labels
+        }
+
+        // when offsetting label, neighbor probably needs to offset too
+        if (labelOffset != 0) {
+            if (mValidAfterDynamic != null) mValidAfterDynamic.invalidate();
+            if (mValidBeforeDynamic != null) mValidBeforeDynamic.invalidate();
+        }
+
         final int labelSize;
         if (isEnabled() && mLabelLayout != null) {
-            mLabelLayout.draw(canvas);
+            final int count = canvas.save();
+            {
+                canvas.translate(mContentOffset.x, mContentOffset.y + labelOffset);
+                mLabelLayout.draw(canvas);
+            }
+            canvas.restoreToCount(count);
             labelSize = mLabelSize;
         } else {
             labelSize = 0;
@@ -512,4 +607,11 @@
         mSweep.draw(canvas);
     }
 
+    public static float getLabelTop(ChartSweepView view) {
+        return view.getY() + view.mContentOffset.y;
+    }
+
+    public static float getLabelBottom(ChartSweepView view) {
+        return getLabelTop(view) + view.mLabelLayout.getHeight();
+    }
 }
diff --git a/src/com/android/settings/widget/ChartView.java b/src/com/android/settings/widget/ChartView.java
index e3a658a..f410d57 100644
--- a/src/com/android/settings/widget/ChartView.java
+++ b/src/com/android/settings/widget/ChartView.java
@@ -36,8 +36,6 @@
  * and screen coordinates.
  */
 public class ChartView extends FrameLayout {
-    private static final String TAG = "ChartView";
-
     // TODO: extend something that supports two-dimensional scrolling
 
     private static final int SWEEP_GRAVITY = Gravity.TOP | Gravity.LEFT;
@@ -122,29 +120,39 @@
                 child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
 
             } else if (child instanceof ChartSweepView) {
-                // sweep is always placed along specific dimension
-                final ChartSweepView sweep = (ChartSweepView) child;
-                final Rect sweepMargins = sweep.getMargins();
-
-                if (sweep.getFollowAxis() == ChartSweepView.VERTICAL) {
-                    parentRect.top += sweepMargins.top + (int) sweep.getPoint();
-                    parentRect.bottom = parentRect.top;
-                    parentRect.left += sweepMargins.left;
-                    parentRect.right += sweepMargins.right;
-                    Gravity.apply(SWEEP_GRAVITY, parentRect.width(), child.getMeasuredHeight(),
-                            parentRect, childRect);
-
-                } else {
-                    parentRect.left += sweepMargins.left + (int) sweep.getPoint();
-                    parentRect.right = parentRect.left;
-                    parentRect.top += sweepMargins.top;
-                    parentRect.bottom += sweepMargins.bottom;
-                    Gravity.apply(SWEEP_GRAVITY, child.getMeasuredWidth(), parentRect.height(),
-                            parentRect, childRect);
-                }
+                layoutSweep((ChartSweepView) child, parentRect, childRect);
+                child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
             }
+        }
+    }
 
-            child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
+    protected void layoutSweep(ChartSweepView sweep) {
+        final Rect parentRect = new Rect(mContent);
+        final Rect childRect = new Rect();
+
+        layoutSweep(sweep, parentRect, childRect);
+        sweep.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
+    }
+
+    protected void layoutSweep(ChartSweepView sweep, Rect parentRect, Rect childRect) {
+        final Rect sweepMargins = sweep.getMargins();
+
+        // sweep is always placed along specific dimension
+        if (sweep.getFollowAxis() == ChartSweepView.VERTICAL) {
+            parentRect.top += sweepMargins.top + (int) sweep.getPoint();
+            parentRect.bottom = parentRect.top;
+            parentRect.left += sweepMargins.left;
+            parentRect.right += sweepMargins.right;
+            Gravity.apply(SWEEP_GRAVITY, parentRect.width(), sweep.getMeasuredHeight(),
+                    parentRect, childRect);
+
+        } else {
+            parentRect.left += sweepMargins.left + (int) sweep.getPoint();
+            parentRect.right = parentRect.left;
+            parentRect.top += sweepMargins.top;
+            parentRect.bottom += sweepMargins.bottom;
+            Gravity.apply(SWEEP_GRAVITY, sweep.getMeasuredWidth(), parentRect.height(),
+                    parentRect, childRect);
         }
     }