Data usage app icons and details, chart labels.

Add app icons into both summary list and details pane. Also show list
of all applications merged under a UID. Draw dates on chart axis, and
avoid flashing policy sweeps when switching networks in detail mode.

Bug: 5087283, 5038812
Change-Id: I1dcd03ca85b517f8726452af8a46b4be9b3d20f1
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9b2391a..a99eb66 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1185,7 +1185,8 @@
         </activity>
 
         <activity android:name="Settings$DataUsageSummaryActivity"
-                android:label="@string/data_usage_summary_title">
+                android:label="@string/data_usage_summary_title"
+                android:uiOptions="none">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.DEFAULT" />
diff --git a/res/layout/data_usage_app_title.xml b/res/layout/data_usage_app_title.xml
new file mode 100644
index 0000000..baaa685
--- /dev/null
+++ b/res/layout/data_usage_app_title.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:singleLine="true"
+    android:ellipsize="marquee"
+    android:textAppearance="?android:attr/textAppearanceMedium" />
diff --git a/res/layout/data_usage_chart.xml b/res/layout/data_usage_chart.xml
index 3ae8a03..c1f6c27 100644
--- a/res/layout/data_usage_chart.xml
+++ b/res/layout/data_usage_chart.xml
@@ -30,7 +30,7 @@
         settings:primaryDrawable="@drawable/data_grid_primary"
         settings:secondaryDrawable="@drawable/data_grid_secondary"
         settings:borderDrawable="@drawable/data_grid_border"
-        settings:labelColor="#24aae1" />
+        settings:labelColor="#667bb5" />
 
     <com.android.settings.widget.ChartNetworkSeriesView
         android:id="@+id/series"
diff --git a/res/layout/data_usage_detail.xml b/res/layout/data_usage_detail.xml
index 2c8e490..8d0c0cc 100644
--- a/res/layout/data_usage_detail.xml
+++ b/res/layout/data_usage_detail.xml
@@ -20,17 +20,20 @@
     android:layout_height="match_parent"
     android:orientation="vertical">
 
-    <TextView
-        android:id="@+id/app_title"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginLeft="16dip" />
+    <ImageView
+        android:id="@+id/app_icon"
+        android:layout_width="48dip"
+        android:layout_height="48dip"
+        android:layout_marginLeft="16dip"
+        android:scaleType="centerInside" />
 
-    <TextView
-        android:id="@+id/app_subtitle"
+    <LinearLayout
+        android:id="@+id/app_titles"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginLeft="16dip" />
+        android:layout_height="match_parent"
+        android:layout_marginLeft="16dip"
+        android:layout_marginTop="8dip"
+        android:orientation="vertical" />
 
     <Button
         android:id="@+id/app_settings"
diff --git a/res/layout/data_usage_header.xml b/res/layout/data_usage_header.xml
index 9602898..528fc34 100644
--- a/res/layout/data_usage_header.xml
+++ b/res/layout/data_usage_header.xml
@@ -43,6 +43,8 @@
         android:paddingRight="16dip"
         android:paddingTop="8dip"
         android:paddingBottom="8dip"
+        android:singleLine="true"
+        android:ellipsize="marquee"
         android:textAppearance="?android:attr/textAppearanceSmall" />
 
     <TextView
diff --git a/res/layout/data_usage_item.xml b/res/layout/data_usage_item.xml
index 36f8b4d..b41d486 100644
--- a/res/layout/data_usage_item.xml
+++ b/res/layout/data_usage_item.xml
@@ -21,7 +21,15 @@
     android:paddingRight="16dip"
     android:paddingTop="8dip"
     android:paddingBottom="8dip"
-    android:columnCount="2">
+    android:columnCount="3">
+
+    <ImageView
+        android:id="@android:id/icon"
+        android:layout_width="48dip"
+        android:layout_height="48dip"
+        android:layout_rowSpan="2"
+        android:layout_marginRight="8dip"
+        android:scaleType="centerInside" />
 
     <TextView
         android:id="@android:id/title"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7315863..3684dcd 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -3465,7 +3465,7 @@
     <!-- Combination of total network bytes sent and received by an application. [CHAR LIMIT=NONE] -->
     <string name="data_usage_received_sent"><xliff:g id="received" example="128KB">%1$s</xliff:g> received, <xliff:g id="sent" example="1.3GB">%2$s</xliff:g> sent</string>
     <!-- Label displaying total network data transferred during a specific time period. [CHAR LIMIT=64] -->
