OSDN Git Service

Battery graph in QS Detail
authorJason Monk <jmonk@google.com>
Thu, 11 Feb 2016 16:33:09 +0000 (11:33 -0500)
committerJason Monk <jmonk@google.com>
Tue, 16 Feb 2016 21:25:44 +0000 (16:25 -0500)
Add new usage graph view to SettingsLib that shows usage with
same labels and whatnot.

Use that graph in the battery detail panel to show more stuffs.

Change-Id: I397b1314f65f668df566e93bdbc15420e1b3a280

19 files changed:
packages/SettingsLib/res/layout/usage_bottom_label.xml [new file with mode: 0644]
packages/SettingsLib/res/layout/usage_side_label.xml [new file with mode: 0644]
packages/SettingsLib/res/layout/usage_view.xml [new file with mode: 0644]
packages/SettingsLib/res/values/attrs.xml
packages/SettingsLib/res/values/colors.xml
packages/SettingsLib/res/values/dimens.xml
packages/SettingsLib/res/values/strings.xml
packages/SettingsLib/src/com/android/settingslib/BatteryInfo.java
packages/SettingsLib/src/com/android/settingslib/graph/UsageGraph.java [new file with mode: 0644]
packages/SettingsLib/src/com/android/settingslib/graph/UsageView.java [new file with mode: 0644]
packages/SystemUI/res/layout/battery_detail.xml
packages/SystemUI/res/values-sw600dp-land/config.xml
packages/SystemUI/res/values-sw600dp-land/dimens.xml
packages/SystemUI/res/values-w550dp-land/config.xml
packages/SystemUI/res/values-w550dp-land/dimens.xml
packages/SystemUI/res/values/config.xml
packages/SystemUI/res/values/dimens.xml
packages/SystemUI/res/values/strings.xml
packages/SystemUI/src/com/android/systemui/qs/tiles/BatteryTile.java

diff --git a/packages/SettingsLib/res/layout/usage_bottom_label.xml b/packages/SettingsLib/res/layout/usage_bottom_label.xml
new file mode 100644 (file)
index 0000000..6c16880
--- /dev/null
@@ -0,0 +1,20 @@
+<!--
+    Copyright (C) 2016 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="wrap_content"
+    android:layout_height="wrap_content"
+    android:textAppearance="?android:attr/textAppearanceSmall" />
diff --git a/packages/SettingsLib/res/layout/usage_side_label.xml b/packages/SettingsLib/res/layout/usage_side_label.xml
new file mode 100644 (file)
index 0000000..6c16880
--- /dev/null
@@ -0,0 +1,20 @@
+<!--
+    Copyright (C) 2016 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="wrap_content"
+    android:layout_height="wrap_content"
+    android:textAppearance="?android:attr/textAppearanceSmall" />
diff --git a/packages/SettingsLib/res/layout/usage_view.xml b/packages/SettingsLib/res/layout/usage_view.xml
new file mode 100644 (file)
index 0000000..56716d3
--- /dev/null
@@ -0,0 +1,86 @@
+<!--
+    Copyright (C) 2016 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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingEnd="@dimen/usage_graph_padding_end"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/usage_graph_area_height"
+        android:orientation="horizontal"
+        android:clipChildren="false"
+        android:clipToPadding="false">
+
+        <LinearLayout
+            android:layout_width="@dimen/usage_graph_labels_width"
+            android:layout_height="match_parent"
+            android:paddingStart="@dimen/usage_graph_labels_padding"
+            android:orientation="vertical">
+
+            <include android:id="@+id/label_top"
+                layout="@layout/usage_side_label" />
+
+            <Space
+                android:layout_width="wrap_content"
+                android:layout_height="0dp"
+                android:layout_weight="1" />
+
+            <include android:id="@+id/label_middle"
+                layout="@layout/usage_side_label" />
+
+            <Space
+                android:layout_width="wrap_content"
+                android:layout_height="0dp"
+                android:layout_weight="1" />
+
+            <include android:id="@+id/label_bottom"
+                layout="@layout/usage_side_label" />
+
+        </LinearLayout>
+
+        <com.android.settingslib.graph.UsageGraph
+            android:id="@+id/usage_graph"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:layout_marginTop="@dimen/usage_graph_margin_top_bottom"
+            android:layout_marginBottom="@dimen/usage_graph_margin_top_bottom" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingStart="@dimen/usage_graph_labels_width"
+        android:orientation="horizontal">
+
+        <include android:id="@+id/label_start"
+            layout="@layout/usage_side_label" />
+
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1" />
+
+        <include android:id="@+id/label_end"
+            layout="@layout/usage_side_label" />
+
+    </LinearLayout>
+
+</LinearLayout>
index 3e1fc4a..7aa4220 100644 (file)
     </declare-styleable>
     <attr name="wifi_signal" format="reference" />
 
