blob: 3eb12711d1a2cc38764cee5f3eec944c97263f5f [file] [log] [blame]
Jason Monkabe19742015-09-29 09:47:06 -04001/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui;
18
19import android.animation.ArgbEvaluator;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.content.res.Resources;
23import android.content.res.TypedArray;
24import android.database.ContentObserver;
25import android.graphics.*;
26import android.graphics.drawable.Drawable;
27import android.net.Uri;
28import android.os.Bundle;
29import android.os.Handler;
30import android.provider.Settings;
31import com.android.systemui.statusbar.policy.BatteryController;
32
33public class BatteryMeterDrawable extends Drawable implements DemoMode,
34 BatteryController.BatteryStateChangeCallback {
35
36 private static final float ASPECT_RATIO = 9.5f / 14.5f;
37 public static final String TAG = BatteryMeterDrawable.class.getSimpleName();
38 public static final String SHOW_PERCENT_SETTING = "status_bar_show_battery_percent";
39
40 private static final boolean SINGLE_DIGIT_PERCENT = false;
41
42 private static final int FULL = 96;
43
44 private static final float BOLT_LEVEL_THRESHOLD = 0.3f; // opaque bolt below this fraction
45
46 private final int[] mColors;
47
48 private boolean mShowPercent;
49 private float mButtonHeightFraction;
50 private float mSubpixelSmoothingLeft;
51 private float mSubpixelSmoothingRight;
52 private final Paint mFramePaint, mBatteryPaint, mWarningTextPaint, mTextPaint, mBoltPaint;
53 private float mTextHeight, mWarningTextHeight;
54 private int mIconTint = Color.WHITE;
55
56 private int mHeight;
57 private int mWidth;
58 private String mWarningString;
59 private final int mCriticalLevel;
60 private int mChargeColor;
61 private final float[] mBoltPoints;
62 private final Path mBoltPath = new Path();
63
64 private final RectF mFrame = new RectF();
65 private final RectF mButtonFrame = new RectF();
66 private final RectF mBoltFrame = new RectF();
67
68 private final Path mShapePath = new Path();
69 private final Path mClipPath = new Path();
70 private final Path mTextPath = new Path();
71
72 private BatteryController mBatteryController;
73 private boolean mPowerSaveEnabled;
74
75 private int mDarkModeBackgroundColor;
76 private int mDarkModeFillColor;
77
78 private int mLightModeBackgroundColor;
79 private int mLightModeFillColor;
80
81 private final SettingObserver mSettingObserver = new SettingObserver();
82
83 private final Context mContext;
84 private final Handler mHandler;
85
86 private int mLevel = -1;
87 private boolean mPluggedIn;
88 private boolean mListening;
89
90 public BatteryMeterDrawable(Context context, Handler handler, int frameColor) {
91 mContext = context;
92 mHandler = handler;
93 final Resources res = context.getResources();
94 TypedArray levels = res.obtainTypedArray(R.array.batterymeter_color_levels);
95 TypedArray colors = res.obtainTypedArray(R.array.batterymeter_color_values);
96
97 final int N = levels.length();
98 mColors = new int[2*N];
99 for (int i=0; i<N; i++) {
100 mColors[2*i] = levels.getInt(i, 0);
101 mColors[2*i+1] = colors.getColor(i, 0);
102 }
103 levels.recycle();
104 colors.recycle();
105 updateShowPercent();
106 mWarningString = context.getString(R.string.battery_meter_very_low_overlay_symbol);
107 mCriticalLevel = mContext.getResources().getInteger(
108 com.android.internal.R.integer.config_criticalBatteryWarningLevel);
109 mButtonHeightFraction = context.getResources().getFraction(
110 R.fraction.battery_button_height_fraction, 1, 1);
111 mSubpixelSmoothingLeft = context.getResources().getFraction(
112 R.fraction.battery_subpixel_smoothing_left, 1, 1);
113 mSubpixelSmoothingRight = context.getResources().getFraction(
114 R.fraction.battery_subpixel_smoothing_right, 1, 1);
115
116 mFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
117 mFramePaint.setColor(frameColor);
118 mFramePaint.setDither(true);
119 mFramePaint.setStrokeWidth(0);
120 mFramePaint.setStyle(Paint.Style.FILL_AND_STROKE);
121
122 mBatteryPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
123 mBatteryPaint.setDither(true);
124 mBatteryPaint.setStrokeWidth(0);
125 mBatteryPaint.setStyle(Paint.Style.FILL_AND_STROKE);
126
127 mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
128 Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD);
129 mTextPaint.setTypeface(font);
130 mTextPaint.setTextAlign(Paint.Align.CENTER);
131
132 mWarningTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
133 mWarningTextPaint.setColor(mColors[1]);
134 font = Typeface.create("sans-serif", Typeface.BOLD);
135 mWarningTextPaint.setTypeface(font);
136 mWarningTextPaint.setTextAlign(Paint.Align.CENTER);
137
138 mChargeColor = context.getColor(R.color.batterymeter_charge_color);
139
140 mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
141 mBoltPaint.setColor(context.getColor(R.color.batterymeter_bolt_color));
142 mBoltPoints = loadBoltPoints(res);
143
144 mDarkModeBackgroundColor =
145 context.getColor(R.color.dark_mode_icon_color_dual_tone_background);
146 mDarkModeFillColor = context.getColor(R.color.dark_mode_icon_color_dual_tone_fill);
147 mLightModeBackgroundColor =
148 context.getColor(R.color.light_mode_icon_color_dual_tone_background);
149 mLightModeFillColor = context.getColor(R.color.light_mode_icon_color_dual_tone_fill);
150 }
151
152 public void startListening() {
153 mListening = true;
154 mContext.getContentResolver().registerContentObserver(
155 Settings.System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver);
156 if (mDemoMode) return;
157 mBatteryController.addStateChangedCallback(this);
158 }
159
160 public void stopListening() {
161 mListening = false;
162 mContext.getContentResolver().unregisterContentObserver(mSettingObserver);
163 if (mDemoMode) return;
164 mBatteryController.removeStateChangedCallback(this);
165 }
166
167 private void postInvalidate() {
168 mHandler.post(new Runnable() {
169 @Override
170 public void run() {
171 invalidateSelf();
172 }
173 });
174 }
175
176 public void setBatteryController(BatteryController batteryController) {
177 mBatteryController = batteryController;
178 mPowerSaveEnabled = mBatteryController.isPowerSave();
179 }
180
181 @Override
182 public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
183 mLevel = level;
184 mPluggedIn = pluggedIn;
185
186 postInvalidate();
187 }
188
189 @Override
190 public void onPowerSaveChanged() {
191 mPowerSaveEnabled = mBatteryController.isPowerSave();
192 invalidateSelf();
193 }
194
195 private static float[] loadBoltPoints(Resources res) {
196 final int[] pts = res.getIntArray(R.array.batterymeter_bolt_points);
197 int maxX = 0, maxY = 0;
198 for (int i = 0; i < pts.length; i += 2) {
199 maxX = Math.max(maxX, pts[i]);
200 maxY = Math.max(maxY, pts[i + 1]);
201 }
202 final float[] ptsF = new float[pts.length];
203 for (int i = 0; i < pts.length; i += 2) {
204 ptsF[i] = (float)pts[i] / maxX;
205 ptsF[i + 1] = (float)pts[i + 1] / maxY;
206 }
207 return ptsF;
208 }
209
210 @Override
211 public void setBounds(int left, int top, int right, int bottom) {
212 super.setBounds(left, top, right, bottom);
213 mHeight = bottom - top;
214 mWidth = right - left;
215 mWarningTextPaint.setTextSize(mHeight * 0.75f);
216 mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent;
217 }
218
219 private void updateShowPercent() {
220 mShowPercent = 0 != Settings.System.getInt(mContext.getContentResolver(),
221 SHOW_PERCENT_SETTING, 0);
222 }
223
224 private int getColorForLevel(int percent) {
225
226 // If we are in power save mode, always use the normal color.
227 if (mPowerSaveEnabled) {
228 return mColors[mColors.length-1];
229 }
230 int thresh, color = 0;
231 for (int i=0; i<mColors.length; i+=2) {
232 thresh = mColors[i];
233 color = mColors[i+1];
234 if (percent <= thresh) {
235
236 // Respect tinting for "normal" level
237 if (i == mColors.length-2) {
238 return mIconTint;
239 } else {
240 return color;
241 }
242 }
243 }
244 return color;
245 }
246
247 public void setDarkIntensity(float darkIntensity) {
248 int backgroundColor = getBackgroundColor(darkIntensity);
249 int fillColor = getFillColor(darkIntensity);
250 mIconTint = fillColor;
251 mFramePaint.setColor(backgroundColor);
252 mBoltPaint.setColor(fillColor);
253 mChargeColor = fillColor;
254 invalidateSelf();
255 }
256
257 private int getBackgroundColor(float darkIntensity) {
258 return getColorForDarkIntensity(
259 darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor);
260 }
261
262 private int getFillColor(float darkIntensity) {
263 return getColorForDarkIntensity(
264 darkIntensity, mLightModeFillColor, mDarkModeFillColor);
265 }
266
267 private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
268 return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
269 }
270
271 @Override
272 public void draw(Canvas c) {
273 final int level = mLevel;
274
275 if (level == -1) return;
276
277 float drawFrac = (float) level / 100f;
278 final int height = mHeight;
279 final int width = (int) (ASPECT_RATIO * mHeight);
280 int px = (mWidth - width) / 2;
281
282 final int buttonHeight = (int) (height * mButtonHeightFraction);
283
284 mFrame.set(0, 0, width, height);
285 mFrame.offset(px, 0);
286
287 // button-frame: area above the battery body
288 mButtonFrame.set(
289 mFrame.left + Math.round(width * 0.25f),
290 mFrame.top,
291 mFrame.right - Math.round(width * 0.25f),
292 mFrame.top + buttonHeight);
293
294 mButtonFrame.top += mSubpixelSmoothingLeft;
295 mButtonFrame.left += mSubpixelSmoothingLeft;
296 mButtonFrame.right -= mSubpixelSmoothingRight;
297
298 // frame: battery body area
299 mFrame.top += buttonHeight;
300 mFrame.left += mSubpixelSmoothingLeft;
301 mFrame.top += mSubpixelSmoothingLeft;
302 mFrame.right -= mSubpixelSmoothingRight;
303 mFrame.bottom -= mSubpixelSmoothingRight;
304
305 // set the battery charging color
306 mBatteryPaint.setColor(mPluggedIn ? mChargeColor : getColorForLevel(level));
307
308 if (level >= FULL) {
309 drawFrac = 1f;
310 } else if (level <= mCriticalLevel) {
311 drawFrac = 0f;
312 }
313
314 final float levelTop = drawFrac == 1f ? mButtonFrame.top
315 : (mFrame.top + (mFrame.height() * (1f - drawFrac)));
316
317 // define the battery shape
318 mShapePath.reset();
319 mShapePath.moveTo(mButtonFrame.left, mButtonFrame.top);
320 mShapePath.lineTo(mButtonFrame.right, mButtonFrame.top);
321 mShapePath.lineTo(mButtonFrame.right, mFrame.top);
322 mShapePath.lineTo(mFrame.right, mFrame.top);
323 mShapePath.lineTo(mFrame.right, mFrame.bottom);
324 mShapePath.lineTo(mFrame.left, mFrame.bottom);
325 mShapePath.lineTo(mFrame.left, mFrame.top);
326 mShapePath.lineTo(mButtonFrame.left, mFrame.top);
327 mShapePath.lineTo(mButtonFrame.left, mButtonFrame.top);
328
329 if (mPluggedIn) {
330 // define the bolt shape
331 final float bl = mFrame.left + mFrame.width() / 4.5f;
332 final float bt = mFrame.top + mFrame.height() / 6f;
333 final float br = mFrame.right - mFrame.width() / 7f;
334 final float bb = mFrame.bottom - mFrame.height() / 10f;
335 if (mBoltFrame.left != bl || mBoltFrame.top != bt
336 || mBoltFrame.right != br || mBoltFrame.bottom != bb) {
337 mBoltFrame.set(bl, bt, br, bb);
338 mBoltPath.reset();
339 mBoltPath.moveTo(
340 mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(),
341 mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height());
342 for (int i = 2; i < mBoltPoints.length; i += 2) {
343 mBoltPath.lineTo(
344 mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(),
345 mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height());
346 }
347 mBoltPath.lineTo(
348 mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(),
349 mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height());
350 }
351
352 float boltPct = (mBoltFrame.bottom - levelTop) / (mBoltFrame.bottom - mBoltFrame.top);
353 boltPct = Math.min(Math.max(boltPct, 0), 1);
354 if (boltPct <= BOLT_LEVEL_THRESHOLD) {
355 // draw the bolt if opaque
356 c.drawPath(mBoltPath, mBoltPaint);
357 } else {
358 // otherwise cut the bolt out of the overall shape
359 mShapePath.op(mBoltPath, Path.Op.DIFFERENCE);
360 }
361 }
362
363 // compute percentage text
364 boolean pctOpaque = false;
365 float pctX = 0, pctY = 0;
366 String pctText = null;
367 if (!mPluggedIn && level > mCriticalLevel && mShowPercent) {
368 mTextPaint.setColor(getColorForLevel(level));
369 mTextPaint.setTextSize(height *
370 (SINGLE_DIGIT_PERCENT ? 0.75f
371 : (mLevel == 100 ? 0.38f : 0.5f)));
372 mTextHeight = -mTextPaint.getFontMetrics().ascent;
373 pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level);
374 pctX = mWidth * 0.5f;
375 pctY = (mHeight + mTextHeight) * 0.47f;
376 pctOpaque = levelTop > pctY;
377 if (!pctOpaque) {
378 mTextPath.reset();
379 mTextPaint.getTextPath(pctText, 0, pctText.length(), pctX, pctY, mTextPath);
380 // cut the percentage text out of the overall shape
381 mShapePath.op(mTextPath, Path.Op.DIFFERENCE);
382 }
383 }
384
385 // draw the battery shape background
386 c.drawPath(mShapePath, mFramePaint);
387
388 // draw the battery shape, clipped to charging level
389 mFrame.top = levelTop;
390 mClipPath.reset();
391 mClipPath.addRect(mFrame, Path.Direction.CCW);
392 mShapePath.op(mClipPath, Path.Op.INTERSECT);
393 c.drawPath(mShapePath, mBatteryPaint);
394
395 if (!mPluggedIn) {
396 if (level <= mCriticalLevel) {
397 // draw the warning text
398 final float x = mWidth * 0.5f;
399 final float y = (mHeight + mWarningTextHeight) * 0.48f;
400 c.drawText(mWarningString, x, y, mWarningTextPaint);
401 } else if (pctOpaque) {
402 // draw the percentage text
403 c.drawText(pctText, pctX, pctY, mTextPaint);
404 }
405 }
406 }
407
408 // Some stuff required by Drawable.
409 @Override
410 public void setAlpha(int alpha) {
411 }
412
413 @Override
414 public void setColorFilter(@Nullable ColorFilter colorFilter) {
415 }
416
417 @Override
418 public int getOpacity() {
419 return 0;
420 }
421
422 private boolean mDemoMode;
423
424 @Override
425 public void dispatchDemoCommand(String command, Bundle args) {
426 if (!mDemoMode && command.equals(COMMAND_ENTER)) {
427 mBatteryController.removeStateChangedCallback(this);
428 mDemoMode = true;
429 if (mListening) {
430 mBatteryController.removeStateChangedCallback(this);
431 }
432 } else if (mDemoMode && command.equals(COMMAND_EXIT)) {
433 mDemoMode = false;
434 postInvalidate();
435 if (mListening) {
436 mBatteryController.addStateChangedCallback(this);
437 }
438 } else if (mDemoMode && command.equals(COMMAND_BATTERY)) {
439 String level = args.getString("level");
440 String plugged = args.getString("plugged");
441 if (level != null) {
442 mLevel = Math.min(Math.max(Integer.parseInt(level), 0), 100);
443 }
444 if (plugged != null) {
445 mPluggedIn = Boolean.parseBoolean(plugged);
446 }
447 postInvalidate();
448 }
449 }
450
451 private final class SettingObserver extends ContentObserver {
452 public SettingObserver() {
453 super(new Handler());
454 }
455
456 @Override
457 public void onChange(boolean selfChange, Uri uri) {
458 super.onChange(selfChange, uri);
459 updateShowPercent();
460 postInvalidate();
461 }
462 }
463
464}