-    <string name="data_usage_total_during_range">Data usage: <xliff:g id="total" example="128KB">%1$s</xliff:g> between <xliff:g id="range" example="Jul 1 - Jul 31">%2$s</xliff:g></string>
+    <string name="data_usage_total_during_range">Data usage: <xliff:g id="total" example="128KB">%1$s</xliff:g> on <xliff:g id="range" example="Jul 1 - Jul 31">%2$s</xliff:g></string>
 
     <!-- Button at the bottom of the CryptKeeper screen to make an emergency call. -->
     <string name="cryptkeeper_emergency_call">Emergency call</string>
diff --git a/src/com/android/settings/DataUsageSummary.java b/src/com/android/settings/DataUsageSummary.java
index 45793b1..bccc5a5 100644
--- a/src/com/android/settings/DataUsageSummary.java
+++ b/src/com/android/settings/DataUsageSummary.java
@@ -37,6 +37,9 @@
 import static android.net.NetworkTemplate.buildTemplateMobile4g;
 import static android.net.NetworkTemplate.buildTemplateMobileAll;
 import static android.net.NetworkTemplate.buildTemplateWifi;
+import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH;
+import static android.text.format.DateUtils.FORMAT_SHOW_DATE;
+import static android.text.format.Time.TIMEZONE_UTC;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
 import android.animation.LayoutTransition;
@@ -57,6 +60,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
 import android.net.ConnectivityManager;
 import android.net.INetworkPolicyManager;
 import android.net.INetworkStatsService;
@@ -78,7 +82,6 @@
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.text.format.Formatter;
-import android.text.format.Time;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -97,6 +100,7 @@
 import android.widget.CheckBox;
 import android.widget.CompoundButton;
 import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.ListView;
 import android.widget.NumberPicker;
@@ -190,8 +194,8 @@
     private TextView mEmpty;
 
     private View mAppDetail;
-    private TextView mAppTitle;
-    private TextView mAppSubtitle;
+    private ImageView mAppIcon;
+    private ViewGroup mAppTitles;
     private Button mAppSettings;
 
     private LinearLayout mAppSwitches;