+    <declare-styleable name="UsageView">
+        <attr name="android:colorAccent" />
+        <attr name="sideLabels" format="reference" />
+        <attr name="bottomLabels" format="reference" />
+        <attr name="textColor" format="color" />
+    </declare-styleable>
+
 </resources>
index c090468..796273d 100644 (file)
@@ -16,4 +16,6 @@
 
 <resources>
     <color name="disabled_text_color">#66000000</color> <!-- 38% black -->
+
+    <color name="usage_graph_dots">#455A64</color>
 </resources>
index 811751c..84d3bff 100644 (file)
 
     <dimen name="wifi_preference_badge_padding">8dip</dimen>
 
+    <!-- Usage graph dimens -->
+    <dimen name="usage_graph_area_height">122dp</dimen>
+    <dimen name="usage_graph_margin_top_bottom">9dp</dimen>
+    <dimen name="usage_graph_padding_end">24dp</dimen>
+    <dimen name="usage_graph_labels_width">72dp</dimen>
+    <dimen name="usage_graph_labels_padding">16dp</dimen>
+
+    <dimen name="usage_graph_divider_size">1dp</dimen>
+
+    <dimen name="usage_graph_line_width">3dp</dimen>
+    <dimen name="usage_graph_line_corner_radius">6dp</dimen>
+
+    <dimen name="usage_graph_dot_size">.75dp</dimen>
+    <dimen name="usage_graph_dot_interval">7dp</dimen>
+
 </resources>
index 6d047d0..20e5ac9 100644 (file)
     <!-- Summary shown for color space correction preference when its value is overridden by another preference [CHAR LIMIT=35] -->
     <string name="daltonizer_type_overridden">Overridden by <xliff:g id="title" example="Simulate color space">%1$s</xliff:g></string>
 
+    <!-- [CHAR_LIMIT=40] Label for estimated remaining duration of battery charging/discharging -->
+    <string name="power_remaining_duration_only">Approx. <xliff:g id="time">%2$s</xliff:g> left</string>
+
     <!-- [CHAR_LIMIT=40] Label for battery level chart when discharging with duration -->
     <string name="power_discharging_duration"><xliff:g id="level">%1$s</xliff:g>
         - approx. <xliff:g id="time">%2$s</xliff:g> left</string>
     <!-- Option in navigation drawer that leads to Settings main screen [CHAR LIMIT=30] -->
     <string name="home">Home</string>
 
+    <string-array name="battery_labels" translatable="false">
+        <item>0%</item>
+        <item>50%</item>
+        <item>100%</item>
+    </string-array>
+
+    <!-- Label for length of time since the battery graph started [CHAR LIMIT=20] -->
+    <string name="charge_length_format"><xliff:g name="time" example="3 hours">%1$s</xliff:g> ago</string>
+
+    <!-- Label for length of time until battery is charged [CHAR LIMIT=20] -->
+    <string name="remaining_length_format"><xliff:g name="time" example="3 hours">%1$s</xliff:g> left</string>
+
 </resources>
index d81bdeb..aeb56a8 100644 (file)
@@ -17,13 +17,17 @@ package com.android.settingslib;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.res.Resources;
 import android.os.AsyncTask;
 import android.os.BatteryManager;
 import android.os.BatteryStats;
+import android.os.BatteryStats.HistoryItem;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.text.format.Formatter;
+import android.util.SparseIntArray;
 import com.android.internal.os.BatteryStatsHelper;
