--- /dev/null
+<!--
+ 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" />
--- /dev/null
+<!--
+ 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" />
--- /dev/null
+<!--
+ 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>
</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>
<resources>
<color name="disabled_text_color">#66000000</color> <!-- 38% black -->
+
+ <color name="usage_graph_dots">#455A64</color>
</resources>
<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>
<!-- 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>
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 {
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
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) {
} 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;
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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]);
+ }
+ }
+
+}
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>
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>
<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>
<!-- 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>
<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>
<!-- 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>
<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>
<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>
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;
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);