aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorJason A. Donenfeld <Jason@zx2c4.com>2018-07-09 18:40:41 +0200
committerJason A. Donenfeld <Jason@zx2c4.com>2018-07-09 18:55:55 +0200
commite80da3ee14660f1d056fd10d66a23f051098edee (patch)
tree8d1bdb5cd404b2f5ec1a4ced06953b86c0c6f708
parentBaseFragment: in the event no view is available, use toast (diff)
downloadwireguard-android-e80da3ee14660f1d056fd10d66a23f051098edee.tar.xz
wireguard-android-e80da3ee14660f1d056fd10d66a23f051098edee.zip
QuickTileService: automatically slash the tile
-rw-r--r--app/src/main/java/com/wireguard/android/QuickTileService.java30
-rw-r--r--app/src/main/java/com/wireguard/android/widget/SlashDrawable.java236
-rw-r--r--app/src/main/res/drawable/ic_tile_disabled.xml29
3 files changed, 262 insertions, 33 deletions
diff --git a/app/src/main/java/com/wireguard/android/QuickTileService.java b/app/src/main/java/com/wireguard/android/QuickTileService.java
index c93a7c5..2108103 100644
--- a/app/src/main/java/com/wireguard/android/QuickTileService.java
+++ b/app/src/main/java/com/wireguard/android/QuickTileService.java
@@ -10,6 +10,10 @@ import android.annotation.TargetApi;
import android.content.Intent;
import android.databinding.Observable;
import android.databinding.Observable.OnPropertyChangedCallback;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.service.quicksettings.Tile;
@@ -21,6 +25,7 @@ import com.wireguard.android.activity.MainActivity;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.util.ExceptionLoggers;
+import com.wireguard.android.widget.SlashDrawable;
import java.util.Objects;
@@ -37,6 +42,26 @@ public class QuickTileService extends TileService {
private final OnStateChangedCallback onStateChangedCallback = new OnStateChangedCallback();
private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback();
private Tunnel tunnel;
+ private Icon iconOn;
+ private Icon iconOff;
+
+ @Override
+ public void onCreate() {
+ final SlashDrawable icon = new SlashDrawable(getResources().getDrawable(R.drawable.ic_tile));
+ icon.setAnimationEnabled(false); /* Unfortunately we can't have animations, since Icons are marshaled. */
+ icon.setSlashed(false);
+ Bitmap b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(b);
+ icon.setBounds(0, 0, c.getWidth(), c.getHeight());
+ icon.draw(c);
+ iconOn = Icon.createWithBitmap(b);
+ icon.setSlashed(true);
+ b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ c = new Canvas(b);
+ icon.setBounds(0, 0, c.getWidth(), c.getHeight());
+ icon.draw(c);
+ iconOff = Icon.createWithBitmap(b);
+ }
@Override
public void onClick() {
@@ -99,10 +124,7 @@ public class QuickTileService extends TileService {
return;
tile.setLabel(label);
if (tile.getState() != state) {
- // The icon must be changed every time the state changes, or the shade will not change.
- final Integer iconResource = state == Tile.STATE_ACTIVE ? R.drawable.ic_tile
- : R.drawable.ic_tile_disabled;
- tile.setIcon(Icon.createWithResource(this, iconResource));
+ tile.setIcon(state == Tile.STATE_ACTIVE ? iconOn : iconOff);
tile.setState(state);
}
tile.updateTile();
diff --git a/app/src/main/java/com/wireguard/android/widget/SlashDrawable.java b/app/src/main/java/com/wireguard/android/widget/SlashDrawable.java
new file mode 100644
index 0000000..bd2da16
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/widget/SlashDrawable.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright © 2018 The Android Open Source Project
+ * Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.widget;
+
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.TargetApi;
+import android.content.res.ColorStateList;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.FloatProperty;
+
+
+@TargetApi(Build.VERSION_CODES.N)
+public class SlashDrawable extends Drawable {
+
+ private static final float CORNER_RADIUS = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0f : 1f;
+ private static final long QS_ANIM_LENGTH = 350;
+
+ private final Path mPath = new Path();
+ private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ // These values are derived in un-rotated (vertical) orientation
+ private static final float SLASH_WIDTH = 1.8384776f;
+ private static final float SLASH_HEIGHT = 28f;
+ private static final float CENTER_X = 10.65f;
+ private static final float CENTER_Y = 11.869239f;
+ private static final float SCALE = 24f;
+
+ // Bottom is derived during animation
+ private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
+ private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
+ private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
+ // Draw the slash washington-monument style; rotate to no-u-turn style
+ private static final float DEFAULT_ROTATION = -45f;
+
+ private Drawable mDrawable;
+ private final RectF mSlashRect = new RectF(0, 0, 0, 0);
+ private float mRotation;
+ private boolean mSlashed;
+ private Mode mTintMode;
+ private ColorStateList mTintList;
+ private boolean mAnimationEnabled = true;
+
+ public SlashDrawable(final Drawable d) {
+ setDrawable(d);
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mDrawable != null ? mDrawable.getIntrinsicHeight(): 0;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mDrawable != null ? mDrawable.getIntrinsicWidth(): 0;
+ }
+
+ @Override
+ protected void onBoundsChange(final Rect bounds) {
+ super.onBoundsChange(bounds);
+ mDrawable.setBounds(bounds);
+ }
+
+ public void setDrawable(final Drawable d) {
+ mDrawable = d;
+ mDrawable.setCallback(getCallback());
+ mDrawable.setBounds(getBounds());
+ if (mTintMode != null)
+ mDrawable.setTintMode(mTintMode);
+ if (mTintList != null)
+ mDrawable.setTintList(mTintList);
+ invalidateSelf();
+ }
+
+ public void setRotation(final float rotation) {
+ if (mRotation == rotation)
+ return;
+ mRotation = rotation;
+ invalidateSelf();
+ }
+
+ public void setAnimationEnabled(final boolean enabled) {
+ mAnimationEnabled = enabled;
+ }
+
+ // Animate this value on change
+ private float mCurrentSlashLength;
+ private static final FloatProperty mSlashLengthProp = new FloatProperty<SlashDrawable>("slashLength") {
+ @Override
+ public void setValue(final SlashDrawable object, final float value) {
+ object.mCurrentSlashLength = value;
+ }
+
+ @Override
+ public Float get(final SlashDrawable object) {
+ return object.mCurrentSlashLength;
+ }
+ };
+
+ public void setSlashed(final boolean slashed) {
+ if (mSlashed == slashed) return;
+
+ mSlashed = slashed;
+
+ final float end = mSlashed ? SLASH_HEIGHT / SCALE : 0f;
+ final float start = mSlashed ? 0f : SLASH_HEIGHT / SCALE;
+
+ if (mAnimationEnabled) {
+ final ObjectAnimator anim = ObjectAnimator.ofFloat(this, mSlashLengthProp, start, end);
+ anim.addUpdateListener((ValueAnimator valueAnimator) -> invalidateSelf());
+ anim.setDuration(QS_ANIM_LENGTH);
+ anim.start();
+ } else {
+ mCurrentSlashLength = end;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void draw(@NonNull final Canvas canvas) {
+ canvas.save();
+ final Matrix m = new Matrix();
+ final int width = getBounds().width();
+ final int height = getBounds().height();
+ final float radiusX = scale(CORNER_RADIUS, width);
+ final float radiusY = scale(CORNER_RADIUS, height);
+ updateRect(
+ scale(LEFT, width),
+ scale(TOP, height),
+ scale(RIGHT, width),
+ scale(TOP + mCurrentSlashLength, height)
+ );
+
+ mPath.reset();
+ // Draw the slash vertically
+ mPath.addRoundRect(mSlashRect, radiusX, radiusY, Direction.CW);
+ // Rotate -45 + desired rotation
+ m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
+ mPath.transform(m);
+ canvas.drawPath(mPath, mPaint);
+
+ // Rotate back to vertical
+ m.setRotate(-mRotation - DEFAULT_ROTATION, width / 2, height / 2);
+ mPath.transform(m);
+
+ // Draw another rect right next to the first, for clipping
+ m.setTranslate(mSlashRect.width(), 0);
+ mPath.transform(m);
+ mPath.addRoundRect(mSlashRect, 1.0f * width, 1.0f * height, Direction.CW);
+ m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
+ mPath.transform(m);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
+ canvas.clipPath(mPath, Region.Op.DIFFERENCE);
+ else
+ canvas.clipOutPath(mPath);
+
+ mDrawable.draw(canvas);
+ canvas.restore();
+ }
+
+ private float scale(final float frac, final int width) {
+ return frac * width;
+ }
+
+ private void updateRect(final float left, final float top, final float right, final float bottom) {
+ mSlashRect.left = left;
+ mSlashRect.top = top;
+ mSlashRect.right = right;
+ mSlashRect.bottom = bottom;
+ }
+
+ @Override
+ public void setTint(@ColorInt final int tintColor) {
+ super.setTint(tintColor);
+ mDrawable.setTint(tintColor);
+ mPaint.setColor(tintColor);
+ }
+
+ @Override
+ public void setTintList(@Nullable final ColorStateList tint) {
+ mTintList = tint;
+ super.setTintList(tint);
+ setDrawableTintList(tint);
+ mPaint.setColor(tint == null ? 0 : tint.getDefaultColor());
+ invalidateSelf();
+ }
+
+ private void setDrawableTintList(@Nullable final ColorStateList tint) {
+ mDrawable.setTintList(tint);
+ }
+
+ @Override
+ public void setTintMode(@NonNull final Mode tintMode) {
+ mTintMode = tintMode;
+ super.setTintMode(tintMode);
+ mDrawable.setTintMode(tintMode);
+ }
+
+ @Override
+ public void setAlpha(@IntRange(from = 0, to = 255) final int alpha) {
+ mDrawable.setAlpha(alpha);
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(@Nullable final ColorFilter colorFilter) {
+ mDrawable.setColorFilter(colorFilter);
+ mPaint.setColorFilter(colorFilter);
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.OPAQUE;
+ }
+}
diff --git a/app/src/main/res/drawable/ic_tile_disabled.xml b/app/src/main/res/drawable/ic_tile_disabled.xml
deleted file mode 100644
index 3109c39..0000000
--- a/app/src/main/res/drawable/ic_tile_disabled.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="400dp"
- android:height="400dp"
- android:viewportHeight="400.0"
- android:viewportWidth="400.0">
- <path
- android:fillAlpha="1"
- android:fillColor="#ffffff"
- android:pathData="M197.7,0C191.5,0.1 185.2,0.5 178.7,1.1C191.1,4 202.2,6.6 213.4,9.2C213.3,9.8 213.1,10.5 213,11.2C198.1,13.2 183.9,7.7 169.3,5.7C174.6,8.8 180,11.7 185.5,14.2C191.2,16.7 197,18.8 202.8,21.2C195.4,27.5 188,28.8 178.7,26.7C173.7,25.6 168.3,25 163.1,25.2C157.8,25.5 152.4,26.8 147.5,30C152.7,32.7 157.5,34.8 162.1,37.5C163.9,38.6 166.1,40.4 166.6,42.4C167.8,46.9 168.2,51.8 168.9,56.5C160.4,57.5 145.4,66.1 142.4,71.7C155.5,74.3 169.7,71.2 182.2,79.6C178.1,82.8 168.5,86.6 165,89.3C169.3,90.4 179.4,89.9 183.3,89.6C186.6,89.4 188.2,89.3 189.5,90.4L228,120.6C232.1,123.8 248.4,139.3 252.7,149C256.3,157.3 256.8,164.4 256.8,166.1C256.8,170.7 256.2,177.9 253,186C252.7,186.9 252,188.3 251.2,189.8L280.8,219.4C288.3,207.7 293.4,194.5 296.3,180C299.6,163.2 299.4,146.5 291.7,130.5C285.8,118.3 276.1,109.4 265.8,101.3C255.1,93 243.7,85.4 233.1,77C230.2,74.8 228.3,70.8 226.9,67.3C226.4,65.8 228.2,61.7 229.4,61.5C236,60.3 242.6,59.7 249.3,59.4C256.9,59.2 264.6,59.4 272.3,59.5C274,59.5 276.2,59.3 277.2,60.3C281.2,64.2 284.3,61.6 287,59.1C289.4,56.9 291,54 292.9,51.6C291.7,51.4 289.4,50.9 287.1,50.8C279.4,50.6 271.7,50.7 264,50.5C262.6,50.4 261.3,49 259.9,48.2C261.3,47.6 262.8,46.6 264.2,46.6C277.5,46.5 290.8,46.5 304.1,46.5C304.2,39.6 294.9,30.1 286.7,27.5C286.6,28.5 286.5,29.3 286.5,30.3C278.3,30.5 270.3,30.3 263,26.4C261.1,25.4 259.8,23.1 258.3,21.5C256.3,19.3 254.7,16.6 252.3,15.2C247.4,12.3 242,10.3 236.9,7.8C224.3,1.7 211.3,-0.1 197.7,0zM249.6,29.4C250.3,29.4 250.9,29.6 251.6,30.2C252.6,31 253.6,31.9 254.8,33C253.3,33.8 252,34.5 250.8,35.1C249,36 247.6,35.4 246.5,34C245.6,32.8 245.5,31.7 246.8,30.7C247.7,30 248.6,29.4 249.6,29.4zM231.8,213C220.7,221.9 207.6,227.2 193.6,230.1C152.7,238.5 118.7,282.2 128.3,330.3C139.5,386.5 201.5,416.9 252.2,390.2C285,372.9 302.3,339.2 297.7,302.5C295.9,288.1 291,275.4 283.1,264.3L265.1,246.3C264.2,246.2 263.2,246.6 261.9,247.5C253.1,253.1 244.2,258.4 235.1,263.4C229.9,266.2 224.2,268.3 217.7,271.2C219.9,271.8 221,272.1 222.1,272.4C246.5,278.9 259.6,300.3 253.8,324.4C248.7,345.7 227,359.4 205.9,355.8C188.4,352.8 173.1,338.2 170.5,320.8C167.8,301.8 177.2,283.5 194.1,275.9C203.4,271.6 213,267.9 222.3,263.6C232.9,258.7 244.4,254.9 253.6,248.1C256.2,246.2 258.7,244.2 261,242.2L231.8,213z"
- android:strokeColor="#00000000"
- android:strokeWidth="1.3750788" />
- <path
- android:fillAlpha="1"
- android:fillColor="#ffffff"
- android:pathData="m97.9,307.6c-7.8,2 -15.4,4.9 -23.4,7.5 3.9,-26.3 34.7,-50.6 60.8,-47.8 -8.1,10.9 -11.8,23.3 -12.7,35.6 -8.7,1.6 -16.8,2.7 -24.8,4.7"
- android:strokeColor="#00000000"
- android:strokeWidth="1.3750788" />
- <path
- android:fillAlpha="1"
- android:fillColor="#ffffff"
- android:pathData="M177,111.6C175.7,111.6 174.4,111.7 173.1,111.7L247,185.6C248.1,183.2 249,180.6 249.9,177.9C252.2,170.4 251.8,159.3 248,152.1C234.7,126.6 206.6,111.3 177,111.6zM139.7,120.9C137.9,121.8 136.1,122.9 134.3,124C81.2,156.4 84.3,231 132.9,260.7C136.6,263 138.3,262.7 140.8,259.4C148.4,249.2 157.9,241.1 169,234.9C171.7,233.3 174.5,231.9 178.1,230C154,225.8 141.1,214.8 139.6,197.8C137.9,179.4 146.9,164.5 163.2,158.3C167,156.9 171,156.2 174.9,156.1L139.7,120.9zM212.5,193.7C212.5,195.8 212.3,197.9 211.8,200.1C209.9,210.5 203.6,218.1 195.8,224.8C208.5,221.9 219.6,216.9 228.6,209.7L212.5,193.7z"
- android:strokeColor="#00000000"
- android:strokeWidth="1.3750788" />
- <path
- android:fillAlpha="1"
- android:fillColor="#ffffff"
- android:pathData="M78.4,59.6l262,262l-21.3,21.3l-262,-262z"
- android:strokeWidth="0.82064575" />
-</vector>