+import com.android.settingslib.graph.UsageView;
 
 public class BatteryInfo {
 
@@ -31,11 +35,127 @@ public class BatteryInfo {
     public int mBatteryLevel;
     public boolean mDischarging = true;
     public long remainingTimeUs = 0;
+    public String batteryPercentString;
+    public String remainingLabel;
+    private BatteryStats mStats;
+    private boolean mCharging;
 
     public interface Callback {
         void onBatteryInfoLoaded(BatteryInfo info);
     }
 
+    public void bindHistory(UsageView view) {
+        long startWalltime = 0;
+        long endDateWalltime = 0;
+        long endWalltime = 0;
+        long historyStart = 0;
+        long historyEnd = 0;
+        byte lastLevel = -1;
+        long curWalltime = startWalltime;
+        long lastWallTime = 0;
+        long lastRealtime = 0;
+        int lastInteresting = 0;
+        int pos = 0;
+        boolean first = true;
+        if (mStats.startIteratingHistoryLocked()) {
+            final HistoryItem rec = new HistoryItem();
+            while (mStats.getNextHistoryLocked(rec)) {
+                pos++;
+                if (first) {
+                    first = false;
+                    historyStart = rec.time;
+                }
+                if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
+                        || rec.cmd == HistoryItem.CMD_RESET) {
+                    // If there is a ridiculously large jump in time, then we won't be
+                    // able to create a good chart with that data, so just ignore the
+                    // times we got before and pretend like our data extends back from
+                    // the time we have now.
+                    // Also, if we are getting a time change and we are less than 5 minutes
+                    // since the start of the history real time, then also use this new
+                    // time to compute the base time, since whatever time we had before is
+                    // pretty much just noise.
+                    if (rec.currentTime > (lastWallTime+(180*24*60*60*1000L))
+                            || rec.time < (historyStart+(5*60*1000L))) {
+                        startWalltime = 0;
+                    }
+                    lastWallTime = rec.currentTime;
+                    lastRealtime = rec.time;
+                    if (startWalltime == 0) {
+                        startWalltime = lastWallTime - (lastRealtime-historyStart);
+                    }
+                }
+                if (rec.isDeltaData()) {
+                    if (rec.batteryLevel != lastLevel || pos == 1) {
+                        lastLevel = rec.batteryLevel;
+                    }
+                    lastInteresting = pos;
+                    historyEnd = rec.time;
+                }
+            }
+        }
+        mStats.finishIteratingHistoryLocked();
+        endDateWalltime = lastWallTime + historyEnd - lastRealtime;
+        endWalltime = endDateWalltime + (remainingTimeUs / 1000);
+
+        int i = 0;
+        final int N = lastInteresting;
+        SparseIntArray points = new SparseIntArray();
+        view.clearPaths();
+        view.configureGraph((int) (endWalltime - startWalltime), 100, remainingTimeUs != 0,
+                mCharging);
+        if (endDateWalltime > startWalltime && mStats.startIteratingHistoryLocked()) {
+            final HistoryItem rec = new HistoryItem();
+            while (mStats.getNextHistoryLocked(rec) && i < N) {
+                if (rec.isDeltaData()) {
+                    curWalltime += rec.time - lastRealtime;
+                    lastRealtime = rec.time;
+                    long x = (curWalltime - startWalltime);
+                    if (x < 0) {
+                        x = 0;
+                    }
+                    points.put((int) x, rec.batteryLevel);
+                } else {
+                    long lastWalltime = curWalltime;
+                    if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
+                            || rec.cmd == HistoryItem.CMD_RESET) {
+                        if (rec.currentTime >= startWalltime) {
+                            curWalltime = rec.currentTime;
+                        } else {
+                            curWalltime = startWalltime + (rec.time - historyStart);
+                        }
+                        lastRealtime = rec.time;
+                    }
+
+                    if (rec.cmd != HistoryItem.CMD_OVERFLOW
+                            && (rec.cmd != HistoryItem.CMD_CURRENT_TIME
+                                    || Math.abs(lastWalltime-curWalltime) > (60*60*1000))) {
+                        if (points.size() > 1) {
+                            view.addPath(points);
+                        }
+                        points.clear();
+                    }
+                }
+                i++;
+            }
+        }
+        if (points.size() > 1) {
+            view.addPath(points);
+        }
+        long timePast = endDateWalltime - startWalltime;
+        final Context context = view.getContext();
+        String timeString = context.getString(R.string.charge_length_format,
+                Formatter.formatShortElapsedTime(context, timePast));
+        String remaining = "";
+        if (remainingTimeUs != 0) {
+            remaining = context.getString(R.string.remaining_length_format,
+                    Formatter.formatShortElapsedTime(context, remainingTimeUs));
+        }
+        view.setBottomLabels(new CharSequence[] { timeString, remaining});
+
+        mStats.finishIteratingHistoryLocked();
+    }
+
     public static void getBatteryInfo(final Context context, final Callback callback) {
         new AsyncTask<Void, Void, BatteryStats>() {
             @Override
@@ -60,23 +180,29 @@ public class BatteryInfo {
     public static BatteryInfo getBatteryInfo(Context context, Intent batteryBroadcast,
             BatteryStats stats, long elapsedRealtimeUs) {
         BatteryInfo info = new BatteryInfo();
+        info.mStats = stats;
         info.mBatteryLevel = Utils.getBatteryLevel(batteryBroadcast);
-        String batteryPercentString = Utils.formatPercentage(info.mBatteryLevel);
-        if (batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) == 0) {
+        info.batteryPercentString = Utils.formatPercentage(info.mBatteryLevel);
+        info.mCharging = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;
+        final Resources resources = context.getResources();
+        if (!info.mCharging) {
             final long drainTime = stats.computeBatteryTimeRemaining(elapsedRealtimeUs);
             if (drainTime > 0) {
                 info.remainingTimeUs = drainTime;
                 String timeString = Formatter.formatShortElapsedTime(context,
                         drainTime / 1000);
-                info.mChargeLabelString = context.getResources().getString(
-                        R.string.power_discharging_duration, batteryPercentString, timeString);
+                info.remainingLabel = resources.getString(R.string.power_remaining_duration_only,
+                        timeString);
+                info.mChargeLabelString = resources.getString(R.string.power_discharging_duration,
+                        info.batteryPercentString, timeString);
             } else {
-                info.mChargeLabelString = batteryPercentString;
+                info.remainingLabel = null;
+                info.mChargeLabelString = info.batteryPercentString;
             }
         } else {
             final long chargeTime = stats.computeChargeTimeRemaining(elapsedRealtimeUs);
             final String statusLabel = Utils.getBatteryStatus(
-                    context.getResources(), batteryBroadcast);
+                    resources, batteryBroadcast);
             final int status = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_STATUS,
                     BatteryManager.BATTERY_STATUS_UNKNOWN);
             if (chargeTime > 0 && status != BatteryManager.BATTERY_STATUS_FULL) {
@@ -95,11 +221,14 @@ public class BatteryInfo {
                 } else {
                     resId = R.string.power_charging_duration;
                 }
-                info.mChargeLabelString = context.getResources().getString(
-                        resId, batteryPercentString, timeString);
+                info.remainingLabel = resources.getString(R.string.power_remaining_duration_only,
+                        timeString);
+                info.mChargeLabelString = resources.getString(
+                        resId, info.batteryPercentString, timeString);
             } else {
-                info.mChargeLabelString = context.getResources().getString(
-                        R.string.power_charging, batteryPercentString, statusLabel);
+                info.remainingLabel = statusLabel;
+                info.mChargeLabelString = resources.getString(
+                        R.string.power_charging, info.batteryPercentString, statusLabel);
             }
         }
         return info;
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/UsageGraph.java b/packages/SettingsLib/src/com/android/settingslib/graph/UsageGraph.java
new file mode 100644 (file)
index 0000000..530ec16
--- /dev/null
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2016 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.settingslib.graph;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.CornerPathEffect;
+import android.graphics.DashPathEffect;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Paint.Cap;
+import android.graphics.Paint.Join;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.Shader.TileMode;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.SparseIntArray;
+import android.util.TypedValue;
+import android.view.View;
+import com.android.settingslib.R;
+
+public class UsageGraph extends View {
+
+    private static final int PATH_DELIM = -1;
+
+    private final Paint mLinePaint;
+    private final Paint mFillPaint;
+    private final Paint mDottedPaint;
+
+    private final Drawable mDivider;
+    private final int mDividerSize;
+
+    private final Path mPath = new Path();
+
+    // Paths in coordinates they are passed in.
+    private final SparseIntArray mPaths = new SparseIntArray();
+    // Paths in local coordinates for drawing.
+    private final SparseIntArray mLocalPaths = new SparseIntArray();
+
+    private int mAccentColor;
+    private boolean mShowProjection;
+    private boolean mProjectUp;
+
+    private float mMaxX = 100;
+    private float mMaxY = 100;
+
+    public UsageGraph(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        final Resources resources = context.getResources();
+
+        mLinePaint = new Paint();
+        mLinePaint.setStyle(Style.STROKE);
+        mLinePaint.setStrokeCap(Cap.ROUND);
+        mLinePaint.setStrokeJoin(Join.ROUND);
+        mLinePaint.setAntiAlias(true);
+        mLinePaint.setPathEffect(new CornerPathEffect(resources.getDimensionPixelSize(
+                R.dimen.usage_graph_line_corner_radius)));
+        mLinePaint.setStrokeWidth(resources.getDimensionPixelSize(R.dimen.usage_graph_line_width));
+
+        mFillPaint = new Paint(mLinePaint);
+        mFillPaint.setStyle(Style.FILL);
+
+        mDottedPaint = new Paint(mLinePaint);
+        mDottedPaint.setStyle(Style.STROKE);
+        float dots = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_size);
+        float interval = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_interval);
+        mDottedPaint.setStrokeWidth(dots * 3);
+        mDottedPaint.setPathEffect(new DashPathEffect(new float[] {dots, interval}, 0));
+        mDottedPaint.setColor(context.getColor(R.color.usage_graph_dots));
+
+        TypedValue v = new TypedValue();
+        context.getTheme().resolveAttribute(com.android.internal.R.attr.listDivider, v, true);
+        mDivider = context.getDrawable(v.resourceId);
+        mDividerSize = resources.getDimensionPixelSize(R.dimen.usage_graph_divider_size);
+    }
+
+    void clearPaths() {
+        mPaths.clear();
+    }
+
+    void setMax(int maxX, int maxY) {
+        mMaxX = maxX;
+        mMaxY = maxY;
+    }
+
+    public void addPath(SparseIntArray points) {
+        for (int i = 0; i < points.size(); i++) {
+            mPaths.put(points.keyAt(i), points.valueAt(i));
+        }
+        mPaths.put(points.keyAt(points.size() - 1) + 1, PATH_DELIM);
+        calculateLocalPaths();
+        postInvalidate();
+    }
+
+    void setAccentColor(int color) {
+        mAccentColor = color;
+        mLinePaint.setColor(mAccentColor);
+        updateGradient();
+        postInvalidate();
+    }
+
+    void setShowProjection(boolean showProjection, boolean projectUp) {
+        mShowProjection = showProjection;
+        mProjectUp = projectUp;
+        postInvalidate();
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        updateGradient();
+        calculateLocalPaths();
+    }
+
+    private void calculateLocalPaths() {
+        if (getWidth() == 0) return;
+        mLocalPaths.clear();
+        int pendingXLoc = 0;
+        int pendingYLoc = PATH_DELIM;
+        for (int i = 0; i < mPaths.size(); i++) {
+            int x = mPaths.keyAt(i);
+            int y = mPaths.valueAt(i);
+            if (y == PATH_DELIM) {
+                if (i == mPaths.size() - 1 && pendingYLoc != PATH_DELIM) {
+                    // Connect to the end of the graph.
+                    mLocalPaths.put(pendingXLoc, pendingYLoc);
+                }
+                // Clear out any pending points.
+                pendingYLoc = PATH_DELIM;
+                mLocalPaths.put(pendingXLoc + 1, PATH_DELIM);
+            } else {
+                final int lx = getX(x);
+                final int ly = getY(y);
+                pendingXLoc = lx;
+                if (mLocalPaths.size() > 0) {
+                    int lastX = mLocalPaths.keyAt(mLocalPaths.size() - 1);
+                    int lastY = mLocalPaths.valueAt(mLocalPaths.size() - 1);
+                    if (lastY != PATH_DELIM && (lastX == lx || lastY == ly)) {
+                        pendingYLoc = ly;
+                        continue;
+                    }
+                }
+                mLocalPaths.put(lx, ly);
+            }
+        }
+    }
+
+    private int getX(float x) {
+        return (int) (x / mMaxX * getWidth());
+    }
+
+    private int getY(float y) {
+        return (int) (getHeight() * (1 - (y / mMaxY)));
+    }
+
+    private void updateGradient() {
+        mFillPaint.setShader(new LinearGradient(0, 0, 0, getHeight(),
+                getColor(mAccentColor, .2f), 0, TileMode.CLAMP));
+    }
+
+    private int getColor(int color, float alphaScale) {
+        return (color & (((int) (0xff * alphaScale) << 24) | 0xffffff));
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        // Draw lines across the top, middle, and bottom.
+        drawDivider(0, canvas);
+        drawDivider((canvas.getHeight() - mDividerSize) / 2, canvas);
+        drawDivider(canvas.getHeight() - mDividerSize, canvas);
+
+        if (mLocalPaths.size() == 0) {
+            return;
+        }
+        if (mShowProjection) {
+            drawProjection(canvas);
+        }
+        drawFilledPath(canvas);
+        drawLinePath(canvas);
+    }
+
+    private void drawProjection(Canvas canvas) {
+        mPath.reset();
+        int x = mLocalPaths.keyAt(mLocalPaths.size() - 2);
+        int y = mLocalPaths.valueAt(mLocalPaths.size() - 2);
+        mPath.moveTo(x, y);
+        mPath.lineTo(canvas.getWidth(), mProjectUp ? 0 : canvas.getHeight());
+        canvas.drawPath(mPath, mDottedPaint);
+    }
+
+    private void drawLinePath(Canvas canvas) {
+        mPath.reset();
+        mPath.moveTo(mLocalPaths.keyAt(0), mLocalPaths.valueAt(0));
+        for (int i = 1; i < mLocalPaths.size(); i++) {
+            int x = mLocalPaths.keyAt(i);
+            int y = mLocalPaths.valueAt(i);
+            if (y == PATH_DELIM) {
+                if (++i < mLocalPaths.size()) {
+                    mPath.moveTo(mLocalPaths.keyAt(i), mLocalPaths.valueAt(i));
+                }
+            } else {
+                mPath.lineTo(x, y);
+            }
+        }
+        canvas.drawPath(mPath, mLinePaint);
+    }
+
+    private void drawFilledPath(Canvas canvas) {
+        mPath.reset();
+        float lastStartX = mLocalPaths.keyAt(0);
+        mPath.moveTo(mLocalPaths.keyAt(0), mLocalPaths.valueAt(0));
+        for (int i = 1; i < mLocalPaths.size(); i++) {
+            int x = mLocalPaths.keyAt(i);
+            int y = mLocalPaths.valueAt(i);
+            if (y == PATH_DELIM) {
+                mPath.lineTo(mLocalPaths.keyAt(i - 1), getHeight());
+                mPath.lineTo(lastStartX, getHeight());
+                mPath.close();
+                if (++i < mLocalPaths.size()) {
+                    lastStartX = mLocalPaths.keyAt(i);
+                    mPath.moveTo(mLocalPaths.keyAt(i), mLocalPaths.valueAt(i));
+                }
+            } else {
+                mPath.lineTo(x, y);
+            }
+        }
+        canvas.drawPath(mPath, mFillPaint);
+    }
+
+    private void drawDivider(int y, Canvas canvas) {
+        mDivider.setBounds(0, y, canvas.getWidth(), y + mDividerSize);
+        mDivider.draw(canvas);
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/UsageView.java b/packages/SettingsLib/src/com/android/settingslib/graph/UsageView.java
new file mode 100644 (file)
index 0000000..978f16a
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2016 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.settingslib.graph;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.SparseIntArray;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+import com.android.settingslib.R;
+
+public class UsageView extends FrameLayout {
+
+    private final UsageGraph mUsageGraph;
+    private final TextView[] mLabels;
+    private final TextView[] mBottomLabels;
+
+    public UsageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        LayoutInflater.from(context).inflate(R.layout.usage_view, this);
+        mUsageGraph = (UsageGraph) findViewById(R.id.usage_graph);
+        mLabels = new TextView[] {
+                (TextView) findViewById(R.id.label_bottom),
+                (TextView) findViewById(R.id.label_middle),
+                (TextView) findViewById(R.id.label_top),
+        };
+        mBottomLabels = new TextView[] {
+                (TextView) findViewById(R.id.label_start),
+                (TextView) findViewById(R.id.label_end),
+        };
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UsageView, 0, 0);
+        if (a.hasValue(R.styleable.UsageView_sideLabels)) {
+            setSideLabels(a.getTextArray(R.styleable.UsageView_sideLabels));
+        }
+        if (a.hasValue(R.styleable.UsageView_bottomLabels)) {
+            setBottomLabels(a.getTextArray(R.styleable.UsageView_bottomLabels));
+        }
+        if (a.hasValue(R.styleable.UsageView_textColor)) {
+            int color = a.getColor(R.styleable.UsageView_textColor, 0);
+            for (TextView v : mLabels) {
+                v.setTextColor(color);
+            }
+            for (TextView v : mBottomLabels) {
+                v.setTextColor(color);
+            }
+        }
+        mUsageGraph.setAccentColor(a.getColor(R.styleable.UsageView_android_colorAccent, 0));
+    }
+
+    public void clearPaths() {
+        mUsageGraph.clearPaths();
+    }
+
+    public void addPath(SparseIntArray points) {
+        mUsageGraph.addPath(points);
+    }
+
+    public void configureGraph(int maxX, int maxY, boolean showProjection, boolean projectUp) {
+        mUsageGraph.setMax(maxX, maxY);
+        mUsageGraph.setShowProjection(showProjection, projectUp);
+    }
+
+    public void setAccentColor(int color) {
+        mUsageGraph.setAccentColor(color);
+    }
+
+    public void setSideLabels(CharSequence[] labels) {
+        if (labels.length != mLabels.length) {
+            throw new IllegalArgumentException("Invalid number of labels");
+        }
+        for (int i = 0; i < mLabels.length; i++) {
+            mLabels[i].setText(labels[i]);
+        }
+    }
+
+    public void setBottomLabels(CharSequence[] labels) {
+        if (labels.length != mBottomLabels.length) {
+            throw new IllegalArgumentException("Invalid number of labels");
+        }
+        for (int i = 0; i < mBottomLabels.length; i++) {
+            mBottomLabels[i].setText(labels[i]);
+        }
+    }
+
+}
index ea4db4b..ded69be 100644 (file)
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:systemui="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:paddingTop="16dp"
-    android:background="?android:attr/selectableItemBackground"
-    android:clickable="true">
-
-    <ImageView
-        android:id="@android:id/icon"
-        android:layout_width="24dp"
-        android:layout_height="24dp"
-        android:scaleType="fitCenter"
-        android:adjustViewBounds="true"
-        android:layout_alignParentTop="true"
-        android:layout_alignParentStart="true"
-        android:layout_marginStart="16dp"
-        android:layout_marginEnd="32dp" />
+    android:orientation="vertical">
 
     <TextView