@@ -295,8 +299,8 @@
         {
             // bind app detail controls
             mAppDetail = mHeader.findViewById(R.id.app_detail);
-            mAppTitle = (TextView) mAppDetail.findViewById(R.id.app_title);
-            mAppSubtitle = (TextView) mAppDetail.findViewById(R.id.app_subtitle);
+            mAppIcon = (ImageView) mAppDetail.findViewById(R.id.app_icon);
+            mAppTitles = (ViewGroup) mAppDetail.findViewById(R.id.app_titles);
             mAppSwitches = (LinearLayout) mAppDetail.findViewById(R.id.app_switches);
 
             mAppSettings = (Button) mAppDetail.findViewById(R.id.app_settings);
@@ -643,6 +647,10 @@
      * depending on {@link #isAppDetailMode()}.
      */
     private void updateAppDetail() {
+        final Context context = getActivity();
+        final PackageManager pm = context.getPackageManager();
+        final LayoutInflater inflater = getActivity().getLayoutInflater();
+
         if (isAppDetailMode()) {
             mAppDetail.setVisibility(View.VISIBLE);
             mCycleAdapter.setChangeVisible(false);
@@ -658,8 +666,18 @@
         // remove warning/limit sweeps while in detail mode
         mChart.bindNetworkPolicy(null);
 
-        final PackageManager pm = getActivity().getPackageManager();
-        mAppTitle.setText(pm.getNameForUid(mUid));
+        // show icon and all labels appearing under this app
+        final UidDetail detail = resolveDetailForUid(context, mUid);
+        mAppIcon.setImageDrawable(detail.icon);
+
+        mAppTitles.removeAllViews();
+        if (detail.detailLabels != null) {
+            for (CharSequence label : detail.detailLabels) {
+                mAppTitles.addView(inflateAppTitle(inflater, mAppTitles, label));
+            }
+        } else {
+            mAppTitles.addView(inflateAppTitle(inflater, mAppTitles, detail.label));
+        }
 
         // enable settings button when package provides it
         // TODO: target torwards entire UID instead of just first package
@@ -693,7 +711,6 @@
 
         updateDetailData();
 
-        final Context context = getActivity();
         if (NetworkPolicyManager.isUidValidForPolicy(context, mUid) && !getRestrictBackground()
                 && isBandwidthControlEnabled()) {
             mAppRestrictView.setVisibility(View.VISIBLE);
@@ -814,7 +831,9 @@
         if (isNetworkPolicyModifiable()) {
             mDisableAtLimitView.setVisibility(View.VISIBLE);
             mDisableAtLimit.setChecked(policy != null && policy.limitBytes != LIMIT_DISABLED);
-            mChart.bindNetworkPolicy(policy);
+            if (!isAppDetailMode()) {
+                mChart.bindNetworkPolicy(policy);
+            }
 
         } else {
             // controls are disabled; don't bind warning/limit sweeps
@@ -998,11 +1017,6 @@
         if (isAppDetailMode()) {
             if (mDetailHistory != null) {
                 entry = mDetailHistory.getValues(start, end, now, null);
-
-                mAppSubtitle.setText(
-                        getString(R.string.data_usage_received_sent,
-                                Formatter.formatFileSize(context, entry.rxBytes),
-                                Formatter.formatFileSize(context, entry.txBytes)));
             } else {
                 entry = null;
             }
@@ -1015,12 +1029,11 @@
             // kick off loader for detailed stats
             getLoaderManager().restartLoader(LOADER_SUMMARY,
                     SummaryForAllUidLoader.buildArgs(mTemplate, start, end), mSummaryForAllUid);
-
         }
 
         final long totalBytes = entry != null ? entry.rxBytes + entry.txBytes : 0;
         final String totalPhrase = Formatter.formatFileSize(context, totalBytes);
-        final String rangePhrase = formatDateRange(context, start, end, null);
+        final String rangePhrase = formatDateRange(context, start, end, false);
 
         mUsageSummary.setText(
                 getString(R.string.data_usage_total_during_range, totalPhrase, rangePhrase));
@@ -1104,7 +1117,7 @@
         }
 
         public CycleItem(Context context, long start, long end) {
-            this.label = formatDateRange(context, start, end, Time.TIMEZONE_UTC);
+            this.label = formatDateRange(context, start, end, true);
             this.start = start;
             this.end = end;
         }
@@ -1119,14 +1132,11 @@
     private static final java.util.Formatter sFormatter = new java.util.Formatter(
             sBuilder, Locale.getDefault());
 
-    private static String formatDateRange(Context context, long start, long end, String timezone) {
-        synchronized (sBuilder) {
-            int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH;
-            if (Time.getJulianDay(start, 0) == Time.getJulianDay(end, 0)) {
-                // when times are on same day, include time detail
-                flags |= DateUtils.FORMAT_SHOW_TIME;
-            }
+    public static String formatDateRange(Context context, long start, long end, boolean utcTime) {
+        final int flags = FORMAT_SHOW_DATE | FORMAT_ABBREV_MONTH;
+        final String timezone = utcTime ? TIMEZONE_UTC : null;
 
+        synchronized (sBuilder) {
             sBuilder.setLength(0);
             return DateUtils
                     .formatDateRange(context, sFormatter, start, end, flags, timezone).toString();
@@ -1250,13 +1260,17 @@
 
             final Context context = parent.getContext();
 
+            final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
             final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
             final ProgressBar progress = (ProgressBar) convertView.findViewById(
                     android.R.id.progress);
 
             final AppUsageItem item = mItems.get(position);
-            title.setText(resolveLabelForUid(context, item.uid));
+            final UidDetail detail = resolveDetailForUid(context, item.uid);
+
+            icon.setImageDrawable(detail.icon);
+            title.setText(detail.label);
             summary.setText(Formatter.formatFileSize(context, item.total));
 
             final int percentTotal = mLargest != 0 ? (int) (item.total * 100 / mLargest) : 0;
@@ -1536,48 +1550,66 @@
         }
     }
 
+    public static class UidDetail {
+        public CharSequence label;
+        public CharSequence[] detailLabels;
+        public Drawable icon;
+    }
+
     /**
      * Resolve best descriptive label for the given UID.
      */
-    public static CharSequence resolveLabelForUid(Context context, int uid) {
+    public static UidDetail resolveDetailForUid(Context context, int uid) {
         final Resources res = context.getResources();
         final PackageManager pm = context.getPackageManager();
 
+        final UidDetail detail = new UidDetail();
+        detail.label = pm.getNameForUid(uid);
+        detail.icon = pm.getDefaultActivityIcon();
+
         // handle special case labels
         switch (uid) {
             case android.os.Process.SYSTEM_UID:
-                return res.getText(R.string.process_kernel_label);
+                detail.label = res.getString(R.string.process_kernel_label);
+                detail.icon = pm.getDefaultActivityIcon();
+                return detail;
             case TrafficStats.UID_REMOVED:
-                return res.getText(R.string.data_usage_uninstalled_apps);
+                detail.label = res.getString(R.string.data_usage_uninstalled_apps);
+                detail.icon = pm.getDefaultActivityIcon();
+                return detail;
         }
 
         // otherwise fall back to using packagemanager labels
         final String[] packageNames = pm.getPackagesForUid(uid);
         final int length = packageNames != null ? packageNames.length : 0;
 
-        CharSequence label = pm.getNameForUid(uid);
         try {
             if (length == 1) {
                 final ApplicationInfo info = pm.getApplicationInfo(packageNames[0], 0);
-                label = info.loadLabel(pm);
+                detail.label = info.loadLabel(pm).toString();
+                detail.icon = info.loadIcon(pm);
             } else if (length > 1) {
-                for (String packageName : packageNames) {
-                    final PackageInfo info = pm.getPackageInfo(packageName, 0);
-                    if (info.sharedUserLabel != 0) {
-                        label = pm.getText(packageName, info.sharedUserLabel, info.applicationInfo);
-                        if (!TextUtils.isEmpty(label)) {
-                            break;
-                        }
+                detail.detailLabels = new CharSequence[length];
+                for (int i = 0; i < length; i++) {
+                    final String packageName = packageNames[i];
+                    final PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
+                    final ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0);
+
+                    detail.detailLabels[i] = appInfo.loadLabel(pm).toString();
+                    if (packageInfo.sharedUserLabel != 0) {
+                        detail.label = pm.getText(packageName, packageInfo.sharedUserLabel,
+                                packageInfo.applicationInfo).toString();
+                        detail.icon = appInfo.loadIcon(pm);
                     }
                 }
             }
         } catch (NameNotFoundException e) {
         }
 
-        if (TextUtils.isEmpty(label)) {
-            label = Integer.toString(uid);
+        if (TextUtils.isEmpty(detail.label)) {
+            detail.label = Integer.toString(uid);
         }
-        return label;
+        return detail;
     }
 
     /**
@@ -1652,6 +1684,14 @@
         return view;
     }
 
+    private static View inflateAppTitle(
+            LayoutInflater inflater, ViewGroup root, CharSequence label) {
+        final TextView view = (TextView) inflater.inflate(
+                R.layout.data_usage_app_title, root, false);
+        view.setText(label);
+        return view;
+    }
+
     /**
      * Set {@link android.R.id#title} for a preference view inflated with
      * {@link #inflatePreference(LayoutInflater, ViewGroup, View)}.
diff --git a/src/com/android/settings/widget/ChartGridView.java b/src/com/android/settings/widget/ChartGridView.java
index 7a83fbf..c2702e4 100644
--- a/src/com/android/settings/widget/ChartGridView.java
+++ b/src/com/android/settings/widget/ChartGridView.java
@@ -17,12 +17,20 @@
 package com.android.settings.widget;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
 import android.graphics.drawable.Drawable;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
 import android.util.AttributeSet;
+import android.util.TypedValue;
 import android.view.View;
 
+import com.android.settings.DataUsageSummary;
 import com.android.settings.R;
 import com.google.common.base.Preconditions;
 
@@ -32,14 +40,16 @@
  */
 public class ChartGridView extends View {
 
-    // TODO: eventually teach about drawing chart labels
-
     private ChartAxis mHoriz;
     private ChartAxis mVert;
 
     private Drawable mPrimary;
     private Drawable mSecondary;
     private Drawable mBorder;
+    private int mLabelColor;
+
+    private Layout mLayoutStart;
+    private Layout mLayoutEnd;
 
     public ChartGridView(Context context) {
         this(context, null, 0);
@@ -60,7 +70,7 @@
         mPrimary = a.getDrawable(R.styleable.ChartGridView_primaryDrawable);
         mSecondary = a.getDrawable(R.styleable.ChartGridView_secondaryDrawable);
         mBorder = a.getDrawable(R.styleable.ChartGridView_borderDrawable);
-        // TODO: eventually read labelColor
+        mLabelColor = a.getColor(R.styleable.ChartGridView_labelColor, Color.RED);
 
         a.recycle();
     }
@@ -70,6 +80,13 @@
         mVert = Preconditions.checkNotNull(vert, "missing vert");
     }
 
+    void setBounds(long start, long end) {
+        final Context context = getContext();
+        mLayoutStart = makeLayout(DataUsageSummary.formatDateRange(context, start, start, true));
+        mLayoutEnd = makeLayout(DataUsageSummary.formatDateRange(context, end, end, true));
+        invalidate();
+    }
+
     @Override
     protected void onDraw(Canvas canvas) {
         final int width = getWidth();
@@ -98,5 +115,38 @@
 
         mBorder.setBounds(0, 0, width, height);
         mBorder.draw(canvas);
+
+        final int padding = mLayoutStart.getHeight() / 8;
+
+        final Layout start = mLayoutStart;
+        if (start != null) {
+            canvas.save();
+            canvas.translate(0, height + padding);
+            start.draw(canvas);
+            canvas.restore();
+        }
+
+        final Layout end = mLayoutEnd;
+        if (end != null) {
+            canvas.save();
+            canvas.translate(width - end.getWidth(), height + padding);
+            end.draw(canvas);
+            canvas.restore();
+        }
     }
+
+    private Layout makeLayout(CharSequence text) {
+        final Resources res = getResources();
+        final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+        paint.density = res.getDisplayMetrics().density;
+        paint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
+        paint.setColor(mLabelColor);
+        paint.setTextSize(
+                TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10, res.getDisplayMetrics()));
+
+        return new StaticLayout(text, paint,
+                (int) Math.ceil(Layout.getDesiredWidth(text, paint)),
+                Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
+    }
+
 }
diff --git a/src/com/android/settings/widget/ChartSweepView.java b/src/com/android/settings/widget/ChartSweepView.java
index b5e044f..d5e8de8 100644
--- a/src/com/android/settings/widget/ChartSweepView.java
+++ b/src/com/android/settings/widget/ChartSweepView.java
@@ -29,7 +29,6 @@
 import android.text.SpannableStringBuilder;
 import android.text.TextPaint;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.util.MathUtils;
 import android.view.MotionEvent;
 import android.view.View;
diff --git a/src/com/android/settings/widget/DataUsageChartView.java b/src/com/android/settings/widget/DataUsageChartView.java
index b2ad844..a1c92e1 100644
--- a/src/com/android/settings/widget/DataUsageChartView.java
+++ b/src/com/android/settings/widget/DataUsageChartView.java
@@ -343,6 +343,7 @@
      */
     public void setVisibleRange(long visibleStart, long visibleEnd) {
         mHoriz.setBounds(visibleStart, visibleEnd);
+        mGrid.setBounds(visibleStart, visibleEnd);
 
         final long validStart = Math.max(visibleStart, getStatsStart());
         final long validEnd = Math.min(visibleEnd, getStatsEnd());