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