-        android:id="@android:id/title"
-        android:layout_width="wrap_content"
+        android:id="@+id/charge_and_estimation"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_toStartOf="@android:id/toggle"
-        android:layout_toEndOf="@android:id/icon"
-        android:textAppearance="@style/TextAppearance.QS.DetailItemPrimary"
-        android:text="@string/battery_detail_switch_title" />
+        android:paddingStart="72dp"
+        android:paddingBottom="@dimen/battery_detail_graph_space_top"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:textColor="?android:attr/colorAccent" />
 
-    <TextView
-        android:id="@android:id/summary"
-        android:layout_width="wrap_content"
+    <com.android.settingslib.graph.UsageView
+        android:id="@+id/battery_usage"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_below="@android:id/title"
-        android:layout_toStartOf="@android:id/toggle"
-        android:layout_toEndOf="@android:id/icon"
-        android:textAppearance="@style/TextAppearance.QS.DetailItemSecondary"
-        android:text="@string/battery_detail_switch_summary" />
+        systemui:sideLabels="@array/battery_labels"
+        android:colorAccent="?android:attr/colorAccent"
+        systemui:textColor="#66FFFFFF" />
 
-    <Switch
-        android:id="@android:id/toggle"
-        android:layout_width="wrap_content"
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:background="?android:attr/listDivider"
+        android:layout_marginTop="@dimen/battery_detail_graph_space_bottom"
+        android:layout_marginBottom="8dp" />
+
+    <RelativeLayout
+        android:id="@+id/switch_container"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_alignParentEnd="true"
-        android:layout_alignParentTop="true"
-        android:layout_marginEnd="16dp"
-        android:clickable="false"
-        android:textAppearance="@style/TextAppearance.QS.DetailHeader" />
+        android:paddingTop="16dp"
+        android:paddingBottom="16dp"
+        android:background="?android:attr/selectableItemBackground"
+        android:clickable="true">
+
+        <ImageView
+            android:id="@android:id/icon"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:scaleType="fitCenter"
+            android:adjustViewBounds="true"
+            android:layout_alignParentTop="true"
+            android:layout_alignParentStart="true"
+            android:layout_marginStart="16dp"
+            android:layout_marginEnd="32dp" />
+
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_toStartOf="@android:id/toggle"
+            android:layout_toEndOf="@android:id/icon"
+            android:textAppearance="@style/TextAppearance.QS.DetailItemPrimary"
+            android:text="@string/battery_detail_switch_title" />
+
+        <TextView
+            android:id="@android:id/summary"
+            android:visibility="gone"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@android:id/title"
+            android:layout_toStartOf="@android:id/toggle"
+            android:layout_toEndOf="@android:id/icon"
+            android:textAppearance="@style/TextAppearance.QS.DetailItemSecondary"
+            android:text="@string/battery_detail_switch_summary" />
+
+        <Switch
+            android:id="@android:id/toggle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentEnd="true"
+            android:layout_alignParentTop="true"
+            android:layout_marginEnd="16dp"
+            android:clickable="false"
+            android:textAppearance="@style/TextAppearance.QS.DetailHeader" />
 
