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 /**
46  * Drawable displaying a mobile cell signal indicator.
47  */
48 public class SignalDrawable extends DrawableWrapper {
49 
50     private static final String TAG = "SignalDrawable";
51 
52     private static final int NUM_DOTS = 3;
53 
54     private static final float VIEWPORT = 24f;
55     private static final float PAD = 2f / VIEWPORT;
56 
57     private static final float DOT_SIZE = 3f / VIEWPORT;
58     private static final float DOT_PADDING = 1.5f / VIEWPORT;
59 
60     // All of these are masks to push all of the drawable state into one int for easy callbacks
61     // and flow through sysui.
62     private static final int LEVEL_MASK = 0xff;
63     private static final int NUM_LEVEL_SHIFT = 8;
64     private static final int NUM_LEVEL_MASK = 0xff << NUM_LEVEL_SHIFT;
65     private static final int STATE_SHIFT = 16;
66     private static final int STATE_MASK = 0xff << STATE_SHIFT;
67     private static final int STATE_CUT = 2;
68     private static final int STATE_CARRIER_CHANGE = 3;
69 
70     private static final long DOT_DELAY = 1000;
71 
72     // Check the config for which icon we want to use
73     private static final int ICON_RES = SignalDrawable.getIconRes();
74 
75     private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
76     private final Paint mTransparentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
77     private final int mDarkModeFillColor;
78     private final int mLightModeFillColor;
79     private final Path mCutoutPath = new Path();
80     private final Path mForegroundPath = new Path();
81     private final Path mAttributionPath = new Path();
82     private final Matrix mAttributionScaleMatrix = new Matrix();
83     private final Path mScaledAttributionPath = new Path();
84     private final Handler mHandler;
85     private final float mCutoutWidthFraction;
86     private final float mCutoutHeightFraction;
87     private float mDarkIntensity = -1;
88     private final int mIntrinsicSize;
89     private boolean mAnimating;
90     private int mCurrentDot;
91 
SignalDrawable(Context context)92     public SignalDrawable(Context context) {
93         super(context.getDrawable(ICON_RES));
94         final String attributionPathString = context.getString(
95                 com.android.internal.R.string.config_signalAttributionPath);
96         mAttributionPath.set(PathParser.createPathFromPathData(attributionPathString));
97         updateScaledAttributionPath();
98         mCutoutWidthFraction = context.getResources().getFloat(
99                 com.android.internal.R.dimen.config_signalCutoutWidthFraction);
100         mCutoutHeightFraction = context.getResources().getFloat(
101                 com.android.internal.R.dimen.config_signalCutoutHeightFraction);
102         mDarkModeFillColor = Utils.getColorStateListDefaultColor(context,
103                 R.color.dark_mode_icon_color_single_tone);
104         mLightModeFillColor = Utils.getColorStateListDefaultColor(context,
105                 R.color.light_mode_icon_color_single_tone);
106         mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
107         mTransparentPaint.setColor(context.getColor(android.R.color.transparent));
108         mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
109         mHandler = new Handler();
110         setDarkIntensity(0);
111     }
112 
updateScaledAttributionPath()113     private void updateScaledAttributionPath() {
114         if (getBounds().isEmpty()) {
115             mAttributionScaleMatrix.setScale(1f, 1f);
116         } else {
117             mAttributionScaleMatrix.setScale(
118                     getBounds().width() / VIEWPORT, getBounds().height() / VIEWPORT);
119         }
120         mAttributionPath.transform(mAttributionScaleMatrix, mScaledAttributionPath);
121     }
122 
123     @Override
getIntrinsicWidth()124     public int getIntrinsicWidth() {
125         return mIntrinsicSize;
126     }
127 
128     @Override
getIntrinsicHeight()129     public int getIntrinsicHeight() {
130         return mIntrinsicSize;
131     }
132 
updateAnimation()133     private void updateAnimation() {
134         boolean shouldAnimate = isInState(STATE_CARRIER_CHANGE) && isVisible();
135         if (shouldAnimate == mAnimating) return;
136         mAnimating = shouldAnimate;
137         if (shouldAnimate) {
138             mChangeDot.run();
139         } else {
140             mHandler.removeCallbacks(mChangeDot);
141         }
142     }
143 
144     @Override
onLevelChange(int packedState)145     protected boolean onLevelChange(int packedState) {
146         super.onLevelChange(unpackLevel(packedState));
147         updateAnimation();
148         setTintList(ColorStateList.valueOf(mForegroundPaint.getColor()));
149         invalidateSelf();
150         return true;
151     }
152 
unpackLevel(int packedState)153     private int unpackLevel(int packedState) {
154         int numBins = (packedState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT;
155         int cutOutOffset = 0;
156         int levelOffset = numBins == (CellSignalStrength.getNumSignalStrengthLevels() + 1) ? 10 : 0;
157         int level = (packedState & LEVEL_MASK);
158 
159         if (newStatusBarIcons()) {
160             if (isInState(STATE_CUT)) {
161                 cutOutOffset = 20;
162             }
163         }
164 
165         return level + levelOffset + cutOutOffset;
166     }
167 
setDarkIntensity(float darkIntensity)168     public void setDarkIntensity(float darkIntensity) {
169         if (darkIntensity == mDarkIntensity) {
170             return;
171         }
172         setTintList(ColorStateList.valueOf(getFillColor(darkIntensity)));
173     }
174 
175     @Override
setTintList(ColorStateList tint)176     public void setTintList(ColorStateList tint) {
177         super.setTintList(tint);
178         int colorForeground = mForegroundPaint.getColor();
179         mForegroundPaint.setColor(tint.getDefaultColor());
180         if (colorForeground != mForegroundPaint.getColor()) invalidateSelf();
181     }
182 
getFillColor(float darkIntensity)183     private int getFillColor(float darkIntensity) {
184         return getColorForDarkIntensity(
185                 darkIntensity, mLightModeFillColor, mDarkModeFillColor);
186     }
187 
getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor)188     private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
189         return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
190     }
191 
192     @Override
onBoundsChange(Rect bounds)193     protected void onBoundsChange(Rect bounds) {
194         super.onBoundsChange(bounds);
195         updateScaledAttributionPath();
196         invalidateSelf();
197     }
198 
199     @Override
draw(@onNull Canvas canvas)200     public void draw(@NonNull Canvas canvas) {
201         canvas.saveLayer(null, null);
202         final float width = getBounds().width();
203         final float height = getBounds().height();
204 
205         boolean isRtl = getLayoutDirection() == LayoutDirection.RTL;
206         if (isRtl) {
207             canvas.save();
208             // Mirror the drawable
209             canvas.translate(width, 0);
210             canvas.scale(-1.0f, 1.0f);
211         }
212         super.draw(canvas);
213         mCutoutPath.reset();
214         mCutoutPath.setFillType(FillType.WINDING);
215 
216         final float padding = Math.round(PAD * width);
217 
218         if (isInState(STATE_CARRIER_CHANGE)) {
219             float dotSize = (DOT_SIZE * height);
220             float dotPadding = (DOT_PADDING * height);
221             float dotSpacing = dotPadding + dotSize;
222             float x = width - padding - dotSize;
223             float y = height - padding - dotSize;
224             mForegroundPath.reset();
225             drawDotAndPadding(x, y, dotPadding, dotSize, 2);
226             drawDotAndPadding(x - dotSpacing, y, dotPadding, dotSize, 1);
227             drawDotAndPadding(x - dotSpacing * 2, y, dotPadding, dotSize, 0);
228             canvas.drawPath(mCutoutPath, mTransparentPaint);
229             canvas.drawPath(mForegroundPath, mForegroundPaint);
230         } else if (!newStatusBarIcons() && isInState(STATE_CUT)) {
231             float cutX = (mCutoutWidthFraction * width / VIEWPORT);
232             float cutY = (mCutoutHeightFraction * height / VIEWPORT);
233             mCutoutPath.moveTo(width, height);
234             mCutoutPath.rLineTo(-cutX, 0);
235             mCutoutPath.rLineTo(0, -cutY);
236             mCutoutPath.rLineTo(cutX, 0);
237             mCutoutPath.rLineTo(0, cutY);
238             canvas.drawPath(mCutoutPath, mTransparentPaint);
239             canvas.drawPath(mScaledAttributionPath, mForegroundPaint);
240         }
241         if (isRtl) {
242             canvas.restore();
243         }
244         canvas.restore();
245     }
246 
drawDotAndPadding(float x, float y, float dotPadding, float dotSize, int i)247     private void drawDotAndPadding(float x, float y,
248             float dotPadding, float dotSize, int i) {
249         if (i == mCurrentDot) {
250             // Draw dot
251             mForegroundPath.addRect(x, y, x + dotSize, y + dotSize, Direction.CW);
252             // Draw dot padding
253             mCutoutPath.addRect(x - dotPadding, y - dotPadding, x + dotSize + dotPadding,
254                     y + dotSize + dotPadding, Direction.CW);
255         }
256     }
257 
258     @Override
setAlpha(@ntRangefrom = 0, to = 255) int alpha)259     public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
260         super.setAlpha(alpha);
261         mForegroundPaint.setAlpha(alpha);
262     }
263 
264     @Override
setColorFilter(@ullable ColorFilter colorFilter)265     public void setColorFilter(@Nullable ColorFilter colorFilter) {
266         super.setColorFilter(colorFilter);
267         mForegroundPaint.setColorFilter(colorFilter);
268     }
269 
270     @Override
setVisible(boolean visible, boolean restart)271     public boolean setVisible(boolean visible, boolean restart) {
272         boolean changed = super.setVisible(visible, restart);
273         updateAnimation();
274         return changed;
275     }
276 
277     private final Runnable mChangeDot = new Runnable() {
278         @Override
279         public void run() {
280             if (++mCurrentDot == NUM_DOTS) {
281                 mCurrentDot = 0;
282             }
283             invalidateSelf();
284             mHandler.postDelayed(mChangeDot, DOT_DELAY);
285         }
286     };
287 
288     /**
289      * Returns whether this drawable is in the specified state.
290      *
291      * @param state must be one of {@link #STATE_CARRIER_CHANGE} or {@link #STATE_CUT}
292      */
isInState(int state)293     private boolean isInState(int state) {
294         return getState(getLevel()) == state;
295     }
296 
getState(int fullState)297     public static int getState(int fullState) {
298         return (fullState & STATE_MASK) >> STATE_SHIFT;
299     }
300 
getState(int level, int numLevels, boolean cutOut)301     public static int getState(int level, int numLevels, boolean cutOut) {
302         return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT)
303                 | (numLevels << NUM_LEVEL_SHIFT)
304                 | level;
305     }
306 
307     /** Returns the state representing empty mobile signal with the given number of levels. */
getEmptyState(int numLevels)308     public static int getEmptyState(int numLevels) {
309         return getState(0, numLevels, true);
310     }
311 
312     /** Returns the state representing carrier change with the given number of levels. */
getCarrierChangeState(int numLevels)313     public static int getCarrierChangeState(int numLevels) {
314         return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
315     }
316 
getIconRes()317     private static int getIconRes() {
318         if (newStatusBarIcons()) {
319             return R.drawable.ic_mobile_level_list;
320         } else {
321             return com.android.internal.R.drawable.ic_signal_cellular;
322         }
323     }
324 }
325