diff options
Diffstat (limited to 'ui/src/main/java/com/wireguard/android/widget')
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); + } +} |