-</RelativeLayout>
+    </RelativeLayout>
+</LinearLayout>
index 7e8d802..6594bd2 100644 (file)
@@ -19,5 +19,8 @@
          card. -->
     <integer name="keyguard_max_notification_count">3</integer>
 
+    <!-- Whether QuickSettings is in a phone landscape -->
+    <bool name="quick_settings_wide">false</bool>
+
     <integer name="quick_settings_num_columns">3</integer>
 </resources>
index be5b856..4ed15d5 100644 (file)
@@ -37,4 +37,7 @@
 
     <dimen name="navigation_key_width">162dp</dimen>
     <dimen name="navigation_key_padding">42dp</dimen>
+
+    <dimen name="battery_detail_graph_space_top">27dp</dimen>
+    <dimen name="battery_detail_graph_space_bottom">27dp</dimen>
 </resources>
index 71e54a1..16d5317 100644 (file)
@@ -20,5 +20,9 @@
 <!-- These resources are around just to allow their values to be customized
      for different hardware and product builds. -->
 <resources>
+
+    <!-- Whether QuickSettings is in a phone landscape -->
+    <bool name="quick_settings_wide">true</bool>
+
     <integer name="quick_settings_num_columns">4</integer>
 </resources>
index 4160c83..cd17bed 100644 (file)
@@ -20,4 +20,7 @@
     <dimen name="notification_panel_width">544dp</dimen>
 
     <dimen name="qs_expand_margin">32dp</dimen>
