summaryrefslogtreecommitdiffhomepage
path: root/ui/src/main/java/com/wireguard/android/widget
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/main/java/com/wireguard/android/widget')
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.java54
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.java59
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/NameInputFilter.java53
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/SlashDrawable.java217
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.java59
5 files changed, 442 insertions, 0 deletions
diff --git a/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.java b/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.java
new file mode 100644
index 00000000..79572aa3
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.widget;
+
+import androidx.annotation.Nullable;
+import android.text.InputFilter;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+
+import com.wireguard.crypto.Key;
+
+/**
+ * InputFilter for entering WireGuard private/public keys encoded with base64.
+ */
+
+public class KeyInputFilter implements InputFilter {
+ private static boolean isAllowed(final char c) {
+ return Character.isLetterOrDigit(c) || c == '+' || c == '/';
+ }
+
+ public static InputFilter newInstance() {
+ return new KeyInputFilter();
+ }
+
+ @Nullable
+ @Override
+ public CharSequence filter(final CharSequence source,
+ final int sStart, final int sEnd,
+ final Spanned dest,
+ final int dStart, final int dEnd) {
+ SpannableStringBuilder replacement = null;
+ int rIndex = 0;
+ final int dLength = dest.length();
+ for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
+ final char c = source.charAt(sIndex);
+ final int dIndex = dStart + (sIndex - sStart);
+ // Restrict characters to the base64 character set.
+ // Ensure adding this character does not push the length over the limit.
+ if (((dIndex + 1 < Key.Format.BASE64.getLength() && isAllowed(c)) ||
+ (dIndex + 1 == Key.Format.BASE64.getLength() && c == '=')) &&
+ dLength + (sIndex - sStart) < Key.Format.BASE64.getLength()) {
+ ++rIndex;
+ } else {
+ if (replacement == null)
+ replacement = new SpannableStringBuilder(source, sStart, sEnd);
+ replacement.delete(rIndex, rIndex + 1);
+ }
+ }
+ return replacement;
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.java b/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.java
new file mode 100644
index 00000000..2fe9c924
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.RelativeLayout;
+
+import com.wireguard.android.R;
+
+public class MultiselectableRelativeLayout extends RelativeLayout {
+ private static final int[] STATE_MULTISELECTED = {R.attr.state_multiselected};
+ private boolean multiselected;
+
+ public MultiselectableRelativeLayout(final Context context) {
+ super(context);
+ }
+
+ public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(final int extraSpace) {
+ if (multiselected) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ mergeDrawableStates(drawableState, STATE_MULTISELECTED);
+ return drawableState;
+ }
+ return super.onCreateDrawableState(extraSpace);
+ }
+
+ public void setMultiSelected(final boolean on) {
+ if (!multiselected) {
+ multiselected = true;
+ refreshDrawableState();
+ }
+ setActivated(on);
+ }
+
+ public void setSingleSelected(final boolean on) {
+ if (multiselected) {
+ multiselected = false;
+ refreshDrawableState();
+ }
+ setActivated(on);
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.java b/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.java
new file mode 100644
index 00000000..030be25a
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.widget;
+
+import androidx.annotation.Nullable;
+import android.text.InputFilter;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+
+import com.wireguard.android.backend.Tunnel;
+
+/**
+ * InputFilter for entering WireGuard configuration names (Linux interface names).
+ */
+
+public class NameInputFilter implements InputFilter {
+ private static boolean isAllowed(final char c) {
+ return Character.isLetterOrDigit(c) || "_=+.-".indexOf(c) >= 0;
+ }
+
+ public static InputFilter newInstance() {
+ return new NameInputFilter();
+ }
+
+ @Nullable
+ @Override
+ public CharSequence filter(final CharSequence source,
+ final int sStart, final int sEnd,
+ final Spanned dest,
+ final int dStart, final int dEnd) {
+ SpannableStringBuilder replacement = null;
+ int rIndex = 0;
+ final int dLength = dest.length();
+ for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
+ final char c = source.charAt(sIndex);
+ final int dIndex = dStart + (sIndex - sStart);
+ // Restrict characters to those valid in interfaces.
+ // Ensure adding this character does not push the length over the limit.
+ if ((dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c)) &&
+ dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
+ ++rIndex;
+ } else {
+ if (replacement == null)
+ replacement = new SpannableStringBuilder(source, sStart, sEnd);
+ replacement.delete(rIndex, rIndex + 1);
+ }
+ }
+ return replacement;
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.java b/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.java
new file mode 100644
index 00000000..e020aa81
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright © 2018 The Android Open Source Project
+ * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.widget;
+
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+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 androidx.annotation.ColorInt;
+import androidx.annotation.IntRange;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import android.util.FloatProperty;
+
+@RequiresApi(Build.VERSION_CODES.N)
+public class SlashDrawable extends Drawable {
+
+ private static final float CENTER_X = 10.65f;
+ private static final float CENTER_Y = 11.869239f;
+ private static final float CORNER_RADIUS = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0f : 1f;
+ // Draw the slash washington-monument style; rotate to no-u-turn style
+ private static final float DEFAULT_ROTATION = -45f;
+ private static final long QS_ANIM_LENGTH = 350;
+ private static final float SCALE = 24f;
+ private static final float SLASH_HEIGHT = 28f;
+ // These values are derived in un-rotated (vertical) orientation
+ private static final float SLASH_WIDTH = 1.8384776f;
+ // Bottom is derived during animation
+ private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
+ private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
+ private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
+ private static final FloatProperty mSlashLengthProp = new FloatProperty<SlashDrawable>("slashLength") {
+ @Override
+ public Float get(final SlashDrawable object) {
+ return object.mCurrentSlashLength;
+ }
+
+ @Override
+ public void setValue(final SlashDrawable object, final float value) {
+ object.mCurrentSlashLength = value;
+ }
+ };
+ private final Drawable mDrawable;
+ private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Path mPath = new Path();
+ private final RectF mSlashRect = new RectF(0, 0, 0, 0);
+ private boolean mAnimationEnabled = true;
+ // Animate this value on change
+ private float mCurrentSlashLength;
+ private float mRotation;
+ private boolean mSlashed;
+
+ public SlashDrawable(final Drawable d) {
+ mDrawable = d;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void draw(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();
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mDrawable.getIntrinsicHeight();
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mDrawable.getIntrinsicWidth();
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public int getOpacity() {
+ return PixelFormat.OPAQUE;
+ }
+
+ @Override
+ protected void onBoundsChange(final Rect bounds) {
+ super.onBoundsChange(bounds);
+ mDrawable.setBounds(bounds);
+ }
+
+ private float scale(final float frac, final int width) {
+ return frac * width;
+ }
+
+ @Override
+ public void setAlpha(@IntRange(from = 0, to = 255) final int alpha) {
+ mDrawable.setAlpha(alpha);
+ mPaint.setAlpha(alpha);
+ }
+
+ public void setAnimationEnabled(final boolean enabled) {
+ mAnimationEnabled = enabled;
+ }
+
+ @Override
+ public void setColorFilter(@Nullable final ColorFilter colorFilter) {
+ mDrawable.setColorFilter(colorFilter);
+ mPaint.setColorFilter(colorFilter);
+ }
+
+ private void setDrawableTintList(@Nullable final ColorStateList tint) {
+ mDrawable.setTintList(tint);
+ }
+
+ public void setRotation(final float rotation) {
+ if (mRotation == rotation)
+ return;
+ mRotation = rotation;
+ invalidateSelf();
+ }
+
+ @SuppressWarnings("unchecked")
+ 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 setTint(@ColorInt final int tintColor) {
+ super.setTint(tintColor);
+ mDrawable.setTint(tintColor);
+ mPaint.setColor(tintColor);
+ }
+
+ @Override
+ public void setTintList(@Nullable final ColorStateList tint) {
+ super.setTintList(tint);
+ setDrawableTintList(tint);
+ mPaint.setColor(tint == null ? 0 : tint.getDefaultColor());
+ invalidateSelf();
+ }
+
+ @Override
+ public void setTintMode(final Mode tintMode) {
+ super.setTintMode(tintMode);
+ mDrawable.setTintMode(tintMode);
+ }
+
+ 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;
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.java b/ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.java
new file mode 100644
index 00000000..dcb9aceb
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright © 2013 The Android Open Source Project
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.widget;
+
+import android.content.Context;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import android.util.AttributeSet;
+import android.widget.Switch;
+
+public class ToggleSwitch extends Switch {
+ private boolean isRestoringState;
+ @Nullable private OnBeforeCheckedChangeListener listener;
+
+ public ToggleSwitch(final Context context) {
+ this(context, null);
+ }
+
+ @SuppressWarnings({"SameParameterValue", "WeakerAccess"})
+ public ToggleSwitch(final Context context, @Nullable final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void onRestoreInstanceState(final Parcelable state) {
+ isRestoringState = true;
+ super.onRestoreInstanceState(state);
+ isRestoringState = false;
+ }
+
+ @Override
+ public void setChecked(final boolean checked) {
+ if (checked == isChecked())
+ return;
+ if (isRestoringState || listener == null) {
+ super.setChecked(checked);
+ return;
+ }
+ setEnabled(false);
+ listener.onBeforeCheckedChanged(this, checked);
+ }
+
+ public void setCheckedInternal(final boolean checked) {
+ super.setChecked(checked);
+ setEnabled(true);
+ }
+
+ public void setOnBeforeCheckedChangeListener(final OnBeforeCheckedChangeListener listener) {
+ this.listener = listener;
+ }
+
+ public interface OnBeforeCheckedChangeListener {
+ void onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked);
+ }
+}