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