+
+    <dimen name="battery_detail_graph_space_top">9dp</dimen>
+    <dimen name="battery_detail_graph_space_bottom">9dp</dimen>
 </resources>
index 72421a3..4e1680d 100644 (file)
@@ -89,6 +89,9 @@
     <!-- Min alpha % that recent items will fade to while being dismissed -->
     <integer name="config_recent_item_min_alpha">3</integer>
 
+    <!-- Whether QuickSettings is in a phone landscape -->
+    <bool name="quick_settings_wide">false</bool>
+
     <!-- The number of columns in the QuickSettings -->
     <integer name="quick_settings_num_columns">3</integer>
 
index 216d439..11c13e1 100644 (file)
 
     <dimen name="battery_height">14.5dp</dimen>
     <dimen name="battery_width">9.5dp</dimen>
+
+    <dimen name="battery_detail_graph_space_top">27dp</dimen>
+    <dimen name="battery_detail_graph_space_bottom">27dp</dimen>
 </resources>
index 5e8c123..526d842 100644 (file)
     <string name="color_modification_b" translatable="false">B</string>
 
     <!-- Title of the battery settings detail panel [CHAR LIMIT=20] -->
-    <string name="battery_panel_title">Battery (<xliff:g name="pattery_percent" example="52">%1$d</xliff:g>%%)</string>
+    <string name="battery_panel_title">Battery usage</string>
 
     <!-- Summary of battery saver not available [CHAR LIMIT=NONE] -->
     <string name="battery_detail_charging_summary">Battery Saver not available during charging</string>
