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