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