index 72cdf18..6a9d826 100644 (file)
@@ -19,15 +19,18 @@ import android.content.Context;
 import android.content.Intent;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.RelativeSizeSpan;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Checkable;
 import android.widget.ImageView;
 import android.widget.TextView;
-
 import com.android.internal.logging.MetricsProto.MetricsEvent;
 import com.android.settingslib.BatteryInfo;
+import com.android.settingslib.graph.UsageView;
 import com.android.systemui.BatteryMeterDrawable;
 import com.android.systemui.R;
 import com.android.systemui.qs.QSTile;
@@ -161,29 +164,45 @@ public class BatteryTile extends QSTile<QSTile.State> implements BatteryControll
                 BatteryInfo.getBatteryInfo(mContext, new BatteryInfo.Callback() {
                     @Override
                     public void onBatteryInfoLoaded(BatteryInfo info) {
-                        if (mCurrentView != null && mCharging) {
-                            ((TextView) mCurrentView.findViewById(android.R.id.title)).setText(
-                                    info.mChargeLabelString);
+                        if (mCurrentView != null) {
+                            bindBatteryInfo(info);
                         }
                     }
                 });
-                ((TextView) mCurrentView.findViewById(android.R.id.summary)).setText(
+                ((TextView) mCurrentView.findViewById(android.R.id.title)).setText(
                         R.string.battery_detail_charging_summary);
-                mCurrentView.setClickable(false);
                 mCurrentView.findViewById(android.R.id.icon).setVisibility(View.INVISIBLE);
-                mCurrentView.findViewById(android.R.id.toggle).setVisibility(View.INVISIBLE);
+                mCurrentView.findViewById(android.R.id.toggle).setVisibility(View.GONE);
+                mCurrentView.findViewById(R.id.switch_container).setClickable(false);
             } else {
                 ((TextView) mCurrentView.findViewById(android.R.id.title)).setText(
                         R.string.battery_detail_switch_title);
                 ((TextView) mCurrentView.findViewById(android.R.id.summary)).setText(
                         R.string.battery_detail_switch_summary);
-                mCurrentView.setClickable(true);
                 mCurrentView.findViewById(android.R.id.icon).setVisibility(View.VISIBLE);
                 mCurrentView.findViewById(android.R.id.toggle).setVisibility(View.VISIBLE);
-                mCurrentView.setOnClickListener(this);
+                mCurrentView.findViewById(R.id.switch_container).setClickable(true);
+                mCurrentView.findViewById(R.id.switch_container).setOnClickListener(this);
             }
         }
 
+        private void bindBatteryInfo(BatteryInfo info) {
+            SpannableStringBuilder builder = new SpannableStringBuilder();
+            builder.append(info.batteryPercentString, new RelativeSizeSpan(2),
+                    Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+            if (info.remainingLabel != null) {
+                if (mContext.getResources().getBoolean(R.bool.quick_settings_wide)) {
+                    builder.append(' ');
+                } else {
+                    builder.append('\n');
+                }
+                builder.append(info.remainingLabel);
+            }
+            ((TextView) mCurrentView.findViewById(R.id.charge_and_estimation)).setText(builder);
+
+            info.bindHistory((UsageView) mCurrentView.findViewById(R.id.battery_usage));
+        }
+
         @Override
         public void onClick(View v) {
             mBatteryController.setPowerSaveMode(!mPowerSave);