• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.settingslib.graph;
16 
17 import static com.android.settingslib.flags.Flags.newStatusBarIcons;
18 
19 import android.animation.ArgbEvaluator;
20 import android.annotation.IntRange;
21 import android.content.Context;
22 import android.content.res.ColorStateList;
23 import android.graphics.Canvas;
24 import android.graphics.ColorFilter;
25 import android.graphics.Matrix;
26 import android.graphics.Paint;
27 import android.graphics.Path;
28 import android.graphics.Path.Direction;
29 import android.graphics.Path.FillType;
30 import android.graphics.PorterDuff;
31 import android.graphics.PorterDuffXfermode;
32 import android.graphics.Rect;
33 import android.graphics.drawable.DrawableWrapper;
34 import android.os.Handler;
35 import android.telephony.CellSignalStrength;
36 import android.util.LayoutDirection;
37 import android.util.PathParser;
38 
39 import androidx.annotation.NonNull;
40 import androidx.annotation.Nullable;
41 
42 import com.android.settingslib.R;
43 import com.android.settingslib.Utils;
44 
45 import java.util.Objects;
46 
47 /**
48  * Drawable displaying a mobile cell signal indicator.
49  */
50 public class SignalDrawable extends DrawableWrapper {
51 
52     private static final String TAG = "SignalDrawable";
53 
54     private static final int NUM_DOTS = 3;
55 
56     private static final float VIEWPORT = 24f;
57     private static final float PAD = 2f / VIEWPORT;
58 
59     private static final float DOT_SIZE = 3f / VIEWPORT;
60     private static final float DOT_PADDING = 1.5f / VIEWPORT;
61 
62     // All of these are masks to push all of the drawable state into one int for easy callbacks
63     // and flow through sysui.
64     private static final int LEVEL_MASK = 0xff;
65     private static final int NUM_LEVEL_SHIFT = 8;
66     private static final int NUM_LEVEL_MASK = 0xff << NUM_LEVEL_SHIFT;
67     private static final int STATE_SHIFT = 16;
68     private static final int STATE_MASK = 0xff << STATE_SHIFT;
69     private static final int STATE_CUT = 2;
70     private static final int STATE_CARRIER_CHANGE = 3;
71 
72     private static final long DOT_DELAY = 1000;
73 
74     // Check the config for which icon we want to use
75     private static final int ICON_RES = SignalDrawable.getIconRes();
76 
77     private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
78     private final Paint mTransparentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
79     private final int mDarkModeFillColor;
80     private final int mLightModeFillColor;
81     private final Path mCutoutPath = new Path();
82     private final Path mForegroundPath = new Path();
83     private final Path mAttributionPath = new Path();
84     private final Matrix mAttributionScaleMatrix = new Matrix();
85     private final Path mScaledAttributionPath = new Path();
86     private final Handler mHandler;
87     private final float mCutoutWidthFraction;
88     private final float mCutoutHeightFraction;
89     private float mDarkIntensity = -1;
90     private final int mIntrinsicSize;
91     private boolean mAnimating;
92     private int mCurrentDot;
93 
SignalDrawable(Context context)94     public SignalDrawable(Context context) {
95         this(context, new Handler());
96     }
97 
SignalDrawable(@onNull Context context, @NonNull Handler handler)98     public SignalDrawable(@NonNull Context context, @NonNull Handler handler) {
99         super(context.getDrawable(ICON_RES));
100         final String attributionPathString = context.getString(
101                 com.android.internal.R.string.config_signalAttributionPath);
102         mAttributionPath.set(PathParser.createPathFromPathData(attributionPathString));
103         updateScaledAttributionPath();
104         mCutoutWidthFraction = context.getResources().getFloat(
105                 com.android.internal.R.dimen.config_signalCutoutWidthFraction);
106         mCutoutHeightFraction = context.getResources().getFloat(
107                 com.android.internal.R.dimen.config_signalCutoutHeightFraction);
108         mDarkModeFillColor = Utils.getColorStateListDefaultColor(context,
109                 R.color.dark_mode_icon_color_single_tone);
110         mLightModeFillColor = Utils.getColorStateListDefaultColor(context,
111                 R.color.light_mode_icon_color_single_tone);
112         mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
113         mTransparentPaint.setColor(context.getColor(android.R.color.transparent));
114         mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
115         mHandler = handler;
116         setDarkIntensity(0);
117     }
118 
updateScaledAttributionPath()119     private void updateScaledAttributionPath() {
120         if (getBounds().isEmpty()) {
121             mAttributionScaleMatrix.setScale(1f, 1f);
122         } else {
123             mAttributionScaleMatrix.setScale(
124                     getBounds().width() / VIEWPORT, getBounds().height() / VIEWPORT);
125         }
126         mAttributionPath.transform(mAttributionScaleMatrix, mScaledAttributionPath);
127     }
128 
129     @Override
getIntrinsicWidth()130     public int getIntrinsicWidth() {
131         if (newStatusBarIcons()) {
132             return super.getIntrinsicWidth();
133         } else {
134             return mIntrinsicSize;
135         }
136     }
137 
138     @Override
getIntrinsicHeight()139     public int getIntrinsicHeight() {
140         if (newStatusBarIcons()) {
141             return super.getIntrinsicHeight();
142         } else {
143             return mIntrinsicSize;
144         }
145     }
146 
updateAnimation()147     private void updateAnimation() {
148         boolean shouldAnimate = isInState(STATE_CARRIER_CHANGE) && isVisible();
149         if (shouldAnimate == mAnimating) return;
150         mAnimating = shouldAnimate;
151         if (shouldAnimate) {
152             mChangeDot.run();
153         } else {
154             mHandler.removeCallbacks(mChangeDot);
155         }
156     }
157 
158     @Override
onLevelChange(int packedState)159     protected boolean onLevelChange(int packedState) {
160         super.onLevelChange(unpackLevel(packedState));
161         updateAnimation();
162         setTintList(ColorStateList.valueOf(mForegroundPaint.getColor()));
163         invalidateSelf();
164         return true;
165     }
166 
unpackLevel(int packedState)167     private int unpackLevel(int packedState) {
168         int numBins = (packedState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT;
169         int cutOutOffset = 0;
170         int levelOffset = numBins == (CellSignalStrength.getNumSignalStrengthLevels() + 1) ? 10 : 0;
171         int level = (packedState & LEVEL_MASK);
172 
173         if (newStatusBarIcons()) {
174             if (isInState(STATE_CUT)) {
175                 cutOutOffset = 20;
176             }
177         }
178 
179         return level + levelOffset + cutOutOffset;
180     }
181 
setDarkIntensity(float darkIntensity)182     public void setDarkIntensity(float darkIntensity) {
183         if (darkIntensity == mDarkIntensity) {
184             return;
185         }
186         setTintList(ColorStateList.valueOf(getFillColor(darkIntensity)));
187     }
188 
189     @Override
setTintList(ColorStateList tint)190     public void setTintList(ColorStateList tint) {
191         super.setTintList(tint);
192         int colorForeground = mForegroundPaint.getColor();
193         mForegroundPaint.setColor(tint.getDefaultColor());
194         if (colorForeground != mForegroundPaint.getColor()) invalidateSelf();
195     }
196 
getFillColor(float darkIntensity)197     private int getFillColor(float darkIntensity) {
198         return getColorForDarkIntensity(
199                 darkIntensity, mLightModeFillColor, mDarkModeFillColor);
200     }
201 
getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor)202     private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
203         return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
204     }
205 
206     @Override
onBoundsChange(Rect bounds)207     protected void onBoundsChange(Rect bounds) {
208         super.onBoundsChange(bounds);
209         updateScaledAttributionPath();
210         invalidateSelf();
211     }
212 
213     @Override
draw(@onNull Canvas canvas)214     public void draw(@NonNull Canvas canvas) {
215         canvas.saveLayer(null, null);
216         final float width = getBounds().width();
217         final float height = getBounds().height();
218 
219         boolean isRtl = getLayoutDirection() == LayoutDirection.RTL;
220         if (isRtl) {
221             canvas.save();
222             // Mirror the drawable
223             canvas.translate(width, 0);
224             canvas.scale(-1.0f, 1.0f);
225         }
226         super.draw(canvas);
227         mCutoutPath.reset();
228         mCutoutPath.setFillType(FillType.WINDING);
229 
230         final float padding = Math.round(PAD * width);
231 
232         if (isInState(STATE_CARRIER_CHANGE)) {
233             float dotSize = (DOT_SIZE * height);
234             float dotPadding = (DOT_PADDING * height);
235             float dotSpacing = dotPadding + dotSize;
236             float x = width - padding - dotSize;
237             float y = height - padding - dotSize;
238             mForegroundPath.reset();
239             drawDotAndPadding(x, y, dotPadding, dotSize, 2);
240             drawDotAndPadding(x - dotSpacing, y, dotPadding, dotSize, 1);
241             drawDotAndPadding(x - dotSpacing * 2, y, dotPadding, dotSize, 0);
242             canvas.drawPath(mCutoutPath, mTransparentPaint);
243             canvas.drawPath(mForegroundPath, mForegroundPaint);
244         } else if (!newStatusBarIcons() && isInState(STATE_CUT)) {
245             float cutX = (mCutoutWidthFraction * width / VIEWPORT);
246             float cutY = (mCutoutHeightFraction * height / VIEWPORT);
247             mCutoutPath.moveTo(width, height);
248             mCutoutPath.rLineTo(-cutX, 0);
249             mCutoutPath.rLineTo(0, -cutY);
250             mCutoutPath.rLineTo(cutX, 0);
251             mCutoutPath.rLineTo(0, cutY);
252             canvas.drawPath(mCutoutPath, mTransparentPaint);
253             canvas.drawPath(mScaledAttributionPath, mForegroundPaint);
254         }
255         if (isRtl) {
256             canvas.restore();
257         }
258         canvas.restore();
259     }
260 
drawDotAndPadding(float x, float y, float dotPadding, float dotSize, int i)261     private void drawDotAndPadding(float x, float y,
262             float dotPadding, float dotSize, int i) {
263         if (i == mCurrentDot) {
264             // Draw dot
265             mForegroundPath.addRect(x, y, x + dotSize, y + dotSize, Direction.CW);
266             // Draw dot padding
267             mCutoutPath.addRect(x - dotPadding, y - dotPadding, x + dotSize + dotPadding,
268                     y + dotSize + dotPadding, Direction.CW);
269         }
270     }
271 
272     @Override
setAlpha(@ntRangefrom = 0, to = 255) int alpha)273     public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
274         super.setAlpha(alpha);
275         mForegroundPaint.setAlpha(alpha);
276     }
277 
278     @Override
setColorFilter(@ullable ColorFilter colorFilter)279     public void setColorFilter(@Nullable ColorFilter colorFilter) {
280         super.setColorFilter(colorFilter);
281         mForegroundPaint.setColorFilter(colorFilter);
282     }
283 
284     @Override
setVisible(boolean visible, boolean restart)285     public boolean setVisible(boolean visible, boolean restart) {
286         boolean changed = super.setVisible(visible, restart);
287         updateAnimation();
288         return changed;
289     }
290 
291     private final Runnable mChangeDot = new Runnable() {
292         @Override
293         public void run() {
294             if (++mCurrentDot == NUM_DOTS) {
295                 mCurrentDot = 0;
296             }
297             invalidateSelf();
298             mHandler.postDelayed(mChangeDot, DOT_DELAY);
299         }
300     };
301 
302     /**
303      * Returns whether this drawable is in the specified state.
304      *
305      * @param state must be one of {@link #STATE_CARRIER_CHANGE} or {@link #STATE_CUT}
306      */
isInState(int state)307     private boolean isInState(int state) {
308         return getState(getLevel()) == state;
309     }
310 
getState(int fullState)311     public static int getState(int fullState) {
312         return (fullState & STATE_MASK) >> STATE_SHIFT;
313     }
314 
getState(int level, int numLevels, boolean cutOut)315     public static int getState(int level, int numLevels, boolean cutOut) {
316         return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT)
317                 | (numLevels << NUM_LEVEL_SHIFT)
318                 | level;
319     }
320 
321     @Override
equals(@ullable Object other)322     public boolean equals(@Nullable Object other) {
323         return other instanceof SignalDrawable
324                 && ((SignalDrawable) other).getLevel() == this.getLevel();
325     }
326 
327     @Override
hashCode()328     public int hashCode() {
329         return Objects.hash(getLevel());
330     }
331 
332     /** Returns the state representing empty mobile signal with the given number of levels. */
getEmptyState(int numLevels)333     public static int getEmptyState(int numLevels) {
334         return getState(0, numLevels, true);
335     }
336 
337     /** Returns the state representing carrier change with the given number of levels. */
getCarrierChangeState(int numLevels)338     public static int getCarrierChangeState(int numLevels) {
339         return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
340     }
341 
getIconRes()342     private static int getIconRes() {
343         if (newStatusBarIcons()) {
344             return R.drawable.ic_mobile_level_list;
345         } else {
346             return com.android.internal.R.drawable.ic_signal_cellular;
347         }
348     }
349 }
350