From f304e40d57525f81a44dc4783aff568c9f704752 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 17:14:58 +0200 Subject: [PATCH 01/65] Import source from https://github.com/amulyakhare/TextDrawable --- dependencies_groups.gradle | 1 - library/external/textdrawable/build.gradle | 25 ++ .../textdrawable/TextDrawable.java | 316 ++++++++++++++++++ settings.gradle | 1 + vector/build.gradle | 2 +- 5 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 library/external/textdrawable/build.gradle create mode 100644 library/external/textdrawable/src/main/java/com/amulyakhare/textdrawable/TextDrawable.java diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 66d07f258b..07dfa8787f 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -238,7 +238,6 @@ ext.groups = [ regex: [ ], group: [ - 'com.amulyakhare', 'com.otaliastudios', 'com.yqritc', // https://github.com/cmelchior/realmfieldnameshelper/issues/42 diff --git a/library/external/textdrawable/build.gradle b/library/external/textdrawable/build.gradle new file mode 100644 index 0000000000..5eb27bf6aa --- /dev/null +++ b/library/external/textdrawable/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' + +apply plugin: 'com.android.library' + +android { + namespace "com.amulyakhare.textdrawable" + + compileSdk versions.compileSdk + + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + } + + compileOptions { + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat + } +} + +afterEvaluate { + tasks.findAll { it.name.startsWith("lint") }.each { + it.enabled = false + } +} diff --git a/library/external/textdrawable/src/main/java/com/amulyakhare/textdrawable/TextDrawable.java b/library/external/textdrawable/src/main/java/com/amulyakhare/textdrawable/TextDrawable.java new file mode 100644 index 0000000000..db42f8b764 --- /dev/null +++ b/library/external/textdrawable/src/main/java/com/amulyakhare/textdrawable/TextDrawable.java @@ -0,0 +1,316 @@ +package com.amulyakhare.textdrawable; + +import android.graphics.*; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.OvalShape; +import android.graphics.drawable.shapes.RectShape; +import android.graphics.drawable.shapes.RoundRectShape; + +/** + * @author amulya + * @datetime 14 Oct 2014, 3:53 PM + */ +public class TextDrawable extends ShapeDrawable { + + private final Paint textPaint; + private final Paint borderPaint; + private static final float SHADE_FACTOR = 0.9f; + private final String text; + private final int color; + private final RectShape shape; + private final int height; + private final int width; + private final int fontSize; + private final float radius; + private final int borderThickness; + + private TextDrawable(Builder builder) { + super(builder.shape); + + // shape properties + shape = builder.shape; + height = builder.height; + width = builder.width; + radius = builder.radius; + + // text and color + text = builder.toUpperCase ? builder.text.toUpperCase() : builder.text; + color = builder.color; + + // text paint settings + fontSize = builder.fontSize; + textPaint = new Paint(); + textPaint.setColor(builder.textColor); + textPaint.setAntiAlias(true); + textPaint.setFakeBoldText(builder.isBold); + textPaint.setStyle(Paint.Style.FILL); + textPaint.setTypeface(builder.font); + textPaint.setTextAlign(Paint.Align.CENTER); + textPaint.setStrokeWidth(builder.borderThickness); + + // border paint settings + borderThickness = builder.borderThickness; + borderPaint = new Paint(); + borderPaint.setColor(getDarkerShade(color)); + borderPaint.setStyle(Paint.Style.STROKE); + borderPaint.setStrokeWidth(borderThickness); + + // drawable paint color + Paint paint = getPaint(); + paint.setColor(color); + + } + + private int getDarkerShade(int color) { + return Color.rgb((int)(SHADE_FACTOR * Color.red(color)), + (int)(SHADE_FACTOR * Color.green(color)), + (int)(SHADE_FACTOR * Color.blue(color))); + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + Rect r = getBounds(); + + + // draw border + if (borderThickness > 0) { + drawBorder(canvas); + } + + int count = canvas.save(); + canvas.translate(r.left, r.top); + + // draw text + int width = this.width < 0 ? r.width() : this.width; + int height = this.height < 0 ? r.height() : this.height; + int fontSize = this.fontSize < 0 ? (Math.min(width, height) / 2) : this.fontSize; + textPaint.setTextSize(fontSize); + canvas.drawText(text, width / 2, height / 2 - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint); + + canvas.restoreToCount(count); + + } + + private void drawBorder(Canvas canvas) { + RectF rect = new RectF(getBounds()); + rect.inset(borderThickness/2, borderThickness/2); + + if (shape instanceof OvalShape) { + canvas.drawOval(rect, borderPaint); + } + else if (shape instanceof RoundRectShape) { + canvas.drawRoundRect(rect, radius, radius, borderPaint); + } + else { + canvas.drawRect(rect, borderPaint); + } + } + + @Override + public void setAlpha(int alpha) { + textPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + textPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public int getIntrinsicWidth() { + return width; + } + + @Override + public int getIntrinsicHeight() { + return height; + } + + public static IShapeBuilder builder() { + return new Builder(); + } + + public static class Builder implements IConfigBuilder, IShapeBuilder, IBuilder { + + private String text; + + private int color; + + private int borderThickness; + + private int width; + + private int height; + + private Typeface font; + + private RectShape shape; + + public int textColor; + + private int fontSize; + + private boolean isBold; + + private boolean toUpperCase; + + public float radius; + + private Builder() { + text = ""; + color = Color.GRAY; + textColor = Color.WHITE; + borderThickness = 0; + width = -1; + height = -1; + shape = new RectShape(); + font = Typeface.create("sans-serif-light", Typeface.NORMAL); + fontSize = -1; + isBold = false; + toUpperCase = false; + } + + public IConfigBuilder width(int width) { + this.width = width; + return this; + } + + public IConfigBuilder height(int height) { + this.height = height; + return this; + } + + public IConfigBuilder textColor(int color) { + this.textColor = color; + return this; + } + + public IConfigBuilder withBorder(int thickness) { + this.borderThickness = thickness; + return this; + } + + public IConfigBuilder useFont(Typeface font) { + this.font = font; + return this; + } + + public IConfigBuilder fontSize(int size) { + this.fontSize = size; + return this; + } + + public IConfigBuilder bold() { + this.isBold = true; + return this; + } + + public IConfigBuilder toUpperCase() { + this.toUpperCase = true; + return this; + } + + @Override + public IConfigBuilder beginConfig() { + return this; + } + + @Override + public IShapeBuilder endConfig() { + return this; + } + + @Override + public IBuilder rect() { + this.shape = new RectShape(); + return this; + } + + @Override + public IBuilder round() { + this.shape = new OvalShape(); + return this; + } + + @Override + public IBuilder roundRect(int radius) { + this.radius = radius; + float[] radii = {radius, radius, radius, radius, radius, radius, radius, radius}; + this.shape = new RoundRectShape(radii, null, null); + return this; + } + + @Override + public TextDrawable buildRect(String text, int color) { + rect(); + return build(text, color); + } + + @Override + public TextDrawable buildRoundRect(String text, int color, int radius) { + roundRect(radius); + return build(text, color); + } + + @Override + public TextDrawable buildRound(String text, int color) { + round(); + return build(text, color); + } + + @Override + public TextDrawable build(String text, int color) { + this.color = color; + this.text = text; + return new TextDrawable(this); + } + } + + public interface IConfigBuilder { + public IConfigBuilder width(int width); + + public IConfigBuilder height(int height); + + public IConfigBuilder textColor(int color); + + public IConfigBuilder withBorder(int thickness); + + public IConfigBuilder useFont(Typeface font); + + public IConfigBuilder fontSize(int size); + + public IConfigBuilder bold(); + + public IConfigBuilder toUpperCase(); + + public IShapeBuilder endConfig(); + } + + public static interface IBuilder { + + public TextDrawable build(String text, int color); + } + + public static interface IShapeBuilder { + + public IConfigBuilder beginConfig(); + + public IBuilder rect(); + + public IBuilder round(); + + public IBuilder roundRect(int radius); + + public TextDrawable buildRect(String text, int color); + + public TextDrawable buildRoundRect(String text, int color, int radius); + + public TextDrawable buildRound(String text, int color); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index ea20f12175..a704d0d81a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,6 +11,7 @@ include ':library:multipicker' include ':library:external:jsonviewer' include ':library:external:diff-match-patch' include ':library:external:dialpad' +include ':library:external:textdrawable' include ':library:rustCrypto' include ':matrix-sdk-android' diff --git a/vector/build.gradle b/vector/build.gradle index dab8b8bdeb..e38f1ea5e5 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -116,6 +116,7 @@ dependencies { implementation project(":matrix-sdk-android-flow") implementation project(":library:external:jsonviewer") implementation project(":library:external:diff-match-patch") + implementation project(":library:external:textdrawable") implementation project(":library:ui-strings") implementation project(":library:ui-styles") implementation project(":library:core-utils") @@ -184,7 +185,6 @@ dependencies { api libs.androidx.preferenceKtx // UI - implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation libs.google.material api('me.gujun.android:span:1.7') { exclude group: 'com.android.support', module: 'support-annotations' From 3da1497d2743da7b31b307ad86ebaa24a8b2ad28 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 17:40:29 +0200 Subject: [PATCH 02/65] Import source from https://github.com/natario1/Autocomplete --- dependencies_groups.gradle | 2 +- library/external/autocomplete/build.gradle | 32 ++ .../autocomplete/Autocomplete.java | 434 +++++++++++++++ .../autocomplete/AutocompleteCallback.java | 29 + .../autocomplete/AutocompletePolicy.java | 64 +++ .../autocomplete/AutocompletePopup.java | 521 ++++++++++++++++++ .../autocomplete/AutocompletePresenter.java | 129 +++++ .../autocomplete/CharPolicy.java | 184 +++++++ .../autocomplete/RecyclerViewPresenter.java | 152 +++++ settings.gradle | 1 + vector/build.gradle | 3 +- .../autocomplete/RecyclerViewPresenter.kt | 2 +- .../command/CommandAutocompletePolicy.kt | 9 +- 13 files changed, 1553 insertions(+), 9 deletions(-) create mode 100644 library/external/autocomplete/build.gradle create mode 100644 library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/Autocomplete.java create mode 100644 library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompleteCallback.java create mode 100644 library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePolicy.java create mode 100644 library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePopup.java create mode 100644 library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePresenter.java create mode 100644 library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/CharPolicy.java create mode 100644 library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/RecyclerViewPresenter.java diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 07dfa8787f..4fe36dcf0d 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -115,6 +115,7 @@ ext.groups = [ 'com.linkedin.dexmaker', 'com.mapbox.mapboxsdk', 'com.nulab-inc', + 'com.otaliastudios', 'com.otaliastudios.opengl', 'com.parse.bolts', 'com.pinterest', @@ -238,7 +239,6 @@ ext.groups = [ regex: [ ], group: [ - 'com.otaliastudios', 'com.yqritc', // https://github.com/cmelchior/realmfieldnameshelper/issues/42 'dk.ilios', diff --git a/library/external/autocomplete/build.gradle b/library/external/autocomplete/build.gradle new file mode 100644 index 0000000000..39d4d4b19e --- /dev/null +++ b/library/external/autocomplete/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + namespace "com.otaliastudios.autocomplete" + + compileSdk versions.compileSdk + + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + } + + compileOptions { + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation libs.androidx.recyclerview +} + +afterEvaluate { + tasks.findAll { it.name.startsWith("lint") }.each { + it.enabled = false + } +} diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/Autocomplete.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/Autocomplete.java new file mode 100644 index 0000000000..cf58146f14 --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/Autocomplete.java @@ -0,0 +1,434 @@ +package com.otaliastudios.autocomplete; + +import android.database.DataSetObserver; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import android.text.Editable; +import android.text.Selection; +import android.text.SpanWatcher; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextWatcher; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.Window; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.PopupWindow; + +import androidx.annotation.NonNull; + + +/** + * Entry point for adding Autocomplete behavior to a {@link EditText}. + * + * You can construct a {@code Autocomplete} using the builder provided by {@link Autocomplete#on(EditText)}. + * Building is enough, but you can hold a reference to this class to call its public methods. + * + * Requires: + * - {@link EditText}: this is both the anchor for the popup, and the source of text events that we listen to + * - {@link AutocompletePresenter}: this presents items in the popup window. See class for more info. + * - {@link AutocompleteCallback}: if specified, this listens to click events and visibility changes + * - {@link AutocompletePolicy}: if specified, this controls how and when to show the popup based on text events + * If not, this defaults to {@link SimplePolicy}: shows the popup when text.length() bigger than 0. + */ +public final class Autocomplete implements TextWatcher, SpanWatcher { + + private final static String TAG = Autocomplete.class.getSimpleName(); + private final static boolean DEBUG = false; + + private static void log(String log) { + if (DEBUG) Log.e(TAG, log); + } + + /** + * Builder for building {@link Autocomplete}. + * The only mandatory item is a presenter, {@link #with(AutocompletePresenter)}. + * + * @param the data model + */ + public final static class Builder { + private EditText source; + private AutocompletePresenter presenter; + private AutocompletePolicy policy; + private AutocompleteCallback callback; + private Drawable backgroundDrawable; + private float elevationDp = 6; + + private Builder(EditText source) { + this.source = source; + } + + /** + * Registers the {@link AutocompletePresenter} to be used, responsible for showing + * items. See the class for info. + * + * @param presenter desired presenter + * @return this for chaining + */ + public Builder with(AutocompletePresenter presenter) { + this.presenter = presenter; + return this; + } + + /** + * Registers the {@link AutocompleteCallback} to be used, responsible for listening to + * clicks provided by the presenter, and visibility changes. + * + * @param callback desired callback + * @return this for chaining + */ + public Builder with(AutocompleteCallback callback) { + this.callback = callback; + return this; + } + + /** + * Registers the {@link AutocompletePolicy} to be used, responsible for showing / dismissing + * the popup when certain events happen (e.g. certain characters are typed). + * + * @param policy desired policy + * @return this for chaining + */ + public Builder with(AutocompletePolicy policy) { + this.policy = policy; + return this; + } + + /** + * Sets a background drawable for the popup. + * + * @param backgroundDrawable drawable + * @return this for chaining + */ + public Builder with(Drawable backgroundDrawable) { + this.backgroundDrawable = backgroundDrawable; + return this; + } + + /** + * Sets elevation for the popup. Defaults to 6 dp. + * + * @param elevationDp popup elevation, in DP + * @return this for chaning. + */ + public Builder with(float elevationDp) { + this.elevationDp = elevationDp; + return this; + } + + /** + * Builds an Autocomplete instance. This is enough for autocomplete to be set up, + * but you can hold a reference to the object and call its public methods. + * + * @return an Autocomplete instance, if you need it + * + * @throws RuntimeException if either EditText or the presenter are null + */ + public Autocomplete build() { + if (source == null) throw new RuntimeException("Autocomplete needs a source!"); + if (presenter == null) throw new RuntimeException("Autocomplete needs a presenter!"); + if (policy == null) policy = new SimplePolicy(); + return new Autocomplete(this); + } + + private void clear() { + source = null; + presenter = null; + callback = null; + policy = null; + backgroundDrawable = null; + elevationDp = 6; + } + } + + /** + * Entry point for building autocomplete on a certain {@link EditText}. + * @param anchor the anchor for the popup, and the source of text events + * @param your data model + * @return a Builder for set up + */ + public static Builder on(EditText anchor) { + return new Builder(anchor); + } + + private AutocompletePolicy policy; + private AutocompletePopup popup; + private AutocompletePresenter presenter; + private AutocompleteCallback callback; + private EditText source; + + private boolean block; + private boolean disabled; + private boolean openBefore; + private String lastQuery = "null"; + + private Autocomplete(Builder builder) { + policy = builder.policy; + presenter = builder.presenter; + callback = builder.callback; + source = builder.source; + + // Set up popup + popup = new AutocompletePopup(source.getContext()); + popup.setAnchorView(source); + popup.setGravity(Gravity.START); + popup.setModal(false); + popup.setBackgroundDrawable(builder.backgroundDrawable); + popup.setElevation(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, builder.elevationDp, + source.getContext().getResources().getDisplayMetrics())); + + // popup dimensions + AutocompletePresenter.PopupDimensions dim = this.presenter.getPopupDimensions(); + popup.setWidth(dim.width); + popup.setHeight(dim.height); + popup.setMaxWidth(dim.maxWidth); + popup.setMaxHeight(dim.maxHeight); + + // Fire visibility events + popup.setOnDismissListener(new PopupWindow.OnDismissListener() { + @Override + public void onDismiss() { + lastQuery = "null"; + if (callback != null) callback.onPopupVisibilityChanged(false); + boolean saved = block; + block = true; + policy.onDismiss(source.getText()); + block = saved; + presenter.hideView(); + } + }); + + // Set up source + source.getText().setSpan(this, 0, source.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); + source.addTextChangedListener(this); + + // Set up presenter + presenter.registerClickProvider(new AutocompletePresenter.ClickProvider() { + @Override + public void click(@NonNull T item) { + AutocompleteCallback callback = Autocomplete.this.callback; + EditText edit = Autocomplete.this.source; + if (callback == null) return; + boolean saved = block; + block = true; + boolean dismiss = callback.onPopupItemClicked(edit.getText(), item); + if (dismiss) dismissPopup(); + block = saved; + } + }); + + builder.clear(); + } + + /** + * Controls how the popup operates with an input method. + * + * If the popup is showing, calling this method will take effect only + * the next time the popup is shown. + * + * @param mode a {@link PopupWindow} input method mode + */ + public void setInputMethodMode(int mode) { + popup.setInputMethodMode(mode); + } + + /** + * Sets the operating mode for the soft input area. + * + * @param mode The desired mode, see {@link WindowManager.LayoutParams#softInputMode} + */ + public void setSoftInputMode(int mode) { + popup.setSoftInputMode(mode); + } + + /** + * Shows the popup with the given query. + * There is rarely need to call this externally: it is already triggered by events on the anchor. + * To control when this is called, provide a good implementation of {@link AutocompletePolicy}. + * + * @param query query text. + */ + public void showPopup(@NonNull CharSequence query) { + if (isPopupShowing() && lastQuery.equals(query.toString())) return; + lastQuery = query.toString(); + + log("showPopup: called with filter "+query); + if (!isPopupShowing()) { + log("showPopup: showing"); + presenter.registerDataSetObserver(new Observer()); // Calling new to avoid leaking... maybe... + popup.setView(presenter.getView()); + presenter.showView(); + popup.show(); + if (callback != null) callback.onPopupVisibilityChanged(true); + } + log("showPopup: popup should be showing... "+isPopupShowing()); + presenter.onQuery(query); + } + + /** + * Dismisses the popup, if showing. + * There is rarely need to call this externally: it is already triggered by events on the anchor. + * To control when this is called, provide a good implementation of {@link AutocompletePolicy}. + */ + public void dismissPopup() { + if (isPopupShowing()) { + popup.dismiss(); + } + } + + /** + * Returns true if the popup is showing. + * @return whether the popup is currently showing + */ + public boolean isPopupShowing() { + return this.popup.isShowing(); + } + + /** + * Switch to control the autocomplete behavior. When disabled, no popup is shown. + * This is useful if you want to do runtime edits to the anchor text, without triggering + * the popup. + * + * @param enabled whether to enable autocompletion + */ + public void setEnabled(boolean enabled) { + disabled = !enabled; + } + + /** + * Sets the gravity for the popup. Basically only {@link Gravity#START} and {@link Gravity#END} + * do work. + * + * @param gravity gravity for the popup + */ + public void setGravity(int gravity) { + popup.setGravity(gravity); + } + + /** + * Controls the vertical offset of the popup from the EditText anchor. + * + * @param offset offset in pixels. + */ + public void setOffsetFromAnchor(int offset) { popup.setVerticalOffset(offset); } + + /** + * Controls whether the popup should listen to clicks outside its boundaries. + * + * @param outsideTouchable true to listen to outside clicks + */ + public void setOutsideTouchable(boolean outsideTouchable) { popup.setOutsideTouchable(outsideTouchable); } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + if (block || disabled) return; + openBefore = isPopupShowing(); + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (block || disabled) return; + if (openBefore && !isPopupShowing()) { + return; // Copied from somewhere. + } + if (!(s instanceof Spannable)) { + source.setText(new SpannableString(s)); + return; + } + Spannable sp = (Spannable) s; + + int cursor = source.getSelectionEnd(); + log("onTextChanged: cursor end position is "+cursor); + if (cursor == -1) { // No cursor present. + dismissPopup(); return; + } + if (cursor != source.getSelectionStart()) { + // Not sure about this. We should have no problems dealing with multi selections, + // we just take the end... + // dismissPopup(); return; + } + + boolean b = block; + block = true; // policy might add spans or other stuff. + if (isPopupShowing() && policy.shouldDismissPopup(sp, cursor)) { + log("onTextChanged: dismissing"); + dismissPopup(); + } else if (isPopupShowing() || policy.shouldShowPopup(sp, cursor)) { + // LOG.now("onTextChanged: updating with filter "+policy.getQuery(sp)); + showPopup(policy.getQuery(sp)); + } + block = b; + } + + @Override + public void afterTextChanged(Editable s) {} + + @Override + public void onSpanAdded(Spannable text, Object what, int start, int end) {} + + @Override + public void onSpanRemoved(Spannable text, Object what, int start, int end) {} + + @Override + public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart, int nend) { + if (disabled || block) return; + if (what == Selection.SELECTION_END) { + // Selection end changed from ostart to nstart. Trigger a check. + log("onSpanChanged: selection end moved from "+ostart+" to "+nstart); + log("onSpanChanged: block is "+block); + boolean b = block; + block = true; + if (!isPopupShowing() && policy.shouldShowPopup(text, nstart)) { + showPopup(policy.getQuery(text)); + } + block = b; + } + } + + private class Observer extends DataSetObserver implements Runnable { + private Handler ui = new Handler(Looper.getMainLooper()); + + @Override + public void onChanged() { + // ??? Not sure this is needed... + ui.post(this); + } + + @Override + public void run() { + if (isPopupShowing()) { + // Call show again to revisit width and height. + popup.show(); + } + } + } + + /** + * A very simple {@link AutocompletePolicy} implementation. + * Popup is shown when text length is bigger than 0, and hidden when text is empty. + * The query string is the whole text. + */ + public static class SimplePolicy implements AutocompletePolicy { + @Override + public boolean shouldShowPopup(@NonNull Spannable text, int cursorPos) { + return text.length() > 0; + } + + @Override + public boolean shouldDismissPopup(@NonNull Spannable text, int cursorPos) { + return text.length() == 0; + } + + @NonNull + @Override + public CharSequence getQuery(@NonNull Spannable text) { + return text; + } + + @Override + public void onDismiss(@NonNull Spannable text) {} + } +} diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompleteCallback.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompleteCallback.java new file mode 100644 index 0000000000..56870db610 --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompleteCallback.java @@ -0,0 +1,29 @@ +package com.otaliastudios.autocomplete; + +import android.text.Editable; + +import androidx.annotation.NonNull; + +/** + * Optional callback to be passed to {@link Autocomplete.Builder}. + */ +public interface AutocompleteCallback { + + /** + * Called when an item inside your list is clicked. + * This works if your presenter has dispatched a click event. + * At this point you can edit the text, e.g. {@code editable.append(item.toString())}. + * + * @param editable editable text that you can work on + * @param item item that was clicked + * @return true if the action is valid and the popup can be dismissed + */ + boolean onPopupItemClicked(@NonNull Editable editable, @NonNull T item); + + /** + * Called when popup visibility state changes. + * + * @param shown true if the popup was just shown, false if it was just hidden + */ + void onPopupVisibilityChanged(boolean shown); +} diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePolicy.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePolicy.java new file mode 100644 index 0000000000..05dfaa9f0a --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePolicy.java @@ -0,0 +1,64 @@ +package com.otaliastudios.autocomplete; + +import android.text.Spannable; + +import androidx.annotation.NonNull; + +/** + * This interface controls when to show or hide the popup window, and, in the first case, + * what text should be passed to the popup {@link AutocompletePresenter}. + * + * @see Autocomplete.SimplePolicy for the simplest possible implementation + */ +public interface AutocompletePolicy { + + /** + * Called to understand whether the popup should be shown. Some naive examples: + * - Show when there's text: {@code return text.length() > 0} + * - Show when last char is @: {@code return text.getCharAt(text.length()-1) == '@'} + * + * @param text current text, along with its Spans + * @param cursorPos the position of the cursor + * @return true if popup should be shown + */ + boolean shouldShowPopup(@NonNull Spannable text, int cursorPos); + + /** + * Called to understand whether a currently shown popup should be closed, maybe + * because text is invalid. A reasonable implementation is + * {@code return !shouldShowPopup(text, cursorPos)}. + * + * However this is defined so you can add or clear spans. + * + * @param text current text, along with its Spans + * @param cursorPos the position of the cursor + * @return true if popup should be hidden + */ + boolean shouldDismissPopup(@NonNull Spannable text, int cursorPos); + + /** + * Called to understand which query should be passed to {@link AutocompletePresenter} + * for a showing popup. If this is called, {@link #shouldShowPopup(Spannable, int)} just returned + * true, or {@link #shouldDismissPopup(Spannable, int)} just returned false. + * + * This is useful to understand which part of the text should be passed to presenters. + * For example, user might have typed '@john' to select a username, but you just want to + * search for 'john'. + * + * For more complex cases, you can add inclusive Spans in {@link #shouldShowPopup(Spannable, int)}, + * and get the span position here. + * + * @param text current text, along with its Spans + * @return the query for presenter + */ + @NonNull + CharSequence getQuery(@NonNull Spannable text); + + /** + * Called when popup is dismissed. This can be used, for instance, to clear custom Spans + * from the text. + * + * @param text text at the moment of dismissing + */ + void onDismiss(@NonNull Spannable text); +} diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePopup.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePopup.java new file mode 100644 index 0000000000..6467085068 --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePopup.java @@ -0,0 +1,521 @@ +package com.otaliastudios.autocomplete; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.PopupWindow; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.core.view.ViewCompat; +import androidx.core.widget.PopupWindowCompat; + +/** + * A simplified version of andriod.widget.ListPopupWindow, which is the class used by + * AutocompleteTextView. + * + * Other than being simplified, this deals with Views rather than ListViews, so the content + * can be whatever. Lots of logic (clicks, selections etc.) has been removed because we manage that + * in {@link AutocompletePresenter}. + * + */ +class AutocompletePopup { + private Context mContext; + private ViewGroup mView; + private int mHeight = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mWidth = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mMaxHeight = Integer.MAX_VALUE; + private int mMaxWidth = Integer.MAX_VALUE; + private int mUserMaxHeight = Integer.MAX_VALUE; + private int mUserMaxWidth = Integer.MAX_VALUE; + private int mHorizontalOffset = 0; + private int mVerticalOffset = 0; + private boolean mVerticalOffsetSet; + private int mGravity = Gravity.NO_GRAVITY; + private boolean mAlwaysVisible = false; + private boolean mOutsideTouchable = true; + private View mAnchorView; + private final Rect mTempRect = new Rect(); + private boolean mModal; + private PopupWindow mPopup; + + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + */ + AutocompletePopup(@NonNull Context context) { + super(); + mContext = context; + mPopup = new PopupWindow(context); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + } + + + /** + * Set whether this window should be modal when shown. + * + *

If a popup window is modal, it will receive all touch and key input. + * If the user touches outside the popup window's content area the popup window + * will be dismissed. + * @param modal {@code true} if the popup window should be modal, {@code false} otherwise. + */ + @SuppressWarnings("SameParameterValue") + void setModal(boolean modal) { + mModal = modal; + mPopup.setFocusable(modal); + } + + /** + * Returns whether the popup window will be modal when shown. + * @return {@code true} if the popup window will be modal, {@code false} otherwise. + */ + @SuppressWarnings("unused") + boolean isModal() { + return mModal; + } + + void setElevation(float elevationPx) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) mPopup.setElevation(elevationPx); + } + + /** + * Sets whether the drop-down should remain visible under certain conditions. + * + * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless + * of the size or content of the list. {@link #getBackground()} will fill any space + * that is not used by the list. + * @param dropDownAlwaysVisible Whether to keep the drop-down visible. + * + */ + @SuppressWarnings("unused") + void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { + mAlwaysVisible = dropDownAlwaysVisible; + } + + /** + * @return Whether the drop-down is visible under special conditions. + */ + @SuppressWarnings("unused") + boolean isDropDownAlwaysVisible() { + return mAlwaysVisible; + } + + void setOutsideTouchable(boolean outsideTouchable) { + mOutsideTouchable = outsideTouchable; + } + + @SuppressWarnings("WeakerAccess") + boolean isOutsideTouchable() { + return mOutsideTouchable && !mAlwaysVisible; + } + + /** + * Sets the operating mode for the soft input area. + * @param mode The desired mode, see + * {@link android.view.WindowManager.LayoutParams#softInputMode} + * for the full list + * @see android.view.WindowManager.LayoutParams#softInputMode + * @see #getSoftInputMode() + */ + void setSoftInputMode(int mode) { + mPopup.setSoftInputMode(mode); + } + + /** + * Returns the current value in {@link #setSoftInputMode(int)}. + * @see #setSoftInputMode(int) + * @see android.view.WindowManager.LayoutParams#softInputMode + */ + @SuppressWarnings({"WeakerAccess", "unused"}) + int getSoftInputMode() { + return mPopup.getSoftInputMode(); + } + + /** + * @return The background drawable for the popup window. + */ + @SuppressWarnings({"WeakerAccess", "unused"}) + @Nullable + Drawable getBackground() { + return mPopup.getBackground(); + } + + /** + * Sets a drawable to be the background for the popup window. + * @param d A drawable to set as the background. + */ + void setBackgroundDrawable(@Nullable Drawable d) { + mPopup.setBackgroundDrawable(d); + } + + /** + * Set an animation style to use when the popup window is shown or dismissed. + * @param animationStyle Animation style to use. + */ + @SuppressWarnings("unused") + void setAnimationStyle(@StyleRes int animationStyle) { + mPopup.setAnimationStyle(animationStyle); + } + + /** + * Returns the animation style that will be used when the popup window is + * shown or dismissed. + * @return Animation style that will be used. + */ + @SuppressWarnings("unused") + @StyleRes + int getAnimationStyle() { + return mPopup.getAnimationStyle(); + } + + /** + * Returns the view that will be used to anchor this popup. + * @return The popup's anchor view + */ + @SuppressWarnings("WeakerAccess") + View getAnchorView() { + return mAnchorView; + } + + /** + * Sets the popup's anchor view. This popup will always be positioned relative to + * the anchor view when shown. + * @param anchor The view to use as an anchor. + */ + void setAnchorView(@NonNull View anchor) { + mAnchorView = anchor; + } + + /** + * Set the horizontal offset of this popup from its anchor view in pixels. + * @param offset The horizontal offset of the popup from its anchor. + */ + @SuppressWarnings("unused") + void setHorizontalOffset(int offset) { + mHorizontalOffset = offset; + } + + /** + * Set the vertical offset of this popup from its anchor view in pixels. + * @param offset The vertical offset of the popup from its anchor. + */ + void setVerticalOffset(int offset) { + mVerticalOffset = offset; + mVerticalOffsetSet = true; + } + + /** + * Set the gravity of the dropdown list. This is commonly used to + * set gravity to START or END for alignment with the anchor. + * @param gravity Gravity value to use + */ + void setGravity(int gravity) { + mGravity = gravity; + } + + /** + * @return The width of the popup window in pixels. + */ + @SuppressWarnings("unused") + int getWidth() { + return mWidth; + } + + /** + * Sets the width of the popup window in pixels. Can also be MATCH_PARENT + * or WRAP_CONTENT. + * @param width Width of the popup window. + */ + void setWidth(int width) { + mWidth = width; + } + + /** + * Sets the width of the popup window by the size of its content. The final width may be + * larger to accommodate styled window dressing. + * @param width Desired width of content in pixels. + */ + @SuppressWarnings("unused") + void setContentWidth(int width) { + Drawable popupBackground = mPopup.getBackground(); + if (popupBackground != null) { + popupBackground.getPadding(mTempRect); + width += mTempRect.left + mTempRect.right; + } + setWidth(width); + } + + void setMaxWidth(int width) { + if (width > 0) { + mUserMaxWidth = width; + } + } + + /** + * @return The height of the popup window in pixels. + */ + @SuppressWarnings("unused") + int getHeight() { + return mHeight; + } + + /** + * Sets the height of the popup window in pixels. Can also be MATCH_PARENT. + * @param height Height of the popup window. + */ + void setHeight(int height) { + mHeight = height; + } + + /** + * Sets the height of the popup window by the size of its content. The final height may be + * larger to accommodate styled window dressing. + * @param height Desired height of content in pixels. + */ + @SuppressWarnings("unused") + void setContentHeight(int height) { + Drawable popupBackground = mPopup.getBackground(); + if (popupBackground != null) { + popupBackground.getPadding(mTempRect); + height += mTempRect.top + mTempRect.bottom; + } + setHeight(height); + } + + void setMaxHeight(int height) { + if (height > 0) { + mUserMaxHeight = height; + } + } + + void setOnDismissListener(PopupWindow.OnDismissListener listener) { + mPopup.setOnDismissListener(listener); + } + + /** + * Show the popup list. If the list is already showing, this method + * will recalculate the popup's size and position. + */ + void show() { + if (!ViewCompat.isAttachedToWindow(getAnchorView())) return; + + int height = buildDropDown(); + final boolean noInputMethod = isInputMethodNotNeeded(); + int mDropDownWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; + PopupWindowCompat.setWindowLayoutType(mPopup, mDropDownWindowLayoutType); + + if (mPopup.isShowing()) { + // First pass for this special case, don't know why. + if (mHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + int tempWidth = mWidth == ViewGroup.LayoutParams.MATCH_PARENT ? ViewGroup.LayoutParams.MATCH_PARENT : 0; + if (noInputMethod) { + mPopup.setWidth(tempWidth); + mPopup.setHeight(0); + } else { + mPopup.setWidth(tempWidth); + mPopup.setHeight(ViewGroup.LayoutParams.MATCH_PARENT); + } + } + + // The call to PopupWindow's update method below can accept -1 + // for any value you do not want to update. + + // Width. + int widthSpec; + if (mWidth == ViewGroup.LayoutParams.MATCH_PARENT) { + widthSpec = -1; + } else if (mWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + widthSpec = getAnchorView().getWidth(); + } else { + widthSpec = mWidth; + } + widthSpec = Math.min(widthSpec, mMaxWidth); + widthSpec = (widthSpec < 0) ? - 1 : widthSpec; + + // Height. + int heightSpec; + if (mHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; + } else if (mHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + heightSpec = height; + } else { + heightSpec = mHeight; + } + heightSpec = Math.min(heightSpec, mMaxHeight); + heightSpec = (heightSpec < 0) ? - 1 : heightSpec; + + // Update. + mPopup.setOutsideTouchable(isOutsideTouchable()); + if (heightSpec == 0) { + dismiss(); + } else { + mPopup.update(getAnchorView(), mHorizontalOffset, mVerticalOffset, widthSpec, heightSpec); + } + + } else { + int widthSpec; + if (mWidth == ViewGroup.LayoutParams.MATCH_PARENT) { + widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; + } else if (mWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + widthSpec = getAnchorView().getWidth(); + } else { + widthSpec = mWidth; + } + widthSpec = Math.min(widthSpec, mMaxWidth); + + int heightSpec; + if (mHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; + } else if (mHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + heightSpec = height; + } else { + heightSpec = mHeight; + } + heightSpec = Math.min(heightSpec, mMaxHeight); + + // Set width and height. + mPopup.setWidth(widthSpec); + mPopup.setHeight(heightSpec); + mPopup.setClippingEnabled(true); + + // use outside touchable to dismiss drop down when touching outside of it, so + // only set this if the dropdown is not always visible + mPopup.setOutsideTouchable(isOutsideTouchable()); + PopupWindowCompat.showAsDropDown(mPopup, getAnchorView(), mHorizontalOffset, mVerticalOffset, mGravity); + } + } + + /** + * Dismiss the popup window. + */ + void dismiss() { + mPopup.dismiss(); + mPopup.setContentView(null); + mView = null; + } + + /** + * Control how the popup operates with an input method: one of + * INPUT_METHOD_FROM_FOCUSABLE, INPUT_METHOD_NEEDED, + * or INPUT_METHOD_NOT_NEEDED. + * + *

If the popup is showing, calling this method will take effect only + * the next time the popup is shown or through a manual call to the {@link #show()} + * method.

+ * + * @see #show() + */ + void setInputMethodMode(int mode) { + mPopup.setInputMethodMode(mode); + } + + + /** + * @return {@code true} if the popup is currently showing, {@code false} otherwise. + */ + boolean isShowing() { + return mPopup.isShowing(); + } + + /** + * @return {@code true} if this popup is configured to assume the user does not need + * to interact with the IME while it is showing, {@code false} otherwise. + */ + @SuppressWarnings("WeakerAccess") + boolean isInputMethodNotNeeded() { + return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; + } + + + void setView(ViewGroup view) { + mView = view; + mView.setFocusable(true); + mView.setFocusableInTouchMode(true); + ViewGroup dropDownView = mView; + mPopup.setContentView(dropDownView); + ViewGroup.LayoutParams params = mView.getLayoutParams(); + if (params != null) { + if (params.height > 0) setHeight(params.height); + if (params.width > 0) setWidth(params.width); + } + } + + /** + *

Builds the popup window's content and returns the height the popup + * should have. Returns -1 when the content already exists.

+ * + * @return the content's wrap content height or -1 if content already exists + */ + private int buildDropDown() { + int otherHeights = 0; + + // getMaxAvailableHeight() subtracts the padding, so we put it back + // to get the available height for the whole window. + final int paddingVert; + final int paddingHoriz; + final Drawable background = mPopup.getBackground(); + if (background != null) { + background.getPadding(mTempRect); + paddingVert = mTempRect.top + mTempRect.bottom; + paddingHoriz = mTempRect.left + mTempRect.right; + + // If we don't have an explicit vertical offset, determine one from + // the window background so that content will line up. + if (!mVerticalOffsetSet) { + mVerticalOffset = -mTempRect.top; + } + } else { + mTempRect.setEmpty(); + paddingVert = 0; + paddingHoriz = 0; + } + + // Redefine dimensions taking into account maxWidth and maxHeight. + final boolean ignoreBottomDecorations = mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; + final int maxContentHeight = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? + mPopup.getMaxAvailableHeight(getAnchorView(), mVerticalOffset, ignoreBottomDecorations) : + mPopup.getMaxAvailableHeight(getAnchorView(), mVerticalOffset); + final int maxContentWidth = mContext.getResources().getDisplayMetrics().widthPixels - paddingHoriz; + + mMaxHeight = Math.min(maxContentHeight + paddingVert, mUserMaxHeight); + mMaxWidth = Math.min(maxContentWidth + paddingHoriz, mUserMaxWidth); + // if (mHeight > 0) mHeight = Math.min(mHeight, maxContentHeight); + // if (mWidth > 0) mWidth = Math.min(mWidth, maxContentWidth); + + if (mAlwaysVisible || mHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + return mMaxHeight; + } + + final int childWidthSpec; + switch (mWidth) { + case ViewGroup.LayoutParams.WRAP_CONTENT: + childWidthSpec = View.MeasureSpec.makeMeasureSpec(maxContentWidth, View.MeasureSpec.AT_MOST); break; + case ViewGroup.LayoutParams.MATCH_PARENT: + childWidthSpec = View.MeasureSpec.makeMeasureSpec(maxContentWidth, View.MeasureSpec.EXACTLY); break; + default: + //noinspection Range + childWidthSpec = View.MeasureSpec.makeMeasureSpec(mWidth, View.MeasureSpec.EXACTLY); break; + } + + // Add padding only if the list has items in it, that way we don't show + // the popup if it is not needed. For this reason, we measure as wrap_content. + mView.measure(childWidthSpec, View.MeasureSpec.makeMeasureSpec(maxContentHeight, View.MeasureSpec.AT_MOST)); + final int viewHeight = mView.getMeasuredHeight(); + if (viewHeight > 0) { + otherHeights += paddingVert + mView.getPaddingTop() + mView.getPaddingBottom(); + } + + return Math.min(viewHeight + otherHeights, mMaxHeight); + } + + +} \ No newline at end of file diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePresenter.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePresenter.java new file mode 100644 index 0000000000..d49e8c8f30 --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePresenter.java @@ -0,0 +1,129 @@ +package com.otaliastudios.autocomplete; + +import android.content.Context; +import android.database.DataSetObserver; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Base class for presenting items inside a popup. This is abstract and must be implemented. + * + * Most important methods are {@link #getView()} and {@link #onQuery(CharSequence)}. + */ +public abstract class AutocompletePresenter { + + private Context context; + private boolean isShowing; + + @SuppressWarnings("WeakerAccess") + public AutocompletePresenter(@NonNull Context context) { + this.context = context; + } + + /** + * At this point the presenter is passed the {@link ClickProvider}. + * The contract is that {@link ClickProvider#click(Object)} must be called when a list item + * is clicked. This ensure that the autocomplete callback will receive the event. + * + * @param provider a click provider for this presenter. + */ + protected void registerClickProvider(ClickProvider provider) { + + } + + /** + * Useful if you wish to change width/height based on content height. + * The contract is to call {@link DataSetObserver#onChanged()} when your view has + * changes. + * + * This is called after {@link #getView()}. + * + * @param observer the observer. + */ + protected void registerDataSetObserver(@NonNull DataSetObserver observer) {} + + /** + * Called each time the popup is shown. You are meant to inflate the view here. + * You can get a LayoutInflater using {@link #getContext()}. + * + * @return a ViewGroup for the popup + */ + @NonNull + protected abstract ViewGroup getView(); + + /** + * Provide the {@link PopupDimensions} for this popup. Called just once. + * You can use fixed dimensions or {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and + * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}. + * + * @return a PopupDimensions object + */ + // Called at first to understand which dimensions to use for the popup. + @NonNull + protected PopupDimensions getPopupDimensions() { + return new PopupDimensions(); + } + + /** + * Perform firther initialization here. Called after {@link #getView()}, + * each time the popup is shown. + */ + protected abstract void onViewShown(); + + /** + * Called to update the view to filter results with the query. + * It is called any time the popup is shown, and any time the text changes and query is updated. + * + * @param query query from the edit text, to filter our results + */ + protected abstract void onQuery(@Nullable CharSequence query); + + /** + * Called when the popup is hidden, to release resources. + */ + protected abstract void onViewHidden(); + + /** + * @return this presenter context + */ + @NonNull + protected final Context getContext() { + return context; + } + + /** + * @return whether we are showing currently + */ + @SuppressWarnings("unused") + protected final boolean isShowing() { + return isShowing; + } + + final void showView() { + isShowing = true; + onViewShown(); + } + + final void hideView() { + isShowing = false; + onViewHidden(); + } + + public interface ClickProvider { + void click(@NonNull T item); + } + + /** + * Provides width, height, maxWidth and maxHeight for the popup. + * @see #getPopupDimensions() + */ + @SuppressWarnings("WeakerAccess") + public static class PopupDimensions { + public int width = ViewGroup.LayoutParams.WRAP_CONTENT; + public int height = ViewGroup.LayoutParams.WRAP_CONTENT; + public int maxWidth = Integer.MAX_VALUE; + public int maxHeight = Integer.MAX_VALUE; + } +} diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/CharPolicy.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/CharPolicy.java new file mode 100644 index 0000000000..9c94983317 --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/CharPolicy.java @@ -0,0 +1,184 @@ +package com.otaliastudios.autocomplete; + +import android.text.Spannable; +import android.text.Spanned; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + + +/** + * A special {@link AutocompletePolicy} for cases when you want to trigger the popup when a + * certain character is shown. + * + * For instance, this might be the case for hashtags ('#') or usernames ('@') or whatever you wish. + * Passing this to {@link Autocomplete.Builder} ensures the following behavior (assuming '@'): + * - text "@john" : presenter will be passed the query "john" + * - text "You should see this @j" : presenter will be passed the query "j" + * - text "You should see this @john @m" : presenter will be passed the query "m" + */ +public class CharPolicy implements AutocompletePolicy { + + private final static String TAG = CharPolicy.class.getSimpleName(); + private final static boolean DEBUG = false; + + private static void log(@NonNull String log) { + if (DEBUG) Log.e(TAG, log); + } + + private final char CH; + private final int[] INT = new int[2]; + private boolean needSpaceBefore = true; + + /** + * Constructs a char policy for the given character. + * + * @param trigger the triggering character. + */ + public CharPolicy(char trigger) { + CH = trigger; + } + + /** + * Constructs a char policy for the given character. + * You can choose whether a whitespace is needed before 'trigger'. + * + * @param trigger the triggering character. + * @param needSpaceBefore whether we need a space before trigger + */ + @SuppressWarnings("unused") + public CharPolicy(char trigger, boolean needSpaceBefore) { + CH = trigger; + this.needSpaceBefore = needSpaceBefore; + } + + /** + * Can be overriden to understand which characters are valid. The default implementation + * returns true for any character except whitespaces. + * + * @param ch the character + * @return whether it's valid part of a query + */ + @SuppressWarnings("WeakerAccess") + protected boolean isValidChar(char ch) { + return !Character.isWhitespace(ch); + } + + @Nullable + private int[] checkText(@NonNull Spannable text, int cursorPos) { + final int spanEnd = cursorPos; + char last = 'x'; + cursorPos -= 1; // If the cursor is at the end, we will have cursorPos = length. Go back by 1. + while (cursorPos >= 0 && last != CH) { + char ch = text.charAt(cursorPos); + log("checkText: char is "+ch); + if (isValidChar(ch)) { + // We are going back + log("checkText: char is valid"); + cursorPos -= 1; + last = ch; + } else { + // We got a whitespace before getting a CH. This is invalid. + log("checkText: char is not valid, returning NULL"); + return null; + } + } + cursorPos += 1; // + 1 because we end BEHIND the valid selection + + // Start checking. + if (cursorPos == 0 && last != CH) { + // We got to the start of the string, and no CH was encountered. Nothing to do. + log("checkText: got to start but no CH, returning NULL"); + return null; + } + + // Additional checks for cursorPos - 1 + if (cursorPos > 0 && needSpaceBefore) { + char ch = text.charAt(cursorPos-1); + if (!Character.isWhitespace(ch)) { + log("checkText: char before is not whitespace, returning NULL"); + return null; + } + } + + // All seems OK. + final int spanStart = cursorPos + 1; // + 1 because we want to exclude CH from the query + INT[0] = spanStart; + INT[1] = spanEnd; + log("checkText: found! cursorPos="+cursorPos); + log("checkText: found! spanStart="+spanStart); + log("checkText: found! spanEnd="+spanEnd); + return INT; + } + + @Override + public boolean shouldShowPopup(@NonNull Spannable text, int cursorPos) { + // Returning true if, right before cursorPos, we have a word starting with @. + log("shouldShowPopup: text is "+text); + log("shouldShowPopup: cursorPos is "+cursorPos); + int[] show = checkText(text, cursorPos); + if (show != null) { + text.setSpan(new QuerySpan(), show[0], show[1], Spanned.SPAN_INCLUSIVE_INCLUSIVE); + return true; + } + log("shouldShowPopup: returning false"); + return false; + } + + @Override + public boolean shouldDismissPopup(@NonNull Spannable text, int cursorPos) { + log("shouldDismissPopup: text is "+text); + log("shouldDismissPopup: cursorPos is "+cursorPos); + boolean dismiss = checkText(text, cursorPos) == null; + log("shouldDismissPopup: returning "+dismiss); + return dismiss; + } + + @NonNull + @Override + public CharSequence getQuery(@NonNull Spannable text) { + QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class); + if (span == null || span.length == 0) { + // Should never happen. + log("getQuery: there's no span!"); + return ""; + } + log("getQuery: found spans: "+span.length); + QuerySpan sp = span[0]; + log("getQuery: span start is "+text.getSpanStart(sp)); + log("getQuery: span end is "+text.getSpanEnd(sp)); + CharSequence seq = text.subSequence(text.getSpanStart(sp), text.getSpanEnd(sp)); + log("getQuery: returning "+seq); + return seq; + } + + + @Override + public void onDismiss(@NonNull Spannable text) { + // Remove any span added by shouldShow. Should be useless, but anyway. + QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class); + for (QuerySpan s : span) { + text.removeSpan(s); + } + } + + private static class QuerySpan {} + + /** + * Returns the current query out of the given Spannable. + * @param text the anchor text + * @return an int[] with query start and query end positions + */ + @Nullable + public static int[] getQueryRange(@NonNull Spannable text) { + QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class); + if (span == null || span.length == 0) return null; + if (span.length > 1) { + // Won't happen + log("getQueryRange: ERR: MORE THAN ONE QuerySpan."); + } + QuerySpan sp = span[0]; + return new int[]{text.getSpanStart(sp), text.getSpanEnd(sp)}; + } +} diff --git a/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/RecyclerViewPresenter.java b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/RecyclerViewPresenter.java new file mode 100644 index 0000000000..1338ec7cac --- /dev/null +++ b/library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/RecyclerViewPresenter.java @@ -0,0 +1,152 @@ +package com.otaliastudios.autocomplete; + +import android.content.Context; +import android.database.DataSetObserver; +import android.view.ViewGroup; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Simple {@link AutocompletePresenter} implementation that hosts a {@link RecyclerView}. + * Supports {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} natively. + * The only contract is to + * + * - provide a {@link RecyclerView.Adapter} in {@link #instantiateAdapter()} + * - call {@link #dispatchClick(Object)} when an object is clicked + * - update your data during {@link #onQuery(CharSequence)} + * + * @param your model object (the object displayed by the list) + */ +public abstract class RecyclerViewPresenter extends AutocompletePresenter { + + private RecyclerView recycler; + private ClickProvider clicks; + private Observer observer; + + public RecyclerViewPresenter(@NonNull Context context) { + super(context); + } + + @Override + protected final void registerClickProvider(@NonNull ClickProvider provider) { + this.clicks = provider; + } + + @Override + protected final void registerDataSetObserver(@NonNull DataSetObserver observer) { + this.observer = new Observer(observer); + } + + @NonNull + @Override + protected ViewGroup getView() { + recycler = new RecyclerView(getContext()); + RecyclerView.Adapter adapter = instantiateAdapter(); + recycler.setAdapter(adapter); + recycler.setLayoutManager(instantiateLayoutManager()); + if (observer != null) { + adapter.registerAdapterDataObserver(observer); + observer = null; + } + return recycler; + } + + @Override + protected void onViewShown() {} + + @CallSuper + @Override + protected void onViewHidden() { + recycler = null; + observer = null; + } + + @SuppressWarnings("unused") + @Nullable + protected final RecyclerView getRecyclerView() { + return recycler; + } + + /** + * Dispatch click event to {@link AutocompleteCallback}. + * Should be called when items are clicked. + * + * @param item the clicked item. + */ + protected final void dispatchClick(@NonNull T item) { + if (clicks != null) clicks.click(item); + } + + /** + * Request that the popup should recompute its dimensions based on a recent change in + * the view being displayed. + * + * This is already managed internally for {@link RecyclerView} events. + * Only use it for changes in other views that you have added to the popup, + * and only if one of the dimensions for the popup is WRAP_CONTENT . + */ + @SuppressWarnings("unused") + protected final void dispatchLayoutChange() { + if (observer != null) observer.onChanged(); + } + + /** + * Provide an adapter for the recycler. + * This should be a fresh instance every time this is called. + * + * @return a new adapter. + */ + @NonNull + protected abstract RecyclerView.Adapter instantiateAdapter(); + + /** + * Provides a layout manager for the recycler. + * This should be a fresh instance every time this is called. + * Defaults to a vertical LinearLayoutManager, which is guaranteed to work well. + * + * @return a new layout manager. + */ + @SuppressWarnings("WeakerAccess") + @NonNull + protected RecyclerView.LayoutManager instantiateLayoutManager() { + return new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false); + } + + private final static class Observer extends RecyclerView.AdapterDataObserver { + + private DataSetObserver root; + + Observer(@NonNull DataSetObserver root) { + this.root = root; + } + + @Override + public void onChanged() { + root.onChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + root.onChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + root.onChanged(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + root.onChanged(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + root.onChanged(); + } + } +} diff --git a/settings.gradle b/settings.gradle index a704d0d81a..b3ecd410d6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,6 +12,7 @@ include ':library:external:jsonviewer' include ':library:external:diff-match-patch' include ':library:external:dialpad' include ':library:external:textdrawable' +include ':library:external:autocomplete' include ':library:rustCrypto' include ':matrix-sdk-android' diff --git a/vector/build.gradle b/vector/build.gradle index e38f1ea5e5..83f09fb10b 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -117,6 +117,7 @@ dependencies { implementation project(":library:external:jsonviewer") implementation project(":library:external:diff-match-patch") implementation project(":library:external:textdrawable") + implementation project(":library:external:autocomplete") implementation project(":library:ui-strings") implementation project(":library:ui-styles") implementation project(":library:core-utils") @@ -210,8 +211,6 @@ dependencies { // Alerter implementation 'com.github.tapadoo:alerter:7.2.4' - implementation 'com.otaliastudios:autocomplete:1.1.0' - // Shake detection implementation 'com.squareup:seismic:1.0.3' diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/RecyclerViewPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/RecyclerViewPresenter.kt index 7625eb6216..2648f2f210 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/RecyclerViewPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/RecyclerViewPresenter.kt @@ -23,7 +23,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.otaliastudios.autocomplete.AutocompletePresenter -abstract class RecyclerViewPresenter(context: Context?) : AutocompletePresenter(context) { +abstract class RecyclerViewPresenter(context: Context) : AutocompletePresenter(context) { private var recyclerView: RecyclerView? = null private var clicks: ClickProvider? = null diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/command/CommandAutocompletePolicy.kt b/vector/src/main/java/im/vector/app/features/autocomplete/command/CommandAutocompletePolicy.kt index 08f61be0f8..9097d2be43 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/command/CommandAutocompletePolicy.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/command/CommandAutocompletePolicy.kt @@ -32,16 +32,15 @@ class CommandAutocompletePolicy @Inject constructor() : AutocompletePolicy { return "" } - override fun onDismiss(text: Spannable?) { + override fun onDismiss(text: Spannable) { } // Only if text which starts with '/' and without space - override fun shouldShowPopup(text: Spannable?, cursorPos: Int): Boolean { - return enabled && text?.startsWith("/") == true && - !text.contains(" ") + override fun shouldShowPopup(text: Spannable, cursorPos: Int): Boolean { + return enabled && text.startsWith("/") && !text.contains(" ") } - override fun shouldDismissPopup(text: Spannable?, cursorPos: Int): Boolean { + override fun shouldDismissPopup(text: Spannable, cursorPos: Int): Boolean { return !shouldShowPopup(text, cursorPos) } } From 1dd3c1589eb38dd7957998f36fd303fe8fe0f522 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 17:48:06 +0200 Subject: [PATCH 03/65] Remove unused dep. --- dependencies_groups.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 4fe36dcf0d..6ca6c00dfa 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -239,7 +239,6 @@ ext.groups = [ regex: [ ], group: [ - 'com.yqritc', // https://github.com/cmelchior/realmfieldnameshelper/issues/42 'dk.ilios', 'im.dlg', From cd292488b6b48731f04eb6ebd5a86ea65a0548d3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 17:51:57 +0200 Subject: [PATCH 04/65] Fix warning --- .../vector/app/features/autocomplete/RecyclerViewPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/RecyclerViewPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/RecyclerViewPresenter.kt index 2648f2f210..cd7ecb8a00 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/RecyclerViewPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/RecyclerViewPresenter.kt @@ -23,7 +23,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.otaliastudios.autocomplete.AutocompletePresenter -abstract class RecyclerViewPresenter(context: Context) : AutocompletePresenter(context) { +abstract class RecyclerViewPresenter(context: Context) : AutocompletePresenter(context) { private var recyclerView: RecyclerView? = null private var clicks: ClickProvider? = null From ff09ba1208fb50b29dba464e693a65a1367063a4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 17:58:51 +0200 Subject: [PATCH 05/65] Import source from https://github.com/cmelchior/realmfieldnameshelper --- dependencies_groups.gradle | 2 - .../realmfieldnameshelper/build.gradle | 23 +++ .../dk/ilios/realmfieldnames/ClassData.kt | 24 +++ .../realmfieldnames/FieldNameFormatter.kt | 75 +++++++ .../dk/ilios/realmfieldnames/FileGenerator.kt | 82 ++++++++ .../RealmFieldNamesProcessor.kt | 187 ++++++++++++++++++ .../gradle/incremental.annotation.processors | 1 + .../javax.annotation.processing.Processor | 1 + matrix-sdk-android/build.gradle | 2 +- settings.gradle | 1 + 10 files changed, 395 insertions(+), 3 deletions(-) create mode 100644 library/external/realmfieldnameshelper/build.gradle create mode 100644 library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt create mode 100644 library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt create mode 100644 library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt create mode 100644 library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt create mode 100644 library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors create mode 100644 library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 6ca6c00dfa..35ba4b41a9 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -239,8 +239,6 @@ ext.groups = [ regex: [ ], group: [ - // https://github.com/cmelchior/realmfieldnameshelper/issues/42 - 'dk.ilios', 'im.dlg', 'me.dm7.barcodescanner', 'me.gujun.android', diff --git a/library/external/realmfieldnameshelper/build.gradle b/library/external/realmfieldnameshelper/build.gradle new file mode 100644 index 0000000000..13d8de7ca3 --- /dev/null +++ b/library/external/realmfieldnameshelper/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'kotlin' +apply plugin: 'java' + +sourceCompatibility = '1.7' +targetCompatibility = '1.7' + +dependencies { + implementation 'com.squareup:javapoet:1.13.0' +} + +task javadocJar(type: Jar, dependsOn: 'javadoc') { + from javadoc.destinationDir + classifier = 'javadoc' +} +task sourcesJar(type: Jar, dependsOn: 'classes') { + from sourceSets.main.allSource + classifier = 'sources' +} + +sourceSets { + main.java.srcDirs += 'src/main/kotlin' +} + diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt new file mode 100644 index 0000000000..d683a2adef --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt @@ -0,0 +1,24 @@ +package dk.ilios.realmfieldnames + +import java.util.TreeMap + +/** + * Class responsible for keeping track of the metadata for each Realm model class. + */ +class ClassData(val packageName: String?, val simpleClassName: String, val libraryClass: Boolean = false) { + + val fields = TreeMap() // + + fun addField(field: String, linkedType: String?) { + fields.put(field, linkedType) + } + + val qualifiedClassName: String + get() { + if (packageName != null && !packageName.isEmpty()) { + return packageName + "." + simpleClassName + } else { + return simpleClassName + } + } +} diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt new file mode 100644 index 0000000000..fbb44d333b --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt @@ -0,0 +1,75 @@ +package dk.ilios.realmfieldnames + +import java.util.Locale + +/** + * Class for encapsulating the rules for converting between the field name in the Realm model class + * and the matching name in the "<class>Fields" class. + */ +class FieldNameFormatter { + + @JvmOverloads fun format(fieldName: String?, locale: Locale = Locale.US): String { + if (fieldName == null || fieldName == "") { + return "" + } + + // Normalize word separator chars + val normalizedFieldName : String = fieldName.replace('-', '_') + + // Iterate field name using the following rules + // lowerCase m followed by upperCase anything is considered hungarian notation + // lowercase char followed by uppercase char is considered camel case + // Two uppercase chars following each other is considered non-standard camelcase + // _ and - are treated as word separators + val result = StringBuilder(normalizedFieldName.length) + + if (normalizedFieldName.codePointCount(0, normalizedFieldName.length) == 1) { + result.append(normalizedFieldName) + } else { + var previousCodepoint: Int? + var currentCodepoint: Int? = null + val length = normalizedFieldName.length + var offset = 0 + while (offset < length) { + previousCodepoint = currentCodepoint + currentCodepoint = normalizedFieldName.codePointAt(offset) + + if (previousCodepoint != null) { + if (Character.isUpperCase(currentCodepoint) && !Character.isUpperCase(previousCodepoint) && previousCodepoint === 'm'.code as Int? && result.length == 1) { + // Hungarian notation starting with: mX + result.delete(0, 1) + result.appendCodePoint(currentCodepoint) + + } else if (Character.isUpperCase(currentCodepoint) && Character.isUpperCase(previousCodepoint)) { + // InvalidCamelCase: XXYx (should have been xxYx) + if (offset + Character.charCount(currentCodepoint) < normalizedFieldName.length) { + val nextCodePoint = normalizedFieldName.codePointAt(offset + Character.charCount(currentCodepoint)) + if (Character.isLowerCase(nextCodePoint)) { + result.append("_") + } + } + result.appendCodePoint(currentCodepoint) + + } else if (currentCodepoint === '-'.code as Int? || currentCodepoint === '_'.code as Int?) { + // Word-separator: x-x or x_x + result.append("_") + + } else if (Character.isUpperCase(currentCodepoint) && !Character.isUpperCase(previousCodepoint) && Character.isLetterOrDigit(previousCodepoint)) { + // camelCase: xX + result.append("_") + result.appendCodePoint(currentCodepoint) + } else { + // Unknown type + result.appendCodePoint(currentCodepoint) + } + } else { + // Only triggered for first code point + result.appendCodePoint(currentCodepoint) + } + offset += Character.charCount(currentCodepoint) + } + } + + return result.toString().uppercase(locale) + } +} diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt new file mode 100644 index 0000000000..02bdc70f44 --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt @@ -0,0 +1,82 @@ +package dk.ilios.realmfieldnames + +import com.squareup.javapoet.FieldSpec +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.TypeSpec + +import java.io.IOException + +import javax.annotation.processing.Filer +import javax.lang.model.element.Modifier + +/** + * Class responsible for creating the final output files. + */ +class FileGenerator(private val filer: Filer) { + private val formatter: FieldNameFormatter + + init { + this.formatter = FieldNameFormatter() + } + + /** + * Generates all the "<class>Fields" fields with field name references. + * @param fileData Files to create. + * * + * @return `true` if the files where generated, `false` if not. + */ + fun generate(fileData: Set): Boolean { + return fileData + .filter { !it.libraryClass } + .all { generateFile(it, fileData) } + } + + private fun generateFile(classData: ClassData, classPool: Set): Boolean { + + val fileBuilder = TypeSpec.classBuilder(classData.simpleClassName + "Fields") + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addJavadoc("This class enumerate all queryable fields in {@link \$L.\$L}\n", + classData.packageName, classData.simpleClassName) + + + // Add a static field reference to each queryable field in the Realm model class + classData.fields.forEach { fieldName, value -> + if (value != null) { + // Add linked field names (only up to depth 1) + for (data in classPool) { + if (data.qualifiedClassName == value) { + val linkedTypeSpec = TypeSpec.classBuilder(formatter.format(fieldName)) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) + val linkedClassFields = data.fields + addField(linkedTypeSpec, "$", fieldName) + for (linkedFieldName in linkedClassFields.keys) { + addField(linkedTypeSpec, linkedFieldName, fieldName + "." + linkedFieldName) + } + fileBuilder.addType(linkedTypeSpec.build()) + } + } + } else { + // Add normal field name + addField(fileBuilder, fieldName, fieldName) + } + } + + val javaFile = JavaFile.builder(classData.packageName, fileBuilder.build()).build() + try { + javaFile.writeTo(filer) + return true + } catch (e: IOException) { + e.printStackTrace() + return false + } + + } + + private fun addField(fileBuilder: TypeSpec.Builder, fieldName: String, fieldNameValue: String) { + val field = FieldSpec.builder(String::class.java, formatter.format(fieldName)) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .initializer("\$S", fieldNameValue) + .build() + fileBuilder.addField(field) + } +} diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt new file mode 100644 index 0000000000..6f82882333 --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt @@ -0,0 +1,187 @@ +package dk.ilios.realmfieldnames + +import java.util.* + +import javax.annotation.processing.AbstractProcessor +import javax.annotation.processing.Messager +import javax.annotation.processing.ProcessingEnvironment +import javax.annotation.processing.RoundEnvironment +import javax.annotation.processing.SupportedAnnotationTypes +import javax.lang.model.SourceVersion +import javax.lang.model.element.* +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.TypeMirror +import javax.lang.model.util.Elements +import javax.lang.model.util.Types +import javax.tools.Diagnostic + +/** + * The Realm Field Names Generator is a processor that looks at all available Realm model classes + * and create an companion class with easy, type-safe access to all field names. + */ + +@SupportedAnnotationTypes("io.realm.annotations.RealmClass") +class RealmFieldNamesProcessor : AbstractProcessor() { + + private val classes = HashSet() + lateinit private var typeUtils: Types + lateinit private var messager: Messager + lateinit private var elementUtils: Elements + private var ignoreAnnotation: TypeMirror? = null + private var realmClassAnnotation: TypeElement? = null + private var realmModelInterface: TypeMirror? = null + private var realmListClass: DeclaredType? = null + private var realmResultsClass: DeclaredType? = null + private var fileGenerator: FileGenerator? = null + private var done = false + + @Synchronized override fun init(processingEnv: ProcessingEnvironment) { + super.init(processingEnv) + typeUtils = processingEnv.typeUtils!! + messager = processingEnv.messager!! + elementUtils = processingEnv.elementUtils!! + + // If the Realm class isn't found something is wrong the project setup. + // Most likely Realm isn't on the class path, so just disable the + // annotation processor + val isRealmAvailable = elementUtils.getTypeElement("io.realm.Realm") != null + if (!isRealmAvailable) { + done = true + } else { + ignoreAnnotation = elementUtils.getTypeElement("io.realm.annotations.Ignore")?.asType() + realmClassAnnotation = elementUtils.getTypeElement("io.realm.annotations.RealmClass") + realmModelInterface = elementUtils.getTypeElement("io.realm.RealmModel")?.asType() + realmListClass = typeUtils.getDeclaredType(elementUtils.getTypeElement("io.realm.RealmList"), + typeUtils.getWildcardType(null, null)) + realmResultsClass = typeUtils.getDeclaredType(elementUtils.getTypeElement("io.realm.RealmResults"), + typeUtils.getWildcardType(null, null)) + fileGenerator = FileGenerator(processingEnv.filer) + } + } + + override fun getSupportedSourceVersion(): SourceVersion { + return SourceVersion.latestSupported() + } + + override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { + if (done) { + return CONSUME_ANNOTATIONS + } + + // Create all proxy classes + roundEnv.getElementsAnnotatedWith(realmClassAnnotation).forEach { classElement -> + if (typeUtils.isAssignable(classElement.asType(), realmModelInterface)) { + val classData = processClass(classElement as TypeElement) + classes.add(classData) + } + } + + // If a model class references a library class, the library class will not be part of this + // annotation processor round. For all those references we need to pull field information + // from the classpath instead. + val libraryClasses = HashMap() + classes.forEach { + it.fields.forEach { _, value -> + // Analyze the library class file the first time it is encountered. + if (value != null ) { + if (classes.all{ it.qualifiedClassName != value } && !libraryClasses.containsKey(value)) { + libraryClasses.put(value, processLibraryClass(value)) + } + } + } + } + classes.addAll(libraryClasses.values) + + done = fileGenerator!!.generate(classes) + return CONSUME_ANNOTATIONS + } + + private fun processClass(classElement: TypeElement): ClassData { + val packageName = getPackageName(classElement) + val className = classElement.simpleName.toString() + val data = ClassData(packageName, className) + + // Find all appropriate fields + classElement.enclosedElements.forEach { + val elementKind = it.kind + if (elementKind == ElementKind.FIELD) { + val variableElement = it as VariableElement + + val modifiers = variableElement.modifiers + if (modifiers.contains(Modifier.STATIC)) { + return@forEach // completely ignore any static fields + } + + // Don't add any fields marked with @Ignore + val ignoreField = variableElement.annotationMirrors + .map { it.annotationType.toString() } + .contains("io.realm.annotations.Ignore") + + if (!ignoreField) { + data.addField(it.getSimpleName().toString(), getLinkedFieldType(it)) + } + } + } + + return data + } + + private fun processLibraryClass(qualifiedClassName: String): ClassData { + val libraryClass = Class.forName(qualifiedClassName) // Library classes should be on the classpath + val packageName = libraryClass.`package`.name + val className = libraryClass.simpleName + val data = ClassData(packageName, className, libraryClass = true) + + libraryClass.declaredFields.forEach { field -> + if (java.lang.reflect.Modifier.isStatic(field.modifiers)) { + return@forEach // completely ignore any static fields + } + + // Add field if it is not being ignored. + if (field.annotations.all { it.toString() != "io.realm.annotations.Ignore" }) { + data.addField(field.name, field.type.name) + } + } + + return data + } + + /** + * Returns the qualified name of the linked Realm class field or `null` if it is not a linked + * class. + */ + private fun getLinkedFieldType(field: Element): String? { + if (typeUtils.isAssignable(field.asType(), realmModelInterface)) { + // Object link + val typeElement = elementUtils.getTypeElement(field.asType().toString()) + return typeElement.qualifiedName.toString() + } else if (typeUtils.isAssignable(field.asType(), realmListClass) || typeUtils.isAssignable(field.asType(), realmResultsClass)) { + // List link or LinkingObjects + val fieldType = field.asType() + val typeArguments = (fieldType as DeclaredType).typeArguments + if (typeArguments.size == 0) { + return null + } + return typeArguments[0].toString() + } else { + return null + } + } + + private fun getPackageName(classElement: TypeElement): String? { + val enclosingElement = classElement.enclosingElement + + if (enclosingElement.kind != ElementKind.PACKAGE) { + messager.printMessage(Diagnostic.Kind.ERROR, + "Could not determine the package name. Enclosing element was: " + enclosingElement.kind) + return null + } + + val packageElement = enclosingElement as PackageElement + return packageElement.qualifiedName.toString() + } + + companion object { + private const val CONSUME_ANNOTATIONS = false + } +} diff --git a/library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors b/library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 0000000000..57897c8297 --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1 @@ +dk.ilios.realmfieldnames.RealmFieldNamesProcessor,aggregating \ No newline at end of file diff --git a/library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000000..58fadd699c --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +dk.ilios.realmfieldnames.RealmFieldNamesProcessor \ No newline at end of file diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 7420fba45e..82bb0b31d5 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -189,7 +189,7 @@ dependencies { // Database implementation 'com.github.Zhuinden:realm-monarchy:0.7.1' - kapt 'dk.ilios:realmfieldnameshelper:2.0.0' + kapt project(":library:external:realmfieldnameshelper") // Shared Preferences implementation libs.androidx.preferenceKtx diff --git a/settings.gradle b/settings.gradle index b3ecd410d6..7b57b530f7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,7 @@ include ':library:external:diff-match-patch' include ':library:external:dialpad' include ':library:external:textdrawable' include ':library:external:autocomplete' +include ':library:external:realmfieldnameshelper' include ':library:rustCrypto' include ':matrix-sdk-android' From e9b94346716d300c8e9503c66606424115fc48c9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 18:01:36 +0200 Subject: [PATCH 06/65] Remove unused dep. --- dependencies_groups.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 35ba4b41a9..60808a0d7f 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -239,7 +239,6 @@ ext.groups = [ regex: [ ], group: [ - 'im.dlg', 'me.dm7.barcodescanner', 'me.gujun.android', ] From 69680a98560e675dacb56422737204414cb79e74 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 18:15:22 +0200 Subject: [PATCH 07/65] Import source from https://github.com/2dxgujun/Kpan --- dependencies_groups.gradle | 1 - library/external/jsonviewer/build.gradle | 4 +- library/external/span/build.gradle | 20 ++ .../main/kotlin/me/gujun/android/span/span.kt | 316 ++++++++++++++++++ .../android/span/style/CustomTypefaceSpan.kt | 36 ++ .../android/span/style/LineSpacingSpan.kt | 31 ++ .../android/span/style/SimpleClickableSpan.kt | 10 + .../span/style/TextDecorationLineSpan.kt | 29 ++ .../android/span/style/VerticalPaddingSpan.kt | 41 +++ settings.gradle | 1 + vector-app/build.gradle | 1 + vector/build.gradle | 4 +- 12 files changed, 487 insertions(+), 7 deletions(-) create mode 100644 library/external/span/build.gradle create mode 100644 library/external/span/src/main/kotlin/me/gujun/android/span/span.kt create mode 100644 library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt create mode 100644 library/external/span/src/main/kotlin/me/gujun/android/span/style/LineSpacingSpan.kt create mode 100644 library/external/span/src/main/kotlin/me/gujun/android/span/style/SimpleClickableSpan.kt create mode 100644 library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt create mode 100644 library/external/span/src/main/kotlin/me/gujun/android/span/style/VerticalPaddingSpan.kt diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 60808a0d7f..5505e34b25 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -240,7 +240,6 @@ ext.groups = [ ], group: [ 'me.dm7.barcodescanner', - 'me.gujun.android', ] ] ] diff --git a/library/external/jsonviewer/build.gradle b/library/external/jsonviewer/build.gradle index 7b3b62c082..c501f583d0 100644 --- a/library/external/jsonviewer/build.gradle +++ b/library/external/jsonviewer/build.gradle @@ -58,9 +58,7 @@ dependencies { implementation libs.airbnb.mavericks // Span utils - implementation('me.gujun.android:span:1.7') { - exclude group: 'com.android.support', module: 'support-annotations' - } + implementation project(":library:external:span") implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid diff --git a/library/external/span/build.gradle b/library/external/span/build.gradle new file mode 100644 index 0000000000..05adbacb4d --- /dev/null +++ b/library/external/span/build.gradle @@ -0,0 +1,20 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + namespace "me.gujun.android.span" + compileSdk versions.compileSdk + + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } +} + +dependencies { + implementation 'com.android.support:support-annotations:28.0.0' +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/span.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/span.kt new file mode 100644 index 0000000000..5ca63b7e01 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/span.kt @@ -0,0 +1,316 @@ +package me.gujun.android.span + +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.text.Layout +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.TextUtils +import android.text.style.AbsoluteSizeSpan +import android.text.style.AlignmentSpan +import android.text.style.BackgroundColorSpan +import android.text.style.ForegroundColorSpan +import android.text.style.ImageSpan +import android.text.style.QuoteSpan +import android.text.style.StyleSpan +import android.text.style.SubscriptSpan +import android.text.style.SuperscriptSpan +import android.text.style.TypefaceSpan +import android.text.style.URLSpan +import android.view.View +import androidx.annotation.ColorInt +import androidx.annotation.Dimension +import me.gujun.android.span.style.CustomTypefaceSpan +import me.gujun.android.span.style.LineSpacingSpan +import me.gujun.android.span.style.SimpleClickableSpan +import me.gujun.android.span.style.TextDecorationLineSpan +import me.gujun.android.span.style.VerticalPaddingSpan + +class Span(val parent: Span? = null) : SpannableStringBuilder() { + + companion object { + val EMPTY_STYLE = Span() + + var globalStyle: Span = EMPTY_STYLE + } + + var text: CharSequence = "" + + @ColorInt var textColor: Int? = parent?.textColor + + @ColorInt var backgroundColor: Int? = parent?.backgroundColor + + @Dimension(unit = Dimension.PX) var textSize: Int? = parent?.textSize + + var fontFamily: String? = parent?.fontFamily + + var typeface: Typeface? = parent?.typeface + + var textStyle: String? = parent?.textStyle + + var alignment: String? = parent?.alignment + + var textDecorationLine: String? = parent?.textDecorationLine + + @Dimension(unit = Dimension.PX) var lineSpacing: Int? = null + + @Dimension(unit = Dimension.PX) var paddingTop: Int? = null + + @Dimension(unit = Dimension.PX) var paddingBottom: Int? = null + + @Dimension(unit = Dimension.PX) var verticalPadding: Int? = null + + var onClick: (() -> Unit)? = null + + var spans: ArrayList = ArrayList() + + var style: Span = EMPTY_STYLE + + private fun buildCharacterStyle(builder: ArrayList) { + if (textColor != null) { + builder.add(ForegroundColorSpan(textColor!!)) + } + + if (backgroundColor != null) { + builder.add(BackgroundColorSpan(backgroundColor!!)) + } + + if (textSize != null) { + builder.add(AbsoluteSizeSpan(textSize!!)) + } + + if (!TextUtils.isEmpty(fontFamily)) { + builder.add(TypefaceSpan(fontFamily)) + } + + if (typeface != null) { + builder.add(CustomTypefaceSpan(typeface!!)) + } + + if (!TextUtils.isEmpty(textStyle)) { + builder.add(StyleSpan(when (textStyle) { + "normal" -> Typeface.NORMAL + "bold" -> Typeface.BOLD + "italic" -> Typeface.ITALIC + "bold_italic" -> Typeface.BOLD_ITALIC + else -> throw RuntimeException("Unknown text style") + })) + } + + if (!TextUtils.isEmpty(textDecorationLine)) { + builder.add(TextDecorationLineSpan(textDecorationLine!!)) + } + + if (onClick != null) { + builder.add(object : SimpleClickableSpan() { + override fun onClick(widget: View) { + onClick?.invoke() + } + }) + } + } + + private fun buildParagraphStyle(builder: ArrayList) { + if (!TextUtils.isEmpty(alignment)) { + builder.add(AlignmentSpan.Standard(when (alignment) { + "normal" -> Layout.Alignment.ALIGN_NORMAL + "opposite" -> Layout.Alignment.ALIGN_OPPOSITE + "center" -> Layout.Alignment.ALIGN_CENTER + else -> throw RuntimeException("Unknown text alignment") + })) + } + + if (lineSpacing != null) { + builder.add(LineSpacingSpan(lineSpacing!!)) + } + + paddingTop = when { + paddingTop != null -> paddingTop + verticalPadding != null -> verticalPadding + else -> 0 + } + paddingBottom = when { + paddingBottom != null -> paddingBottom + verticalPadding != null -> verticalPadding + else -> 0 + } + if (paddingTop != 0 || paddingBottom != 0) { + builder.add(VerticalPaddingSpan(paddingTop!!, paddingBottom!!)) + } + } + + private fun prebuild() { + override(style) + } + + fun build(): Span { + prebuild() + val builder = ArrayList() + if (!TextUtils.isEmpty(text)) { + var p = this.parent + while (p != null) { + if (!TextUtils.isEmpty(p.text)) { + throw RuntimeException("Can't nest \"$text\" in spans") + } + p = p.parent + } + append(text) + buildCharacterStyle(builder) + buildParagraphStyle(builder) + } else { + buildParagraphStyle(builder) + } + + builder.addAll(spans) + builder.forEach { + setSpan(it, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return this + } + + fun override(style: Span) { + if (textColor == null) { + textColor = style.textColor + } + if (backgroundColor == null) { + backgroundColor = style.backgroundColor + } + if (textSize == null) { + textSize = style.textSize + } + if (fontFamily == null) { + fontFamily = style.fontFamily + } + if (typeface == null) { + typeface = style.typeface + } + if (textStyle == null) { + textStyle = style.textStyle + } + if (alignment == null) { + alignment = style.alignment + } + if (textDecorationLine == null) { + textDecorationLine = style.textDecorationLine + } + if (lineSpacing == null) { + lineSpacing = style.lineSpacing + } + if (paddingTop == null) { + paddingTop = style.paddingTop + } + if (paddingBottom == null) { + paddingBottom = style.paddingBottom + } + if (verticalPadding == null) { + verticalPadding = style.verticalPadding + } + if (onClick == null) { + onClick = style.onClick + } + spans.addAll(style.spans) + } + + operator fun CharSequence.unaryPlus(): CharSequence { + return append(Span(parent = this@Span).apply { + text = this@unaryPlus + build() + }) + } + + operator fun Span.plus(other: CharSequence): CharSequence { + return append(Span(parent = this).apply { + text = other + build() + }) + } +} + +fun span(init: Span.() -> Unit): Span = Span().apply { + override(Span.globalStyle) + init() + build() +} + +fun span(text: CharSequence, init: Span.() -> Unit): Span = Span().apply { + override(Span.globalStyle) + this.text = text + init() + build() +} + +fun style(init: Span.() -> Unit): Span = Span().apply { + init() +} + +fun Span.span(init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + init() + build() + }) +} + +fun Span.span(text: CharSequence, init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + this.text = text + init() + build() + }) +} + +fun Span.link(url: String, text: CharSequence = "", + init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + this.text = text + this.spans.add(URLSpan(url)) + init() + build() + }) +} + +fun Span.quote(@ColorInt color: Int, text: CharSequence = "", + init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + this.text = text + this.spans.add(QuoteSpan(color)) + init() + build() + }) +} + +fun Span.superscript(text: CharSequence = "", init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + this.text = text + this.spans.add(SuperscriptSpan()) + init() + build() + }) +} + +fun Span.subscript(text: CharSequence = "", init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + this.text = text + this.spans.add(SubscriptSpan()) + init() + build() + }) +} + +fun Span.image(drawable: Drawable, alignment: String = "bottom", + init: Span.() -> Unit = {}): Span = apply { + drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) + append(Span(parent = this).apply { + this.text = " " + this.spans.add(ImageSpan(drawable, when (alignment) { + "bottom" -> ImageSpan.ALIGN_BOTTOM + "baseline" -> ImageSpan.ALIGN_BASELINE + else -> throw RuntimeException("Unknown image alignment") + })) + init() + build() + }) +} + +fun Span.addSpan(what: Any) = apply { + this.spans.add(what) +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt new file mode 100644 index 0000000000..57ae4548b6 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt @@ -0,0 +1,36 @@ +package me.gujun.android.span.style + +import android.graphics.Paint +import android.graphics.Typeface +import android.text.TextPaint +import android.text.style.MetricAffectingSpan + +class CustomTypefaceSpan(private val tf: Typeface) : MetricAffectingSpan() { + + override fun updateMeasureState(paint: TextPaint) { + apply(paint, tf) + } + + override fun updateDrawState(ds: TextPaint) { + apply(ds, tf) + } + + private fun apply(paint: Paint, tf: Typeface) { + val oldStyle: Int + + val old = paint.typeface + oldStyle = old?.style ?: 0 + + val fake = oldStyle and tf.style.inv() + + if (fake and Typeface.BOLD != 0) { + paint.isFakeBoldText = true + } + + if (fake and Typeface.ITALIC != 0) { + paint.textSkewX = -0.25f + } + + paint.typeface = tf + } +} \ No newline at end of file diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/LineSpacingSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/LineSpacingSpan.kt new file mode 100644 index 0000000000..53aa42aa83 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/LineSpacingSpan.kt @@ -0,0 +1,31 @@ +package me.gujun.android.span.style + +import android.graphics.Paint.FontMetricsInt +import android.text.Spanned +import android.text.style.LineHeightSpan + +class LineSpacingSpan(private val add: Int) : LineHeightSpan { + + override fun chooseHeight(text: CharSequence, start: Int, end: Int, spanstartv: Int, v: Int, + fm: FontMetricsInt) { + text as Spanned + /*val spanStart =*/ text.getSpanStart(this) + val spanEnd = text.getSpanEnd(this) + +// Log.d("DEBUG", "Text: start=$start end=$end v=$v") // end may include the \n character +// Log.d("DEBUG", "${text.slice(start until end)}".replace("\n", "#")) +// Log.d("DEBUG", "LineSpacingSpan: spanStart=$spanStart spanEnd=$spanEnd spanstartv=$spanstartv") +// Log.d("DEBUG", "$fm") +// Log.d("DEBUG", "-----------------------") + + if (spanstartv == v) { + fm.descent += add + } else if (text[start - 1] == '\n') { + fm.descent += add + } + + if (end == spanEnd || end - 1 == spanEnd) { + fm.descent -= add + } + } +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/SimpleClickableSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/SimpleClickableSpan.kt new file mode 100644 index 0000000000..2ae524bca9 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/SimpleClickableSpan.kt @@ -0,0 +1,10 @@ +package me.gujun.android.span.style + +import android.text.TextPaint +import android.text.style.ClickableSpan + +abstract class SimpleClickableSpan : ClickableSpan() { + override fun updateDrawState(ds: TextPaint) { + // no-op + } +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt new file mode 100644 index 0000000000..662cb82fed --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt @@ -0,0 +1,29 @@ +package me.gujun.android.span.style + +import android.text.TextPaint +import android.text.style.CharacterStyle + +class TextDecorationLineSpan(private val textDecorationLine: String) : CharacterStyle() { + + override fun updateDrawState(tp: TextPaint) { + when (textDecorationLine) { + "none" -> { + tp.isUnderlineText = false + tp.isStrikeThruText = false + } + "underline" -> { + tp.isUnderlineText = true + tp.isStrikeThruText = false + } + "line-through" -> { + tp.isUnderlineText = false + tp.isStrikeThruText = true + } + "underline line-through" -> { + tp.isUnderlineText = true + tp.isStrikeThruText = true + } + else -> throw RuntimeException("Unknown text decoration line") + } + } +} \ No newline at end of file diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/VerticalPaddingSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/VerticalPaddingSpan.kt new file mode 100644 index 0000000000..600ea72e57 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/VerticalPaddingSpan.kt @@ -0,0 +1,41 @@ +package me.gujun.android.span.style + +import android.graphics.Paint.FontMetricsInt +import android.text.Spanned +import android.text.style.LineHeightSpan + +class VerticalPaddingSpan(private val paddingTop: Int, + private val paddingBottom: Int) : LineHeightSpan { + + private var flag: Boolean = true + + override fun chooseHeight(text: CharSequence, start: Int, end: Int, spanstartv: Int, v: Int, + fm: FontMetricsInt) { + text as Spanned + /*val spanStart =*/ text.getSpanStart(this) + val spanEnd = text.getSpanEnd(this) + +// Log.d("DEBUG", "Text: start=$start end=$end v=$v") // end may include the \n character +// Log.d("DEBUG", "${text.slice(start until end)}".replace("\n", "#")) +// Log.d("DEBUG", "VerticalPadding: spanStart=$spanStart spanEnd=$spanEnd spanstartv=$spanstartv") +// Log.d("DEBUG", "$fm") +// Log.d("DEBUG", "-----------------------") + + if (spanstartv == v) { + fm.top -= paddingTop + fm.ascent -= paddingTop + flag = true + } else if (flag && text[start - 1] != '\n') { + fm.top += paddingTop + fm.ascent += paddingTop + flag = false + } else { + flag = false + } + + if (end == spanEnd || end - 1 == spanEnd) { + fm.descent += paddingBottom + fm.bottom += paddingBottom + } + } +} diff --git a/settings.gradle b/settings.gradle index 7b57b530f7..bccfa7913a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,6 +14,7 @@ include ':library:external:dialpad' include ':library:external:textdrawable' include ':library:external:autocomplete' include ':library:external:realmfieldnameshelper' +include ':library:external:span' include ':library:rustCrypto' include ':matrix-sdk-android' diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 8221497e74..25980df834 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -396,6 +396,7 @@ dependencies { implementation project(':vector') implementation project(':vector-config') implementation project(':library:core-utils') + debugImplementation project(':library:external:span') debugImplementation project(':library:ui-styles') implementation libs.dagger.hilt implementation 'androidx.multidex:multidex:2.0.1' diff --git a/vector/build.gradle b/vector/build.gradle index 83f09fb10b..e45db64548 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -187,9 +187,7 @@ dependencies { // UI implementation libs.google.material - api('me.gujun.android:span:1.7') { - exclude group: 'com.android.support', module: 'support-annotations' - } + implementation project(":library:external:span") implementation libs.markwon.core implementation libs.markwon.extLatex implementation libs.markwon.imageGlide From e9f9decf00e6a8d80fc3dd1e6c5e2030871e3e16 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 18:58:42 +0200 Subject: [PATCH 08/65] Import source from https://github.com/dm77/barcodescanner --- dependencies_groups.gradle | 1 - .../external/barcodescanner/core/build.gradle | 26 ++ .../core/BarcodeScannerView.java | 339 ++++++++++++++++++ .../core/CameraHandlerThread.java | 37 ++ .../barcodescanner/core/CameraPreview.java | 312 ++++++++++++++++ .../dm7/barcodescanner/core/CameraUtils.java | 63 ++++ .../barcodescanner/core/CameraWrapper.java | 23 ++ .../dm7/barcodescanner/core/DisplayUtils.java | 41 +++ .../dm7/barcodescanner/core/IViewFinder.java | 53 +++ .../barcodescanner/core/ViewFinderView.java | 259 +++++++++++++ .../core/src/main/res/values-hdpi/strings.xml | 5 + .../src/main/res/values-xhdpi/strings.xml | 5 + .../src/main/res/values-xxhdpi/strings.xml | 5 + .../core/src/main/res/values/attrs.xml | 17 + .../core/src/main/res/values/colors.xml | 6 + .../core/src/main/res/values/strings.xml | 4 + .../barcodescanner/zxing/build.gradle | 29 ++ .../zxing/ZXingScannerView.java | 198 ++++++++++ settings.gradle | 2 + vector/build.gradle | 6 +- 20 files changed, 1425 insertions(+), 6 deletions(-) create mode 100644 library/external/barcodescanner/core/build.gradle create mode 100644 library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/BarcodeScannerView.java create mode 100644 library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraHandlerThread.java create mode 100644 library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraPreview.java create mode 100644 library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraUtils.java create mode 100644 library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraWrapper.java create mode 100644 library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/DisplayUtils.java create mode 100644 library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/IViewFinder.java create mode 100644 library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/ViewFinderView.java create mode 100644 library/external/barcodescanner/core/src/main/res/values-hdpi/strings.xml create mode 100644 library/external/barcodescanner/core/src/main/res/values-xhdpi/strings.xml create mode 100644 library/external/barcodescanner/core/src/main/res/values-xxhdpi/strings.xml create mode 100644 library/external/barcodescanner/core/src/main/res/values/attrs.xml create mode 100644 library/external/barcodescanner/core/src/main/res/values/colors.xml create mode 100644 library/external/barcodescanner/core/src/main/res/values/strings.xml create mode 100644 library/external/barcodescanner/zxing/build.gradle create mode 100644 library/external/barcodescanner/zxing/src/main/java/me/dm7/barcodescanner/zxing/ZXingScannerView.java diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 5505e34b25..9e5d1e4072 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -239,7 +239,6 @@ ext.groups = [ regex: [ ], group: [ - 'me.dm7.barcodescanner', ] ] ] diff --git a/library/external/barcodescanner/core/build.gradle b/library/external/barcodescanner/core/build.gradle new file mode 100644 index 0000000000..583b435dd7 --- /dev/null +++ b/library/external/barcodescanner/core/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'com.android.library' + +android { + namespace "me.dm7.barcodescanner.core" + compileSdk versions.compileSdk + + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + } + + compileOptions { + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat + } +} + +dependencies { + implementation 'androidx.annotation:annotation-jvm:1.6.0' +} + +afterEvaluate { + tasks.findAll { it.name.startsWith("lint") }.each { + it.enabled = false + } +} diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/BarcodeScannerView.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/BarcodeScannerView.java new file mode 100644 index 0000000000..ecfba76d89 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/BarcodeScannerView.java @@ -0,0 +1,339 @@ +package me.dm7.barcodescanner.core; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.Rect; +import android.hardware.Camera; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; + +import androidx.annotation.ColorInt; + +public abstract class BarcodeScannerView extends FrameLayout implements Camera.PreviewCallback { + + private CameraWrapper mCameraWrapper; + private CameraPreview mPreview; + private IViewFinder mViewFinderView; + private Rect mFramingRectInPreview; + private CameraHandlerThread mCameraHandlerThread; + private Boolean mFlashState; + private boolean mAutofocusState = true; + private boolean mShouldScaleToFill = true; + + private boolean mIsLaserEnabled = true; + @ColorInt private int mLaserColor = getResources().getColor(R.color.viewfinder_laser); + @ColorInt private int mBorderColor = getResources().getColor(R.color.viewfinder_border); + private int mMaskColor = getResources().getColor(R.color.viewfinder_mask); + private int mBorderWidth = getResources().getInteger(R.integer.viewfinder_border_width); + private int mBorderLength = getResources().getInteger(R.integer.viewfinder_border_length); + private boolean mRoundedCorner = false; + private int mCornerRadius = 0; + private boolean mSquaredFinder = false; + private float mBorderAlpha = 1.0f; + private int mViewFinderOffset = 0; + private float mAspectTolerance = 0.1f; + + public BarcodeScannerView(Context context) { + super(context); + init(); + } + + public BarcodeScannerView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + + TypedArray a = context.getTheme().obtainStyledAttributes( + attributeSet, + R.styleable.BarcodeScannerView, + 0, 0); + + try { + setShouldScaleToFill(a.getBoolean(R.styleable.BarcodeScannerView_shouldScaleToFill, true)); + mIsLaserEnabled = a.getBoolean(R.styleable.BarcodeScannerView_laserEnabled, mIsLaserEnabled); + mLaserColor = a.getColor(R.styleable.BarcodeScannerView_laserColor, mLaserColor); + mBorderColor = a.getColor(R.styleable.BarcodeScannerView_borderColor, mBorderColor); + mMaskColor = a.getColor(R.styleable.BarcodeScannerView_maskColor, mMaskColor); + mBorderWidth = a.getDimensionPixelSize(R.styleable.BarcodeScannerView_borderWidth, mBorderWidth); + mBorderLength = a.getDimensionPixelSize(R.styleable.BarcodeScannerView_borderLength, mBorderLength); + + mRoundedCorner = a.getBoolean(R.styleable.BarcodeScannerView_roundedCorner, mRoundedCorner); + mCornerRadius = a.getDimensionPixelSize(R.styleable.BarcodeScannerView_cornerRadius, mCornerRadius); + mSquaredFinder = a.getBoolean(R.styleable.BarcodeScannerView_squaredFinder, mSquaredFinder); + mBorderAlpha = a.getFloat(R.styleable.BarcodeScannerView_borderAlpha, mBorderAlpha); + mViewFinderOffset = a.getDimensionPixelSize(R.styleable.BarcodeScannerView_finderOffset, mViewFinderOffset); + } finally { + a.recycle(); + } + + init(); + } + + private void init() { + mViewFinderView = createViewFinderView(getContext()); + } + + public final void setupLayout(CameraWrapper cameraWrapper) { + removeAllViews(); + + mPreview = new CameraPreview(getContext(), cameraWrapper, this); + mPreview.setAspectTolerance(mAspectTolerance); + mPreview.setShouldScaleToFill(mShouldScaleToFill); + if (!mShouldScaleToFill) { + RelativeLayout relativeLayout = new RelativeLayout(getContext()); + relativeLayout.setGravity(Gravity.CENTER); + relativeLayout.setBackgroundColor(Color.BLACK); + relativeLayout.addView(mPreview); + addView(relativeLayout); + } else { + addView(mPreview); + } + + if (mViewFinderView instanceof View) { + addView((View) mViewFinderView); + } else { + throw new IllegalArgumentException("IViewFinder object returned by " + + "'createViewFinderView()' should be instance of android.view.View"); + } + } + + /** + *

Method that creates view that represents visual appearance of a barcode scanner

+ *

Override it to provide your own view for visual appearance of a barcode scanner

+ * + * @param context {@link Context} + * @return {@link android.view.View} that implements {@link ViewFinderView} + */ + protected IViewFinder createViewFinderView(Context context) { + ViewFinderView viewFinderView = new ViewFinderView(context); + viewFinderView.setBorderColor(mBorderColor); + viewFinderView.setLaserColor(mLaserColor); + viewFinderView.setLaserEnabled(mIsLaserEnabled); + viewFinderView.setBorderStrokeWidth(mBorderWidth); + viewFinderView.setBorderLineLength(mBorderLength); + viewFinderView.setMaskColor(mMaskColor); + + viewFinderView.setBorderCornerRounded(mRoundedCorner); + viewFinderView.setBorderCornerRadius(mCornerRadius); + viewFinderView.setSquareViewFinder(mSquaredFinder); + viewFinderView.setViewFinderOffset(mViewFinderOffset); + return viewFinderView; + } + + public void setLaserColor(int laserColor) { + mLaserColor = laserColor; + mViewFinderView.setLaserColor(mLaserColor); + mViewFinderView.setupViewFinder(); + } + public void setMaskColor(int maskColor) { + mMaskColor = maskColor; + mViewFinderView.setMaskColor(mMaskColor); + mViewFinderView.setupViewFinder(); + } + public void setBorderColor(int borderColor) { + mBorderColor = borderColor; + mViewFinderView.setBorderColor(mBorderColor); + mViewFinderView.setupViewFinder(); + } + public void setBorderStrokeWidth(int borderStrokeWidth) { + mBorderWidth = borderStrokeWidth; + mViewFinderView.setBorderStrokeWidth(mBorderWidth); + mViewFinderView.setupViewFinder(); + } + public void setBorderLineLength(int borderLineLength) { + mBorderLength = borderLineLength; + mViewFinderView.setBorderLineLength(mBorderLength); + mViewFinderView.setupViewFinder(); + } + public void setLaserEnabled(boolean isLaserEnabled) { + mIsLaserEnabled = isLaserEnabled; + mViewFinderView.setLaserEnabled(mIsLaserEnabled); + mViewFinderView.setupViewFinder(); + } + public void setIsBorderCornerRounded(boolean isBorderCornerRounded) { + mRoundedCorner = isBorderCornerRounded; + mViewFinderView.setBorderCornerRounded(mRoundedCorner); + mViewFinderView.setupViewFinder(); + } + public void setBorderCornerRadius(int borderCornerRadius) { + mCornerRadius = borderCornerRadius; + mViewFinderView.setBorderCornerRadius(mCornerRadius); + mViewFinderView.setupViewFinder(); + } + public void setSquareViewFinder(boolean isSquareViewFinder) { + mSquaredFinder = isSquareViewFinder; + mViewFinderView.setSquareViewFinder(mSquaredFinder); + mViewFinderView.setupViewFinder(); + } + public void setBorderAlpha(float borderAlpha) { + mBorderAlpha = borderAlpha; + mViewFinderView.setBorderAlpha(mBorderAlpha); + mViewFinderView.setupViewFinder(); + } + + public void startCamera(int cameraId) { + if(mCameraHandlerThread == null) { + mCameraHandlerThread = new CameraHandlerThread(this); + } + mCameraHandlerThread.startCamera(cameraId); + } + + public void setupCameraPreview(CameraWrapper cameraWrapper) { + mCameraWrapper = cameraWrapper; + if(mCameraWrapper != null) { + setupLayout(mCameraWrapper); + mViewFinderView.setupViewFinder(); + if(mFlashState != null) { + setFlash(mFlashState); + } + setAutoFocus(mAutofocusState); + } + } + + public void startCamera() { + startCamera(CameraUtils.getDefaultCameraId()); + } + + public void stopCamera() { + if(mCameraWrapper != null) { + mPreview.stopCameraPreview(); + mPreview.setCamera(null, null); + mCameraWrapper.mCamera.release(); + mCameraWrapper = null; + } + if(mCameraHandlerThread != null) { + mCameraHandlerThread.quit(); + mCameraHandlerThread = null; + } + } + + public void stopCameraPreview() { + if(mPreview != null) { + mPreview.stopCameraPreview(); + } + } + + protected void resumeCameraPreview() { + if(mPreview != null) { + mPreview.showCameraPreview(); + } + } + + public synchronized Rect getFramingRectInPreview(int previewWidth, int previewHeight) { + if (mFramingRectInPreview == null) { + Rect framingRect = mViewFinderView.getFramingRect(); + int viewFinderViewWidth = mViewFinderView.getWidth(); + int viewFinderViewHeight = mViewFinderView.getHeight(); + if (framingRect == null || viewFinderViewWidth == 0 || viewFinderViewHeight == 0) { + return null; + } + + Rect rect = new Rect(framingRect); + + if(previewWidth < viewFinderViewWidth) { + rect.left = rect.left * previewWidth / viewFinderViewWidth; + rect.right = rect.right * previewWidth / viewFinderViewWidth; + } + + if(previewHeight < viewFinderViewHeight) { + rect.top = rect.top * previewHeight / viewFinderViewHeight; + rect.bottom = rect.bottom * previewHeight / viewFinderViewHeight; + } + + mFramingRectInPreview = rect; + } + return mFramingRectInPreview; + } + + public void setFlash(boolean flag) { + mFlashState = flag; + if(mCameraWrapper != null && CameraUtils.isFlashSupported(mCameraWrapper.mCamera)) { + + Camera.Parameters parameters = mCameraWrapper.mCamera.getParameters(); + if(flag) { + if(parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_TORCH)) { + return; + } + parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH); + } else { + if(parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_OFF)) { + return; + } + parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); + } + mCameraWrapper.mCamera.setParameters(parameters); + } + } + + public boolean getFlash() { + if(mCameraWrapper != null && CameraUtils.isFlashSupported(mCameraWrapper.mCamera)) { + Camera.Parameters parameters = mCameraWrapper.mCamera.getParameters(); + if(parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_TORCH)) { + return true; + } else { + return false; + } + } + return false; + } + + public void toggleFlash() { + if(mCameraWrapper != null && CameraUtils.isFlashSupported(mCameraWrapper.mCamera)) { + Camera.Parameters parameters = mCameraWrapper.mCamera.getParameters(); + if(parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_TORCH)) { + parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); + } else { + parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH); + } + mCameraWrapper.mCamera.setParameters(parameters); + } + } + + public void setAutoFocus(boolean state) { + mAutofocusState = state; + if(mPreview != null) { + mPreview.setAutoFocus(state); + } + } + + public void setShouldScaleToFill(boolean shouldScaleToFill) { + mShouldScaleToFill = shouldScaleToFill; + } + + public void setAspectTolerance(float aspectTolerance) { + mAspectTolerance = aspectTolerance; + } + + public byte[] getRotatedData(byte[] data, Camera camera) { + Camera.Parameters parameters = camera.getParameters(); + Camera.Size size = parameters.getPreviewSize(); + int width = size.width; + int height = size.height; + + int rotationCount = getRotationCount(); + + if(rotationCount == 1 || rotationCount == 3) { + for (int i = 0; i < rotationCount; i++) { + byte[] rotatedData = new byte[data.length]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) + rotatedData[x * height + height - y - 1] = data[x + y * width]; + } + data = rotatedData; + int tmp = width; + width = height; + height = tmp; + } + } + + return data; + } + + public int getRotationCount() { + int displayOrientation = mPreview.getDisplayOrientation(); + return displayOrientation / 90; + } +} + diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraHandlerThread.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraHandlerThread.java new file mode 100644 index 0000000000..2d4bcee7bf --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraHandlerThread.java @@ -0,0 +1,37 @@ +package me.dm7.barcodescanner.core; + + +import android.hardware.Camera; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; + +// This code is mostly based on the top answer here: http://stackoverflow.com/questions/18149964/best-use-of-handlerthread-over-other-similar-classes +public class CameraHandlerThread extends HandlerThread { + private static final String LOG_TAG = "CameraHandlerThread"; + + private BarcodeScannerView mScannerView; + + public CameraHandlerThread(BarcodeScannerView scannerView) { + super("CameraHandlerThread"); + mScannerView = scannerView; + start(); + } + + public void startCamera(final int cameraId) { + Handler localHandler = new Handler(getLooper()); + localHandler.post(new Runnable() { + @Override + public void run() { + final Camera camera = CameraUtils.getCameraInstance(cameraId); + Handler mainHandler = new Handler(Looper.getMainLooper()); + mainHandler.post(new Runnable() { + @Override + public void run() { + mScannerView.setupCameraPreview(CameraWrapper.getWrapper(camera, cameraId)); + } + }); + } + }); + } +} diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraPreview.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraPreview.java new file mode 100644 index 0000000000..b066e25b2c --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraPreview.java @@ -0,0 +1,312 @@ +package me.dm7.barcodescanner.core; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Point; +import android.hardware.Camera; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Display; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; + +import java.util.List; + +public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { + private static final String TAG = "CameraPreview"; + + private CameraWrapper mCameraWrapper; + private Handler mAutoFocusHandler; + private boolean mPreviewing = true; + private boolean mAutoFocus = true; + private boolean mSurfaceCreated = false; + private boolean mShouldScaleToFill = true; + private Camera.PreviewCallback mPreviewCallback; + private float mAspectTolerance = 0.1f; + + public CameraPreview(Context context, CameraWrapper cameraWrapper, Camera.PreviewCallback previewCallback) { + super(context); + init(cameraWrapper, previewCallback); + } + + public CameraPreview(Context context, AttributeSet attrs, CameraWrapper cameraWrapper, Camera.PreviewCallback previewCallback) { + super(context, attrs); + init(cameraWrapper, previewCallback); + } + + public void init(CameraWrapper cameraWrapper, Camera.PreviewCallback previewCallback) { + setCamera(cameraWrapper, previewCallback); + mAutoFocusHandler = new Handler(); + getHolder().addCallback(this); + getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + } + + public void setCamera(CameraWrapper cameraWrapper, Camera.PreviewCallback previewCallback) { + mCameraWrapper = cameraWrapper; + mPreviewCallback = previewCallback; + } + + public void setShouldScaleToFill(boolean scaleToFill) { + mShouldScaleToFill = scaleToFill; + } + + public void setAspectTolerance(float aspectTolerance) { + mAspectTolerance = aspectTolerance; + } + + @Override + public void surfaceCreated(SurfaceHolder surfaceHolder) { + mSurfaceCreated = true; + } + + @Override + public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i2, int i3) { + if(surfaceHolder.getSurface() == null) { + return; + } + stopCameraPreview(); + showCameraPreview(); + } + + @Override + public void surfaceDestroyed(SurfaceHolder surfaceHolder) { + mSurfaceCreated = false; + stopCameraPreview(); + } + + public void showCameraPreview() { + if(mCameraWrapper != null) { + try { + getHolder().addCallback(this); + mPreviewing = true; + setupCameraParameters(); + mCameraWrapper.mCamera.setPreviewDisplay(getHolder()); + mCameraWrapper.mCamera.setDisplayOrientation(getDisplayOrientation()); + mCameraWrapper.mCamera.setOneShotPreviewCallback(mPreviewCallback); + mCameraWrapper.mCamera.startPreview(); + if(mAutoFocus) { + if (mSurfaceCreated) { // check if surface created before using autofocus + safeAutoFocus(); + } else { + scheduleAutoFocus(); // wait 1 sec and then do check again + } + } + } catch (Exception e) { + Log.e(TAG, e.toString(), e); + } + } + } + + public void safeAutoFocus() { + try { + mCameraWrapper.mCamera.autoFocus(autoFocusCB); + } catch (RuntimeException re) { + // Horrible hack to deal with autofocus errors on Sony devices + // See https://github.com/dm77/barcodescanner/issues/7 for example + scheduleAutoFocus(); // wait 1 sec and then do check again + } + } + + public void stopCameraPreview() { + if(mCameraWrapper != null) { + try { + mPreviewing = false; + getHolder().removeCallback(this); + mCameraWrapper.mCamera.cancelAutoFocus(); + mCameraWrapper.mCamera.setOneShotPreviewCallback(null); + mCameraWrapper.mCamera.stopPreview(); + } catch(Exception e) { + Log.e(TAG, e.toString(), e); + } + } + } + + public void setupCameraParameters() { + Camera.Size optimalSize = getOptimalPreviewSize(); + Camera.Parameters parameters = mCameraWrapper.mCamera.getParameters(); + parameters.setPreviewSize(optimalSize.width, optimalSize.height); + mCameraWrapper.mCamera.setParameters(parameters); + adjustViewSize(optimalSize); + } + + private void adjustViewSize(Camera.Size cameraSize) { + Point previewSize = convertSizeToLandscapeOrientation(new Point(getWidth(), getHeight())); + float cameraRatio = ((float) cameraSize.width) / cameraSize.height; + float screenRatio = ((float) previewSize.x) / previewSize.y; + + if (screenRatio > cameraRatio) { + setViewSize((int) (previewSize.y * cameraRatio), previewSize.y); + } else { + setViewSize(previewSize.x, (int) (previewSize.x / cameraRatio)); + } + } + + @SuppressWarnings("SuspiciousNameCombination") + private Point convertSizeToLandscapeOrientation(Point size) { + if (getDisplayOrientation() % 180 == 0) { + return size; + } else { + return new Point(size.y, size.x); + } + } + + @SuppressWarnings("SuspiciousNameCombination") + private void setViewSize(int width, int height) { + ViewGroup.LayoutParams layoutParams = getLayoutParams(); + int tmpWidth; + int tmpHeight; + if (getDisplayOrientation() % 180 == 0) { + tmpWidth = width; + tmpHeight = height; + } else { + tmpWidth = height; + tmpHeight = width; + } + + if (mShouldScaleToFill) { + int parentWidth = ((View) getParent()).getWidth(); + int parentHeight = ((View) getParent()).getHeight(); + float ratioWidth = (float) parentWidth / (float) tmpWidth; + float ratioHeight = (float) parentHeight / (float) tmpHeight; + + float compensation; + + if (ratioWidth > ratioHeight) { + compensation = ratioWidth; + } else { + compensation = ratioHeight; + } + + tmpWidth = Math.round(tmpWidth * compensation); + tmpHeight = Math.round(tmpHeight * compensation); + } + + layoutParams.width = tmpWidth; + layoutParams.height = tmpHeight; + setLayoutParams(layoutParams); + } + + public int getDisplayOrientation() { + if (mCameraWrapper == null) { + //If we don't have a camera set there is no orientation so return dummy value + return 0; + } + + Camera.CameraInfo info = new Camera.CameraInfo(); + if(mCameraWrapper.mCameraId == -1) { + Camera.getCameraInfo(Camera.CameraInfo.CAMERA_FACING_BACK, info); + } else { + Camera.getCameraInfo(mCameraWrapper.mCameraId, info); + } + + WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + + int rotation = display.getRotation(); + int degrees = 0; + switch (rotation) { + case Surface.ROTATION_0: degrees = 0; break; + case Surface.ROTATION_90: degrees = 90; break; + case Surface.ROTATION_180: degrees = 180; break; + case Surface.ROTATION_270: degrees = 270; break; + } + + int result; + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + result = (info.orientation + degrees) % 360; + result = (360 - result) % 360; // compensate the mirror + } else { // back-facing + result = (info.orientation - degrees + 360) % 360; + } + return result; + } + + private Camera.Size getOptimalPreviewSize() { + if(mCameraWrapper == null) { + return null; + } + + List sizes = mCameraWrapper.mCamera.getParameters().getSupportedPreviewSizes(); + int w = getWidth(); + int h = getHeight(); + if (DisplayUtils.getScreenOrientation(getContext()) == Configuration.ORIENTATION_PORTRAIT) { + int portraitWidth = h; + h = w; + w = portraitWidth; + } + + double targetRatio = (double) w / h; + if (sizes == null) return null; + + Camera.Size optimalSize = null; + double minDiff = Double.MAX_VALUE; + + int targetHeight = h; + + // Try to find an size match aspect ratio and size + for (Camera.Size size : sizes) { + double ratio = (double) size.width / size.height; + if (Math.abs(ratio - targetRatio) > mAspectTolerance) continue; + if (Math.abs(size.height - targetHeight) < minDiff) { + optimalSize = size; + minDiff = Math.abs(size.height - targetHeight); + } + } + + // Cannot find the one match the aspect ratio, ignore the requirement + if (optimalSize == null) { + minDiff = Double.MAX_VALUE; + for (Camera.Size size : sizes) { + if (Math.abs(size.height - targetHeight) < minDiff) { + optimalSize = size; + minDiff = Math.abs(size.height - targetHeight); + } + } + } + return optimalSize; + } + + public void setAutoFocus(boolean state) { + if(mCameraWrapper != null && mPreviewing) { + if(state == mAutoFocus) { + return; + } + mAutoFocus = state; + if(mAutoFocus) { + if (mSurfaceCreated) { // check if surface created before using autofocus + Log.v(TAG, "Starting autofocus"); + safeAutoFocus(); + } else { + scheduleAutoFocus(); // wait 1 sec and then do check again + } + } else { + Log.v(TAG, "Cancelling autofocus"); + mCameraWrapper.mCamera.cancelAutoFocus(); + } + } + } + + private Runnable doAutoFocus = new Runnable() { + public void run() { + if(mCameraWrapper != null && mPreviewing && mAutoFocus && mSurfaceCreated) { + safeAutoFocus(); + } + } + }; + + // Mimic continuous auto-focusing + Camera.AutoFocusCallback autoFocusCB = new Camera.AutoFocusCallback() { + public void onAutoFocus(boolean success, Camera camera) { + scheduleAutoFocus(); + } + }; + + private void scheduleAutoFocus() { + mAutoFocusHandler.postDelayed(doAutoFocus, 1000); + } +} diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraUtils.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraUtils.java new file mode 100644 index 0000000000..599bd5fa48 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraUtils.java @@ -0,0 +1,63 @@ +package me.dm7.barcodescanner.core; + +import android.hardware.Camera; + +import java.util.List; + +public class CameraUtils { + /** A safe way to get an instance of the Camera object. */ + public static Camera getCameraInstance() { + return getCameraInstance(getDefaultCameraId()); + } + + /** Favor back-facing camera by default. If none exists, fallback to whatever camera is available **/ + public static int getDefaultCameraId() { + int numberOfCameras = Camera.getNumberOfCameras(); + Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + int defaultCameraId = -1; + for (int i = 0; i < numberOfCameras; i++) { + defaultCameraId = i; + Camera.getCameraInfo(i, cameraInfo); + if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) { + return i; + } + } + return defaultCameraId; + } + + /** A safe way to get an instance of the Camera object. */ + public static Camera getCameraInstance(int cameraId) { + Camera c = null; + try { + if(cameraId == -1) { + c = Camera.open(); // attempt to get a Camera instance + } else { + c = Camera.open(cameraId); // attempt to get a Camera instance + } + } + catch (Exception e) { + // Camera is not available (in use or does not exist) + } + return c; // returns null if camera is unavailable + } + + public static boolean isFlashSupported(Camera camera) { + /* Credits: Top answer at http://stackoverflow.com/a/19599365/868173 */ + if (camera != null) { + Camera.Parameters parameters = camera.getParameters(); + + if (parameters.getFlashMode() == null) { + return false; + } + + List supportedFlashModes = parameters.getSupportedFlashModes(); + if (supportedFlashModes == null || supportedFlashModes.isEmpty() || supportedFlashModes.size() == 1 && supportedFlashModes.get(0).equals(Camera.Parameters.FLASH_MODE_OFF)) { + return false; + } + } else { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraWrapper.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraWrapper.java new file mode 100644 index 0000000000..49759ae0f9 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/CameraWrapper.java @@ -0,0 +1,23 @@ +package me.dm7.barcodescanner.core; + +import android.hardware.Camera; + +import androidx.annotation.NonNull; + +public class CameraWrapper { + public final Camera mCamera; + public final int mCameraId; + + private CameraWrapper(@NonNull Camera camera, int cameraId) { + this.mCamera = camera; + this.mCameraId = cameraId; + } + + public static CameraWrapper getWrapper(Camera camera, int cameraId) { + if (camera == null) { + return null; + } else { + return new CameraWrapper(camera, cameraId); + } + } +} diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/DisplayUtils.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/DisplayUtils.java new file mode 100644 index 0000000000..6c47312221 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/DisplayUtils.java @@ -0,0 +1,41 @@ +package me.dm7.barcodescanner.core; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Point; +import android.view.Display; +import android.view.WindowManager; + +public class DisplayUtils { + public static Point getScreenResolution(Context context) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Point screenResolution = new Point(); + if (android.os.Build.VERSION.SDK_INT >= 13) { + display.getSize(screenResolution); + } else { + screenResolution.set(display.getWidth(), display.getHeight()); + } + + return screenResolution; + } + + public static int getScreenOrientation(Context context) + { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + + int orientation = Configuration.ORIENTATION_UNDEFINED; + if(display.getWidth()==display.getHeight()){ + orientation = Configuration.ORIENTATION_SQUARE; + } else{ + if(display.getWidth() < display.getHeight()){ + orientation = Configuration.ORIENTATION_PORTRAIT; + }else { + orientation = Configuration.ORIENTATION_LANDSCAPE; + } + } + return orientation; + } + +} diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/IViewFinder.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/IViewFinder.java new file mode 100644 index 0000000000..a6caaa4cb6 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/IViewFinder.java @@ -0,0 +1,53 @@ +package me.dm7.barcodescanner.core; + +import android.graphics.Rect; + +public interface IViewFinder { + + void setLaserColor(int laserColor); + void setMaskColor(int maskColor); + void setBorderColor(int borderColor); + void setBorderStrokeWidth(int borderStrokeWidth); + void setBorderLineLength(int borderLineLength); + void setLaserEnabled(boolean isLaserEnabled); + + void setBorderCornerRounded(boolean isBorderCornersRounded); + void setBorderAlpha(float alpha); + void setBorderCornerRadius(int borderCornersRadius); + void setViewFinderOffset(int offset); + void setSquareViewFinder(boolean isSquareViewFinder); + /** + * Method that executes when Camera preview is starting. + * It is recommended to update framing rect here and invalidate view after that.
+ * For example see: {@link ViewFinderView#setupViewFinder()} + */ + void setupViewFinder(); + + /** + * Provides {@link Rect} that identifies area where barcode scanner can detect visual codes + *

Note: This rect is a area representation in absolute pixel values.
+ * For example:
+ * If View's size is 1024x800 so framing rect might be 500x400

+ * + * @return {@link Rect} that identifies barcode scanner area + */ + Rect getFramingRect(); + + /** + * Width of a {@link android.view.View} that implements this interface + *

Note: this is already implemented in {@link android.view.View}, + * so you don't need to override method and provide your implementation

+ * + * @return width of a view + */ + int getWidth(); + + /** + * Height of a {@link android.view.View} that implements this interface + *

Note: this is already implemented in {@link android.view.View}, + * so you don't need to override method and provide your implementation

+ * + * @return height of a view + */ + int getHeight(); +} diff --git a/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/ViewFinderView.java b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/ViewFinderView.java new file mode 100644 index 0000000000..307a8a42b4 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/java/me/dm7/barcodescanner/core/ViewFinderView.java @@ -0,0 +1,259 @@ +package me.dm7.barcodescanner.core; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Canvas; +import android.graphics.CornerPathEffect; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +public class ViewFinderView extends View implements IViewFinder { + private static final String TAG = "ViewFinderView"; + + private Rect mFramingRect; + + private static final float PORTRAIT_WIDTH_RATIO = 6f/8; + private static final float PORTRAIT_WIDTH_HEIGHT_RATIO = 0.75f; + + private static final float LANDSCAPE_HEIGHT_RATIO = 5f/8; + private static final float LANDSCAPE_WIDTH_HEIGHT_RATIO = 1.4f; + private static final int MIN_DIMENSION_DIFF = 50; + + private static final float DEFAULT_SQUARE_DIMENSION_RATIO = 5f / 8; + + private static final int[] SCANNER_ALPHA = {0, 64, 128, 192, 255, 192, 128, 64}; + private int scannerAlpha; + private static final int POINT_SIZE = 10; + private static final long ANIMATION_DELAY = 80l; + + private final int mDefaultLaserColor = getResources().getColor(R.color.viewfinder_laser); + private final int mDefaultMaskColor = getResources().getColor(R.color.viewfinder_mask); + private final int mDefaultBorderColor = getResources().getColor(R.color.viewfinder_border); + private final int mDefaultBorderStrokeWidth = getResources().getInteger(R.integer.viewfinder_border_width); + private final int mDefaultBorderLineLength = getResources().getInteger(R.integer.viewfinder_border_length); + + protected Paint mLaserPaint; + protected Paint mFinderMaskPaint; + protected Paint mBorderPaint; + protected int mBorderLineLength; + protected boolean mSquareViewFinder; + private boolean mIsLaserEnabled; + private float mBordersAlpha; + private int mViewFinderOffset = 0; + + public ViewFinderView(Context context) { + super(context); + init(); + } + + public ViewFinderView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + init(); + } + + private void init() { + //set up laser paint + mLaserPaint = new Paint(); + mLaserPaint.setColor(mDefaultLaserColor); + mLaserPaint.setStyle(Paint.Style.FILL); + + //finder mask paint + mFinderMaskPaint = new Paint(); + mFinderMaskPaint.setColor(mDefaultMaskColor); + + //border paint + mBorderPaint = new Paint(); + mBorderPaint.setColor(mDefaultBorderColor); + mBorderPaint.setStyle(Paint.Style.STROKE); + mBorderPaint.setStrokeWidth(mDefaultBorderStrokeWidth); + mBorderPaint.setAntiAlias(true); + + mBorderLineLength = mDefaultBorderLineLength; + } + + @Override + public void setLaserColor(int laserColor) { + mLaserPaint.setColor(laserColor); + } + + @Override + public void setMaskColor(int maskColor) { + mFinderMaskPaint.setColor(maskColor); + } + + @Override + public void setBorderColor(int borderColor) { + mBorderPaint.setColor(borderColor); + } + + @Override + public void setBorderStrokeWidth(int borderStrokeWidth) { + mBorderPaint.setStrokeWidth(borderStrokeWidth); + } + + @Override + public void setBorderLineLength(int borderLineLength) { + mBorderLineLength = borderLineLength; + } + + @Override + public void setLaserEnabled(boolean isLaserEnabled) { mIsLaserEnabled = isLaserEnabled; } + + @Override + public void setBorderCornerRounded(boolean isBorderCornersRounded) { + if (isBorderCornersRounded) { + mBorderPaint.setStrokeJoin(Paint.Join.ROUND); + } else { + mBorderPaint.setStrokeJoin(Paint.Join.BEVEL); + } + } + + @Override + public void setBorderAlpha(float alpha) { + int colorAlpha = (int) (255 * alpha); + mBordersAlpha = alpha; + mBorderPaint.setAlpha(colorAlpha); + } + + @Override + public void setBorderCornerRadius(int borderCornersRadius) { + mBorderPaint.setPathEffect(new CornerPathEffect(borderCornersRadius)); + } + + @Override + public void setViewFinderOffset(int offset) { + mViewFinderOffset = offset; + } + + // TODO: Need a better way to configure this. Revisit when working on 2.0 + @Override + public void setSquareViewFinder(boolean set) { + mSquareViewFinder = set; + } + + public void setupViewFinder() { + updateFramingRect(); + invalidate(); + } + + public Rect getFramingRect() { + return mFramingRect; + } + + @Override + public void onDraw(Canvas canvas) { + if(getFramingRect() == null) { + return; + } + + drawViewFinderMask(canvas); + drawViewFinderBorder(canvas); + + if (mIsLaserEnabled) { + drawLaser(canvas); + } + } + + public void drawViewFinderMask(Canvas canvas) { + int width = canvas.getWidth(); + int height = canvas.getHeight(); + Rect framingRect = getFramingRect(); + + canvas.drawRect(0, 0, width, framingRect.top, mFinderMaskPaint); + canvas.drawRect(0, framingRect.top, framingRect.left, framingRect.bottom + 1, mFinderMaskPaint); + canvas.drawRect(framingRect.right + 1, framingRect.top, width, framingRect.bottom + 1, mFinderMaskPaint); + canvas.drawRect(0, framingRect.bottom + 1, width, height, mFinderMaskPaint); + } + + public void drawViewFinderBorder(Canvas canvas) { + Rect framingRect = getFramingRect(); + + // Top-left corner + Path path = new Path(); + path.moveTo(framingRect.left, framingRect.top + mBorderLineLength); + path.lineTo(framingRect.left, framingRect.top); + path.lineTo(framingRect.left + mBorderLineLength, framingRect.top); + canvas.drawPath(path, mBorderPaint); + + // Top-right corner + path.moveTo(framingRect.right, framingRect.top + mBorderLineLength); + path.lineTo(framingRect.right, framingRect.top); + path.lineTo(framingRect.right - mBorderLineLength, framingRect.top); + canvas.drawPath(path, mBorderPaint); + + // Bottom-right corner + path.moveTo(framingRect.right, framingRect.bottom - mBorderLineLength); + path.lineTo(framingRect.right, framingRect.bottom); + path.lineTo(framingRect.right - mBorderLineLength, framingRect.bottom); + canvas.drawPath(path, mBorderPaint); + + // Bottom-left corner + path.moveTo(framingRect.left, framingRect.bottom - mBorderLineLength); + path.lineTo(framingRect.left, framingRect.bottom); + path.lineTo(framingRect.left + mBorderLineLength, framingRect.bottom); + canvas.drawPath(path, mBorderPaint); + } + + public void drawLaser(Canvas canvas) { + Rect framingRect = getFramingRect(); + + // Draw a red "laser scanner" line through the middle to show decoding is active + mLaserPaint.setAlpha(SCANNER_ALPHA[scannerAlpha]); + scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.length; + int middle = framingRect.height() / 2 + framingRect.top; + canvas.drawRect(framingRect.left + 2, middle - 1, framingRect.right - 1, middle + 2, mLaserPaint); + + postInvalidateDelayed(ANIMATION_DELAY, + framingRect.left - POINT_SIZE, + framingRect.top - POINT_SIZE, + framingRect.right + POINT_SIZE, + framingRect.bottom + POINT_SIZE); + } + + @Override + protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld) { + updateFramingRect(); + } + + public synchronized void updateFramingRect() { + Point viewResolution = new Point(getWidth(), getHeight()); + int width; + int height; + int orientation = DisplayUtils.getScreenOrientation(getContext()); + + if(mSquareViewFinder) { + if(orientation != Configuration.ORIENTATION_PORTRAIT) { + height = (int) (getHeight() * DEFAULT_SQUARE_DIMENSION_RATIO); + width = height; + } else { + width = (int) (getWidth() * DEFAULT_SQUARE_DIMENSION_RATIO); + height = width; + } + } else { + if(orientation != Configuration.ORIENTATION_PORTRAIT) { + height = (int) (getHeight() * LANDSCAPE_HEIGHT_RATIO); + width = (int) (LANDSCAPE_WIDTH_HEIGHT_RATIO * height); + } else { + width = (int) (getWidth() * PORTRAIT_WIDTH_RATIO); + height = (int) (PORTRAIT_WIDTH_HEIGHT_RATIO * width); + } + } + + if(width > getWidth()) { + width = getWidth() - MIN_DIMENSION_DIFF; + } + + if(height > getHeight()) { + height = getHeight() - MIN_DIMENSION_DIFF; + } + + int leftOffset = (viewResolution.x - width) / 2; + int topOffset = (viewResolution.y - height) / 2; + mFramingRect = new Rect(leftOffset + mViewFinderOffset, topOffset + mViewFinderOffset, leftOffset + width - mViewFinderOffset, topOffset + height - mViewFinderOffset); + } +} + diff --git a/library/external/barcodescanner/core/src/main/res/values-hdpi/strings.xml b/library/external/barcodescanner/core/src/main/res/values-hdpi/strings.xml new file mode 100644 index 0000000000..d5979caaf8 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/res/values-hdpi/strings.xml @@ -0,0 +1,5 @@ + + + 4 + 60 + \ No newline at end of file diff --git a/library/external/barcodescanner/core/src/main/res/values-xhdpi/strings.xml b/library/external/barcodescanner/core/src/main/res/values-xhdpi/strings.xml new file mode 100644 index 0000000000..7f57b00a3a --- /dev/null +++ b/library/external/barcodescanner/core/src/main/res/values-xhdpi/strings.xml @@ -0,0 +1,5 @@ + + + 5 + 80 + \ No newline at end of file diff --git a/library/external/barcodescanner/core/src/main/res/values-xxhdpi/strings.xml b/library/external/barcodescanner/core/src/main/res/values-xxhdpi/strings.xml new file mode 100644 index 0000000000..d69e2a8b0f --- /dev/null +++ b/library/external/barcodescanner/core/src/main/res/values-xxhdpi/strings.xml @@ -0,0 +1,5 @@ + + + 6 + 100 + \ No newline at end of file diff --git a/library/external/barcodescanner/core/src/main/res/values/attrs.xml b/library/external/barcodescanner/core/src/main/res/values/attrs.xml new file mode 100644 index 0000000000..8e7485ebb3 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/res/values/attrs.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/library/external/barcodescanner/core/src/main/res/values/colors.xml b/library/external/barcodescanner/core/src/main/res/values/colors.xml new file mode 100644 index 0000000000..58c019a6a9 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #60000000 + #ffcc0000 + #ffafed44 + \ No newline at end of file diff --git a/library/external/barcodescanner/core/src/main/res/values/strings.xml b/library/external/barcodescanner/core/src/main/res/values/strings.xml new file mode 100644 index 0000000000..9cfbb9e1c4 --- /dev/null +++ b/library/external/barcodescanner/core/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + 4 + 60 + diff --git a/library/external/barcodescanner/zxing/build.gradle b/library/external/barcodescanner/zxing/build.gradle new file mode 100644 index 0000000000..e84d0f5bb9 --- /dev/null +++ b/library/external/barcodescanner/zxing/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.library' + +android { + namespace "me.dm7.barcodescanner.zxing" + + compileSdk versions.compileSdk + + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + } + + compileOptions { + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat + } +} + +dependencies { + api project(":library:external:barcodescanner:core") + // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 + api 'com.google.zxing:core:3.3.3' +} + +afterEvaluate { + tasks.findAll { it.name.startsWith("lint") }.each { + it.enabled = false + } +} diff --git a/library/external/barcodescanner/zxing/src/main/java/me/dm7/barcodescanner/zxing/ZXingScannerView.java b/library/external/barcodescanner/zxing/src/main/java/me/dm7/barcodescanner/zxing/ZXingScannerView.java new file mode 100644 index 0000000000..d1717ba5be --- /dev/null +++ b/library/external/barcodescanner/zxing/src/main/java/me/dm7/barcodescanner/zxing/ZXingScannerView.java @@ -0,0 +1,198 @@ +package me.dm7.barcodescanner.zxing; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.hardware.Camera; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.util.Log; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.LuminanceSource; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.NotFoundException; +import com.google.zxing.PlanarYUVLuminanceSource; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import me.dm7.barcodescanner.core.BarcodeScannerView; +import me.dm7.barcodescanner.core.DisplayUtils; + +public class ZXingScannerView extends BarcodeScannerView { + private static final String TAG = "ZXingScannerView"; + + public interface ResultHandler { + void handleResult(Result rawResult); + } + + private MultiFormatReader mMultiFormatReader; + public static final List ALL_FORMATS = new ArrayList<>(); + private List mFormats; + private ResultHandler mResultHandler; + + static { + ALL_FORMATS.add(BarcodeFormat.AZTEC); + ALL_FORMATS.add(BarcodeFormat.CODABAR); + ALL_FORMATS.add(BarcodeFormat.CODE_39); + ALL_FORMATS.add(BarcodeFormat.CODE_93); + ALL_FORMATS.add(BarcodeFormat.CODE_128); + ALL_FORMATS.add(BarcodeFormat.DATA_MATRIX); + ALL_FORMATS.add(BarcodeFormat.EAN_8); + ALL_FORMATS.add(BarcodeFormat.EAN_13); + ALL_FORMATS.add(BarcodeFormat.ITF); + ALL_FORMATS.add(BarcodeFormat.MAXICODE); + ALL_FORMATS.add(BarcodeFormat.PDF_417); + ALL_FORMATS.add(BarcodeFormat.QR_CODE); + ALL_FORMATS.add(BarcodeFormat.RSS_14); + ALL_FORMATS.add(BarcodeFormat.RSS_EXPANDED); + ALL_FORMATS.add(BarcodeFormat.UPC_A); + ALL_FORMATS.add(BarcodeFormat.UPC_E); + ALL_FORMATS.add(BarcodeFormat.UPC_EAN_EXTENSION); + } + + public ZXingScannerView(Context context) { + super(context); + initMultiFormatReader(); + } + + public ZXingScannerView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + initMultiFormatReader(); + } + + public void setFormats(List formats) { + mFormats = formats; + initMultiFormatReader(); + } + + public void setResultHandler(ResultHandler resultHandler) { + mResultHandler = resultHandler; + } + + public Collection getFormats() { + if(mFormats == null) { + return ALL_FORMATS; + } + return mFormats; + } + + private void initMultiFormatReader() { + Map hints = new EnumMap<>(DecodeHintType.class); + hints.put(DecodeHintType.POSSIBLE_FORMATS, getFormats()); + mMultiFormatReader = new MultiFormatReader(); + mMultiFormatReader.setHints(hints); + } + + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + if(mResultHandler == null) { + return; + } + + try { + Camera.Parameters parameters = camera.getParameters(); + Camera.Size size = parameters.getPreviewSize(); + int width = size.width; + int height = size.height; + + if (DisplayUtils.getScreenOrientation(getContext()) == Configuration.ORIENTATION_PORTRAIT) { + int rotationCount = getRotationCount(); + if (rotationCount == 1 || rotationCount == 3) { + int tmp = width; + width = height; + height = tmp; + } + data = getRotatedData(data, camera); + } + + Result rawResult = null; + PlanarYUVLuminanceSource source = buildLuminanceSource(data, width, height); + + if (source != null) { + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + try { + rawResult = mMultiFormatReader.decodeWithState(bitmap); + } catch (ReaderException re) { + // continue + } catch (NullPointerException npe) { + // This is terrible + } catch (ArrayIndexOutOfBoundsException aoe) { + + } finally { + mMultiFormatReader.reset(); + } + + if (rawResult == null) { + LuminanceSource invertedSource = source.invert(); + bitmap = new BinaryBitmap(new HybridBinarizer(invertedSource)); + try { + rawResult = mMultiFormatReader.decodeWithState(bitmap); + } catch (NotFoundException e) { + // continue + } finally { + mMultiFormatReader.reset(); + } + } + } + + final Result finalRawResult = rawResult; + + if (finalRawResult != null) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + @Override + public void run() { + // Stopping the preview can take a little long. + // So we want to set result handler to null to discard subsequent calls to + // onPreviewFrame. + ResultHandler tmpResultHandler = mResultHandler; + mResultHandler = null; + + stopCameraPreview(); + if (tmpResultHandler != null) { + tmpResultHandler.handleResult(finalRawResult); + } + } + }); + } else { + camera.setOneShotPreviewCallback(this); + } + } catch(RuntimeException e) { + // TODO: Terrible hack. It is possible that this method is invoked after camera is released. + Log.e(TAG, e.toString(), e); + } + } + + public void resumeCameraPreview(ResultHandler resultHandler) { + mResultHandler = resultHandler; + super.resumeCameraPreview(); + } + + public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) { + Rect rect = getFramingRectInPreview(width, height); + if (rect == null) { + return null; + } + // Go ahead and assume it's YUV rather than die. + PlanarYUVLuminanceSource source = null; + + try { + source = new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top, + rect.width(), rect.height(), false); + } catch(Exception e) { + } + + return source; + } +} diff --git a/settings.gradle b/settings.gradle index bccfa7913a..a0b9ce65ed 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,6 +15,8 @@ include ':library:external:textdrawable' include ':library:external:autocomplete' include ':library:external:realmfieldnameshelper' include ':library:external:span' +include ':library:external:barcodescanner:core' +include ':library:external:barcodescanner:zxing' include ':library:rustCrypto' include ':matrix-sdk-android' diff --git a/vector/build.gradle b/vector/build.gradle index e45db64548..eef4e333cf 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -263,11 +263,7 @@ dependencies { // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 implementation 'com.google.zxing:core:3.3.3' - // Excludes the legacy support library annotation usages - // https://github.com/dm77/barcodescanner/blob/d036996c8a6f36a68843ffe539c834c28944b2d5/core/src/main/java/me/dm7/barcodescanner/core/CameraWrapper.java#L4 - implementation ('me.dm7.barcodescanner:zxing:1.9.13') { - exclude group: 'com.android.support', module: 'support-v4' - } + implementation project(":library:external:barcodescanner:zxing") // Emoji Keyboard api libs.vanniktech.emojiMaterial From dc9e649703b0fe5fcdb4b82ad162df57728035cf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 19:00:06 +0200 Subject: [PATCH 09/65] Remove Jcenter repository ref (#2773) --- build.gradle | 11 ----------- dependencies_groups.gradle | 6 ------ 2 files changed, 17 deletions(-) diff --git a/build.gradle b/build.gradle index a70b54a833..edf07c097a 100644 --- a/build.gradle +++ b/build.gradle @@ -112,16 +112,6 @@ allprojects { groups.google.group.each { includeGroup it } } } - //noinspection JcenterRepositoryObsolete - // Do not use `jcenter`, it prevents Dependabot from working properly - maven { - url 'https://jcenter.bintray.com' - content { - groups.jcenter.regex.each { includeGroupByRegex it } - groups.jcenter.group.each { includeGroup it } - } - } - maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots' content { @@ -129,7 +119,6 @@ allprojects { groups.mavenSnapshots.group.each { includeGroup it } } } - } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 9e5d1e4072..c023eab2eb 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -235,10 +235,4 @@ ext.groups = [ 'xml-apis', ] ], - jcenter : [ - regex: [ - ], - group: [ - ] - ] ] From ea424f29fb67f5368b9b79606cb80241e21dcdeb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 22:58:28 +0200 Subject: [PATCH 10/65] Fix ktlint issue --- .../realmfieldnameshelper/build.gradle | 4 +- .../realmfieldnames/FieldNameFormatter.kt | 5 +-- .../dk/ilios/realmfieldnames/FileGenerator.kt | 5 --- .../RealmFieldNamesProcessor.kt | 40 ++++++++++++------- .../android/span/style/CustomTypefaceSpan.kt | 2 +- .../span/style/TextDecorationLineSpan.kt | 2 +- 6 files changed, 30 insertions(+), 28 deletions(-) diff --git a/library/external/realmfieldnameshelper/build.gradle b/library/external/realmfieldnameshelper/build.gradle index 13d8de7ca3..e051550210 100644 --- a/library/external/realmfieldnameshelper/build.gradle +++ b/library/external/realmfieldnameshelper/build.gradle @@ -1,8 +1,8 @@ apply plugin: 'kotlin' apply plugin: 'java' -sourceCompatibility = '1.7' -targetCompatibility = '1.7' +sourceCompatibility = versions.sourceCompat +targetCompatibility = versions.sourceCompat dependencies { implementation 'com.squareup:javapoet:1.13.0' diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt index fbb44d333b..80ec9f779b 100644 --- a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt @@ -14,7 +14,7 @@ class FieldNameFormatter { } // Normalize word separator chars - val normalizedFieldName : String = fieldName.replace('-', '_') + val normalizedFieldName: String = fieldName.replace('-', '_') // Iterate field name using the following rules // lowerCase m followed by upperCase anything is considered hungarian notation @@ -39,7 +39,6 @@ class FieldNameFormatter { // Hungarian notation starting with: mX result.delete(0, 1) result.appendCodePoint(currentCodepoint) - } else if (Character.isUpperCase(currentCodepoint) && Character.isUpperCase(previousCodepoint)) { // InvalidCamelCase: XXYx (should have been xxYx) if (offset + Character.charCount(currentCodepoint) < normalizedFieldName.length) { @@ -49,11 +48,9 @@ class FieldNameFormatter { } } result.appendCodePoint(currentCodepoint) - } else if (currentCodepoint === '-'.code as Int? || currentCodepoint === '_'.code as Int?) { // Word-separator: x-x or x_x result.append("_") - } else if (Character.isUpperCase(currentCodepoint) && !Character.isUpperCase(previousCodepoint) && Character.isLetterOrDigit(previousCodepoint)) { // camelCase: xX result.append("_") diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt index 02bdc70f44..eeeb5a45b8 100644 --- a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt @@ -3,9 +3,7 @@ package dk.ilios.realmfieldnames import com.squareup.javapoet.FieldSpec import com.squareup.javapoet.JavaFile import com.squareup.javapoet.TypeSpec - import java.io.IOException - import javax.annotation.processing.Filer import javax.lang.model.element.Modifier @@ -32,13 +30,11 @@ class FileGenerator(private val filer: Filer) { } private fun generateFile(classData: ClassData, classPool: Set): Boolean { - val fileBuilder = TypeSpec.classBuilder(classData.simpleClassName + "Fields") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addJavadoc("This class enumerate all queryable fields in {@link \$L.\$L}\n", classData.packageName, classData.simpleClassName) - // Add a static field reference to each queryable field in the Realm model class classData.fields.forEach { fieldName, value -> if (value != null) { @@ -69,7 +65,6 @@ class FileGenerator(private val filer: Filer) { e.printStackTrace() return false } - } private fun addField(fileBuilder: TypeSpec.Builder, fieldName: String, fieldNameValue: String) { diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt index 6f82882333..29d044c46c 100644 --- a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt @@ -1,14 +1,17 @@ package dk.ilios.realmfieldnames -import java.util.* - import javax.annotation.processing.AbstractProcessor import javax.annotation.processing.Messager import javax.annotation.processing.ProcessingEnvironment import javax.annotation.processing.RoundEnvironment import javax.annotation.processing.SupportedAnnotationTypes import javax.lang.model.SourceVersion -import javax.lang.model.element.* +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.Modifier +import javax.lang.model.element.PackageElement +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement import javax.lang.model.type.DeclaredType import javax.lang.model.type.TypeMirror import javax.lang.model.util.Elements @@ -24,9 +27,9 @@ import javax.tools.Diagnostic class RealmFieldNamesProcessor : AbstractProcessor() { private val classes = HashSet() - lateinit private var typeUtils: Types - lateinit private var messager: Messager - lateinit private var elementUtils: Elements + private lateinit var typeUtils: Types + private lateinit var messager: Messager + private lateinit var elementUtils: Elements private var ignoreAnnotation: TypeMirror? = null private var realmClassAnnotation: TypeElement? = null private var realmModelInterface: TypeMirror? = null @@ -35,7 +38,8 @@ class RealmFieldNamesProcessor : AbstractProcessor() { private var fileGenerator: FileGenerator? = null private var done = false - @Synchronized override fun init(processingEnv: ProcessingEnvironment) { + @Synchronized + override fun init(processingEnv: ProcessingEnvironment) { super.init(processingEnv) typeUtils = processingEnv.typeUtils!! messager = processingEnv.messager!! @@ -51,10 +55,14 @@ class RealmFieldNamesProcessor : AbstractProcessor() { ignoreAnnotation = elementUtils.getTypeElement("io.realm.annotations.Ignore")?.asType() realmClassAnnotation = elementUtils.getTypeElement("io.realm.annotations.RealmClass") realmModelInterface = elementUtils.getTypeElement("io.realm.RealmModel")?.asType() - realmListClass = typeUtils.getDeclaredType(elementUtils.getTypeElement("io.realm.RealmList"), - typeUtils.getWildcardType(null, null)) - realmResultsClass = typeUtils.getDeclaredType(elementUtils.getTypeElement("io.realm.RealmResults"), - typeUtils.getWildcardType(null, null)) + realmListClass = typeUtils.getDeclaredType( + elementUtils.getTypeElement("io.realm.RealmList"), + typeUtils.getWildcardType(null, null) + ) + realmResultsClass = typeUtils.getDeclaredType( + elementUtils.getTypeElement("io.realm.RealmResults"), + typeUtils.getWildcardType(null, null) + ) fileGenerator = FileGenerator(processingEnv.filer) } } @@ -83,8 +91,8 @@ class RealmFieldNamesProcessor : AbstractProcessor() { classes.forEach { it.fields.forEach { _, value -> // Analyze the library class file the first time it is encountered. - if (value != null ) { - if (classes.all{ it.qualifiedClassName != value } && !libraryClasses.containsKey(value)) { + if (value != null) { + if (classes.all { it.qualifiedClassName != value } && !libraryClasses.containsKey(value)) { libraryClasses.put(value, processLibraryClass(value)) } } @@ -172,8 +180,10 @@ class RealmFieldNamesProcessor : AbstractProcessor() { val enclosingElement = classElement.enclosingElement if (enclosingElement.kind != ElementKind.PACKAGE) { - messager.printMessage(Diagnostic.Kind.ERROR, - "Could not determine the package name. Enclosing element was: " + enclosingElement.kind) + messager.printMessage( + Diagnostic.Kind.ERROR, + "Could not determine the package name. Enclosing element was: " + enclosingElement.kind + ) return null } diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt index 57ae4548b6..c737c5bf11 100644 --- a/library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt @@ -33,4 +33,4 @@ class CustomTypefaceSpan(private val tf: Typeface) : MetricAffectingSpan() { paint.typeface = tf } -} \ No newline at end of file +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt index 662cb82fed..ce37c138f7 100644 --- a/library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt @@ -26,4 +26,4 @@ class TextDecorationLineSpan(private val textDecorationLine: String) : Character else -> throw RuntimeException("Unknown text decoration line") } } -} \ No newline at end of file +} From 2c57453efd69171995489fbc0a9d00cf85998b46 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 23:00:00 +0200 Subject: [PATCH 11/65] Fix detekt issue --- .../dk/ilios/realmfieldnames/FieldNameFormatter.kt | 13 ++++++++++--- .../dk/ilios/realmfieldnames/FileGenerator.kt | 2 +- .../me/gujun/android/span/{span.kt => Span.kt} | 0 3 files changed, 11 insertions(+), 4 deletions(-) rename library/external/span/src/main/kotlin/me/gujun/android/span/{span.kt => Span.kt} (100%) diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt index 80ec9f779b..95f0024721 100644 --- a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt @@ -8,7 +8,8 @@ import java.util.Locale */ class FieldNameFormatter { - @JvmOverloads fun format(fieldName: String?, locale: Locale = Locale.US): String { + @JvmOverloads + fun format(fieldName: String?, locale: Locale = Locale.US): String { if (fieldName == null || fieldName == "") { return "" } @@ -35,7 +36,11 @@ class FieldNameFormatter { currentCodepoint = normalizedFieldName.codePointAt(offset) if (previousCodepoint != null) { - if (Character.isUpperCase(currentCodepoint) && !Character.isUpperCase(previousCodepoint) && previousCodepoint === 'm'.code as Int? && result.length == 1) { + if (Character.isUpperCase(currentCodepoint) && + !Character.isUpperCase(previousCodepoint) && + previousCodepoint === 'm'.code as Int? && + result.length == 1 + ) { // Hungarian notation starting with: mX result.delete(0, 1) result.appendCodePoint(currentCodepoint) @@ -51,7 +56,9 @@ class FieldNameFormatter { } else if (currentCodepoint === '-'.code as Int? || currentCodepoint === '_'.code as Int?) { // Word-separator: x-x or x_x result.append("_") - } else if (Character.isUpperCase(currentCodepoint) && !Character.isUpperCase(previousCodepoint) && Character.isLetterOrDigit(previousCodepoint)) { + } else if (Character.isUpperCase(currentCodepoint) && !Character.isUpperCase(previousCodepoint) && Character.isLetterOrDigit( + previousCodepoint + )) { // camelCase: xX result.append("_") result.appendCodePoint(currentCodepoint) diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt index eeeb5a45b8..2ddba1ccbd 100644 --- a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt @@ -62,7 +62,7 @@ class FileGenerator(private val filer: Filer) { javaFile.writeTo(filer) return true } catch (e: IOException) { - e.printStackTrace() + // e.printStackTrace() return false } } diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/span.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/Span.kt similarity index 100% rename from library/external/span/src/main/kotlin/me/gujun/android/span/span.kt rename to library/external/span/src/main/kotlin/me/gujun/android/span/Span.kt From 9b63293e45eb9d5def78ce7886e237b0ef715c46 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 23:13:33 +0200 Subject: [PATCH 12/65] Rename files to avoid danger issue. --- .../src/main/res/values-hdpi/{strings.xml => integers.xml} | 0 .../src/main/res/values-xhdpi/{strings.xml => integers.xml} | 0 .../src/main/res/values-xxhdpi/{strings.xml => integers.xml} | 0 .../core/src/main/res/values/{strings.xml => integers.xml} | 3 ++- 4 files changed, 2 insertions(+), 1 deletion(-) rename library/external/barcodescanner/core/src/main/res/values-hdpi/{strings.xml => integers.xml} (100%) rename library/external/barcodescanner/core/src/main/res/values-xhdpi/{strings.xml => integers.xml} (100%) rename library/external/barcodescanner/core/src/main/res/values-xxhdpi/{strings.xml => integers.xml} (100%) rename library/external/barcodescanner/core/src/main/res/values/{strings.xml => integers.xml} (71%) diff --git a/library/external/barcodescanner/core/src/main/res/values-hdpi/strings.xml b/library/external/barcodescanner/core/src/main/res/values-hdpi/integers.xml similarity index 100% rename from library/external/barcodescanner/core/src/main/res/values-hdpi/strings.xml rename to library/external/barcodescanner/core/src/main/res/values-hdpi/integers.xml diff --git a/library/external/barcodescanner/core/src/main/res/values-xhdpi/strings.xml b/library/external/barcodescanner/core/src/main/res/values-xhdpi/integers.xml similarity index 100% rename from library/external/barcodescanner/core/src/main/res/values-xhdpi/strings.xml rename to library/external/barcodescanner/core/src/main/res/values-xhdpi/integers.xml diff --git a/library/external/barcodescanner/core/src/main/res/values-xxhdpi/strings.xml b/library/external/barcodescanner/core/src/main/res/values-xxhdpi/integers.xml similarity index 100% rename from library/external/barcodescanner/core/src/main/res/values-xxhdpi/strings.xml rename to library/external/barcodescanner/core/src/main/res/values-xxhdpi/integers.xml diff --git a/library/external/barcodescanner/core/src/main/res/values/strings.xml b/library/external/barcodescanner/core/src/main/res/values/integers.xml similarity index 71% rename from library/external/barcodescanner/core/src/main/res/values/strings.xml rename to library/external/barcodescanner/core/src/main/res/values/integers.xml index 9cfbb9e1c4..690b6cd3c9 100644 --- a/library/external/barcodescanner/core/src/main/res/values/strings.xml +++ b/library/external/barcodescanner/core/src/main/res/values/integers.xml @@ -1,4 +1,5 @@ - + + 4 60 From 7940584674eea326304f1fd21abd078494b940e0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 23:20:30 +0200 Subject: [PATCH 13/65] Changelog. --- changelog.d/8556.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8556.misc diff --git a/changelog.d/8556.misc b/changelog.d/8556.misc new file mode 100644 index 0000000000..42ff3f60fe --- /dev/null +++ b/changelog.d/8556.misc @@ -0,0 +1 @@ +Include some source code in our project to remove our dependency to artifact hosted by bintray (Jcenter). From 61b05edd9e2098e99095f203dbb0dd2dec13602d Mon Sep 17 00:00:00 2001 From: FIONover Date: Mon, 26 Jun 2023 18:11:59 +0000 Subject: [PATCH 14/65] Translated using Weblate (Armenian) Currently translated at 1.0% (1 of 100 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/hy/ --- fastlane/metadata/android/hy/short_description.txt | 1 + fastlane/metadata/android/hy/title.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/hy/short_description.txt create mode 100644 fastlane/metadata/android/hy/title.txt diff --git a/fastlane/metadata/android/hy/short_description.txt b/fastlane/metadata/android/hy/short_description.txt new file mode 100644 index 0000000000..3b02739117 --- /dev/null +++ b/fastlane/metadata/android/hy/short_description.txt @@ -0,0 +1 @@ +Խմբային մեսինջեր - գաղտնագրված շփում, խմբային չատեր և վիդեո զանգեր diff --git a/fastlane/metadata/android/hy/title.txt b/fastlane/metadata/android/hy/title.txt new file mode 100644 index 0000000000..2f3fd83a51 --- /dev/null +++ b/fastlane/metadata/android/hy/title.txt @@ -0,0 +1 @@ +Element - Անվտանգ Մեսինջեր From 7b8cf5d917725c52bd6fb036f117bc8b20ef4224 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 27 Jun 2023 13:30:17 +0200 Subject: [PATCH 15/65] version++ --- matrix-sdk-android/build.gradle | 2 +- vector-app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index b9e9a71d75..722b4fc0b3 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -63,7 +63,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.6.3\"" + buildConfigField "String", "SDK_VERSION", "\"1.6.5\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 7a09213bb9..10fe0c7525 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 6 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 3 +ext.versionPatch = 5 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' From cf6de0948309dce649608cf09f0fcca3d0dbbdb6 Mon Sep 17 00:00:00 2001 From: Vri Date: Wed, 28 Jun 2023 04:09:41 +0000 Subject: [PATCH 16/65] Translated using Weblate (German) Currently translated at 100.0% (2640 of 2640 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- library/ui-strings/src/main/res/values-de/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml index 334a1f45be..0871842a4d 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -2957,4 +2957,12 @@ App aktualisiert Dein Heim-Server ist nicht erreichbar. Falls du dich dennoch abmeldest, wird dieses Gerät nicht von deiner Geräteliste entfernt, also müsstest du dies mit einer anderen Sitzung selbst machen. Dennoch abmelden + Dennoch Unterhaltung beginnen + Konnte keine Profile für die folgenden Matrix-IDs finden. Möchtest du dennoch eine Unterhaltung beginnen\? +\n +\n%s + Konnte keine Profile für die folgenden Matrix-IDs finden. Möchtest du sie dennoch einladen\? +\n +\n%s + Dennoch einladen \ No newline at end of file From 7080ee1c2651800032af7e63f8251a3a10f0941b Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Thu, 29 Jun 2023 07:17:41 +0000 Subject: [PATCH 17/65] Translated using Weblate (Latvian) Currently translated at 68.0% (1795 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/lv/ --- library/ui-strings/src/main/res/values-lv/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/ui-strings/src/main/res/values-lv/strings.xml b/library/ui-strings/src/main/res/values-lv/strings.xml index 81a7a10dcc..ad70dbafbc 100644 --- a/library/ui-strings/src/main/res/values-lv/strings.xml +++ b/library/ui-strings/src/main/res/values-lv/strings.xml @@ -477,9 +477,9 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.
Visas istabas %s serverī Visas vietējās %s istabas - %d nelasītu paziņotu ziņu - %d nelasīta paziņota ziņa - %d nelasītas paziņotas ziņu + %d paziņojumu par nelasītām ziņām + %d paziņojums par nelasītām ziņām + %d paziņojumi par nelasītām ziņām %d istaba From 05ce2cab2708733d12f6be8074babcf474a0eb3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C4=83n=20Huy=20D=C6=B0=C6=A1ng?= Date: Thu, 29 Jun 2023 06:10:11 +0000 Subject: [PATCH 18/65] Translated using Weblate (Vietnamese) Currently translated at 88.0% (2320 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/vi/ --- .../src/main/res/values-vi/strings.xml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/library/ui-strings/src/main/res/values-vi/strings.xml b/library/ui-strings/src/main/res/values-vi/strings.xml index 38da26bd3d..0a252ed7da 100644 --- a/library/ui-strings/src/main/res/values-vi/strings.xml +++ b/library/ui-strings/src/main/res/values-vi/strings.xml @@ -2531,4 +2531,24 @@ Ẩn địa chỉ Internet (IP) Phiên đã xác thực Hệ điều hành + Bộ lọc + Bộ lọc các chủ đề trong phòng + Sao chép liên kết vào cuộc trò chuyện + Các chủ đề + Bản Beta của Chủ đề + Bản Beta của Chủ đề + Các chủ đề đã chuyển sang Beta 🎉 + Hiển thị tất cả chủ đề trong phòng + Tất cả chủ đề + Sắp xếp các cuộc thảo luận với các chủ đề + Chủ đề + Hiển thị tất cả chủ đề bạn đã tham gia + Element đã được đơn giản hóa với các mục tùy chọn + Chỉ tạo cuộc trò chuyện riêng cho tin nhắn đầu tiên + Bật trì hoãn cho các tin nhắn riêng + Các chủ đề của tôi + Từ một chủ đề + \'\'Chủ đề\'\' giúp giữ cho các cuộc trò chuyện của bạn theo chủ đề và dễ theo dõi. + Máy chủ cục bộ của bạn chưa hỗ trợ cho việc liệt kê các chủ đề + \'\'Chủ đề\'\' giúp giữ cho các cuộc trò chuyện của bạn theo chủ đề và dễ theo dõi. %sBật chủ đề sẽ làm mới ứng dụng. Quá trình này có thể mất nhiều thời gian hơn đối với một số tài khoản. \ No newline at end of file From 0a4bdceff8970bb12d43a1cdd143c1872b679a53 Mon Sep 17 00:00:00 2001 From: Someone Date: Thu, 29 Jun 2023 10:41:20 +0000 Subject: [PATCH 19/65] Translated using Weblate (Vietnamese) Currently translated at 88.0% (2320 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/vi/ --- library/ui-strings/src/main/res/values-vi/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-vi/strings.xml b/library/ui-strings/src/main/res/values-vi/strings.xml index 0a252ed7da..3b7a299fee 100644 --- a/library/ui-strings/src/main/res/values-vi/strings.xml +++ b/library/ui-strings/src/main/res/values-vi/strings.xml @@ -2549,6 +2549,6 @@ Các chủ đề của tôi Từ một chủ đề \'\'Chủ đề\'\' giúp giữ cho các cuộc trò chuyện của bạn theo chủ đề và dễ theo dõi. - Máy chủ cục bộ của bạn chưa hỗ trợ cho việc liệt kê các chủ đề + Máy chủ nhà của bạn chưa hỗ trợ cho việc liệt kê các chủ đề. \'\'Chủ đề\'\' giúp giữ cho các cuộc trò chuyện của bạn theo chủ đề và dễ theo dõi. %sBật chủ đề sẽ làm mới ứng dụng. Quá trình này có thể mất nhiều thời gian hơn đối với một số tài khoản. \ No newline at end of file From e4e17d865b4fd4bbcbf5453be0b9a989b2232bc0 Mon Sep 17 00:00:00 2001 From: Someone Date: Fri, 30 Jun 2023 10:24:14 +0000 Subject: [PATCH 20/65] Translated using Weblate (Vietnamese) Currently translated at 56.0% (56 of 100 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/vi/ --- .../metadata/android/vi/full_description.txt | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/fastlane/metadata/android/vi/full_description.txt b/fastlane/metadata/android/vi/full_description.txt index 282914188b..b54e04e1c8 100644 --- a/fastlane/metadata/android/vi/full_description.txt +++ b/fastlane/metadata/android/vi/full_description.txt @@ -1,42 +1,42 @@ Element vừa là một ứng dụng nhắn tin bảo mật vừa là một ứng dụng cộng tác nhóm năng suất, lý tưởng cho các cuộc trò chuyện nhóm khi làm việc từ xa. Ứng dụng trò chuyện này sử dụng mã hóa đầu cuối để cung cấp tính năng hội thảo truyền hình, chia sẻ tệp và cuộc gọi thoại mạnh mẽ. - Các tính năng của Element bao gồm: -- Các công cụ giao tiếp trực tuyến tiên tiến +Các tính năng của Element bao gồm: +- Công cụ giao tiếp trực tuyến nâng cao - Các tin nhắn được mã hóa hoàn toàn để cho phép liên lạc doanh nghiệp an toàn hơn, ngay cả đối với những người làm việc từ xa - Trò chuyện phi tập trung dựa trên khung mã nguồn mở Matrix -- Chia sẻ tệp một cách an toàn với dữ liệu được mã hóa trong khi quản lý dự án -- Trò chuyện video với gọi thoại qua giao thức Internet (IP - VoIP) và chia sẻ màn hình +- Chia sẻ tệp bảo mật với dữ liệu được mã hóa trong khi quản lý dự án +- Trò chuyện video với gọi thoại qua giao thức Internet (VoIP) và chia sẻ màn hình - Tích hợp dễ dàng với các công cụ cộng tác trực tuyến yêu thích của bạn, công cụ quản lý dự án, dịch vụ VoIP và các ứng dụng nhắn tin nhóm khác -Element hoàn toàn khác với các ứng dụng nhắn tin và cộng tác khác. Hoạt động trên Matrix, một mạng mở để nhắn tin bảo mật và giao tiếp phi tập trung. Đồng thời, cho phép tự lưu trữ để cung cấp cho người dùng quyền sở hữu và kiểm soát tối đa dữ liệu và tin nhắn của họ. +Element hoàn toàn khác với các ứng dụng nhắn tin và cộng tác khác. Hoạt động trên Matrix, một mạng mở để nhắn tin bảo mật và giao tiếp phi tập trung. Đồng thời, cho phép tự vận hành để cung cấp cho người dùng quyền sở hữu và kiểm soát tối đa dữ liệu và tin nhắn của họ. - Nhắn tin mã hóa và riêng tư -Element bảo vệ bạn khỏi các quảng cáo không mong muốn, khai thác dữ liệu và kiểm soát khu vực. Element cũng bảo mật tất cả dữ liệu của bạn, video 1-1 và giao tiếp thoại thông qua mã hóa đầu cuối và xác minh thiết bị có chữ ký chéo. +Nhắn tin mã hóa và riêng tư +Element bảo vệ bạn khỏi các quảng cáo không mong muốn, khai thác dữ liệu và kiểm soát khu vực. Element cũng bảo mật tất cả dữ liệu của bạn, truyền hình 1-1 và giao tiếp thoại thông qua mã hóa đầu cuối và xác thực chéo thiết bị. -Element cung cấp cho bạn quyền kiểm soát quyền riêng tư của mình đồng thời cho phép bạn giao tiếp an toàn với bất kỳ ai trên mạng Matrix hoặc các công cụ cộng tác kinh doanh khác bằng cách tích hợp với các ứng dụng như Slack. +Element cung cấp cho bạn quyền kiểm soát quyền riêng tư của mình đồng thời cho phép bạn giao tiếp bảo mật với bất kỳ ai trên mạng Matrix hoặc các công cụ cộng tác kinh doanh khác bằng cách tích hợp với các ứng dụng như Slack. - Element có thể được tự lưu trữ -Để cho phép kiểm soát nhiều hơn dữ liệu nhạy cảm và các cuộc trò chuyện của bạn, Element có thể được tự lưu trữ hoặc bạn có thể chọn bất kỳ máy chủ Matrix nào - tiêu chuẩn cho giao tiếp phi tập trung, mã nguồn mở. Element cung cấp cho bạn quyền riêng tư, tuân thủ bảo mật và tính linh hoạt trong tích hợp. +Element có thể được tự vận hành +Để cho phép kiểm soát nhiều hơn dữ liệu nhạy cảm và các cuộc trò chuyện của bạn, Element có thể được tự vận hành hoặc bạn có thể chọn bất kỳ máy chủ Matrix nào - tiêu chuẩn cho giao tiếp phi tập trung, mã nguồn mở. Element cung cấp cho bạn quyền riêng tư, tuân thủ bảo mật và tính linh hoạt trong tích hợp. - Sở hữu dữ liệu của bạn -Bạn quyết định nơi lưu giữ dữ liệu và tin nhắn của mình. Không có rủi ro khai thác dữ liệu hoặc truy cập từ bên thứ ba. +Sở hữu dữ liệu của bạn +Bạn quyết định nơi lưu trữ dữ liệu và tin nhắn của mình. Không có rủi ro khai thác dữ liệu hoặc truy cập từ bên thứ ba. Element giúp bạn kiểm soát theo những cách khác nhau: -1. Tạo một tài khoản miễn phí trên máy chủ công cộng matrix.org do các nhà phát triển Matrix vận hành hoặc chọn từ hàng nghìn máy chủ công cộng do các tình nguyện viên lưu trữ +1. Tạo một tài khoản miễn phí trên máy chủ công cộng matrix.org do các nhà phát triển Matrix vận hành hoặc chọn từ hàng nghìn máy chủ công cộng do các tình nguyện viên vận hành 2. Tự lưu trữ tài khoản của bạn bằng cách chạy một máy chủ trên cơ sở hạ tầng CNTT của riêng bạn 3. Đăng ký tài khoản trên máy chủ tùy chỉnh bằng cách chỉ cần đăng ký nền tảng Element Matrix Services hosting - Nhắn tin và cộng tác mở +Nhắn tin và cộng tác mở Bạn có thể trò chuyện với bất kỳ ai trên mạng Matrix, cho dù họ đang sử dụng Element, một ứng dụng Matrix khác hay ngay cả khi họ đang sử dụng một ứng dụng nhắn tin khác. - Siêu bảo mật -Mã hóa đầu-cuối thực (chỉ những người trong cuộc trò chuyện mới có thể giải mã tin nhắn) và xác minh thiết bị xác thực chéo. +Siêu bảo mật +Mã hóa đầu-cuối có thực (chỉ những người trong cuộc trò chuyện mới có thể giải mã tin nhắn) và xác thực chéo thiết bị. - Giao tiếp và tích hợp hoàn chỉnh -Nhắn tin, cuộc gọi thoại và video, chia sẻ tệp, chia sẻ màn hình và một loạt các tích hợp, bot và widget. Xây dựng phòng, cộng đồng, giữ liên lạc và hoàn thành công việc. +Giao tiếp và tích hợp hoàn chỉnh +Nhắn tin, gọi thoại và truyền hình, chia sẻ tệp, chia sẻ màn hình và một loạt các tích hợp, bot và widget. Tạo phòng, cộng đồng, giữ liên lạc và hoàn thành công việc. - Tiếp tục nơi bạn đã dừng lại +Tiếp tục nơi bạn đã dừng lạ Giữ liên lạc mọi lúc mọi nơi với lịch sử tin nhắn được đồng bộ hóa hoàn toàn trên tất cả các thiết bị của bạn và trên web tại https://app.element.io - Mã nguồn mở -Element Android là một dự án mã nguồn mở, được lưu trữ trên GitHub. Vui lòng báo cáo lỗi và / hoặc đóng góp phát triển tại https://github.com/vector-im/element-android +Mã nguồn mở +Element Android là một dự án mã nguồn mở, được lưu trữ trên GitHub. Vui lòng báo cáo lỗi và / hoặc đóng góp, phát triển tại https://github.com/vector-im/element-android From 6fcd582f2d3e00924ee5de151a9f2416f63e1459 Mon Sep 17 00:00:00 2001 From: Someone Date: Fri, 30 Jun 2023 10:36:28 +0000 Subject: [PATCH 21/65] Translated using Weblate (Vietnamese) Currently translated at 88.0% (2320 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/vi/ --- .../src/main/res/values-vi/strings.xml | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/library/ui-strings/src/main/res/values-vi/strings.xml b/library/ui-strings/src/main/res/values-vi/strings.xml index 3b7a299fee..c35964ee3c 100644 --- a/library/ui-strings/src/main/res/values-vi/strings.xml +++ b/library/ui-strings/src/main/res/values-vi/strings.xml @@ -359,9 +359,9 @@ Nhập các mã khoá phòng E2E Quản lý bản sao lưu mã khoá ip không xác định - Nếu chúng không khớp, sự bảo mật của việc giao tiếp của bạn có thể bị can thiệp. - Xác nhận bằng cách so sánh những điều sau đây với Cài đặt người dùng trong phiên làm việc kia của bạn: - Xác minh + Nếu chúng không khớp, bảo mật của việc giao tiếp của bạn có thể bị can thiệp. + Xác thực bằng cách so sánh những thứ sau với Cài đặt người dùng trong phiên làm việc kia của bạn: + Xác thực URL máy chủ nhà Bạn có chắc bạn muốn rời khỏi phòng không\? Rời khỏi phòng @@ -1027,7 +1027,7 @@ Chọn vai trò được yêu cầu để thay đổi thiết lập của phòng Quyền hạn Việc này có thể có nghĩa là ai đó đang can thiệp vào lưu lượng của bạn, hoặc điện thoại của bạn không tin cậy chứng chỉ được máy chủ trên mạng cung cấp. - Không thể xác minh danh tính của máy chủ trên mạng. + Không thể xác thực danh tính của máy chủ trên mạng. Mã kiểm tra (%s): Bỏ qua Đăng xuất @@ -1148,7 +1148,7 @@ Các lời mời đã gửi tới %1$s và %2$d người nữa - Hiển thị %d thiết bị bạn có thể xác minh ngay bây giờ + Hiển thị %d thiết bị bạn có thể xác thực ngay bây giờ %d phiên đang hoạt động @@ -1466,11 +1466,11 @@ Vui lòng chọn tên người dùng. Thất bại trong việc thiết lập Xác thực chéo Xác nhận danh tính của bạn bằng cách xác minh đăng nhập này, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. - Xác nhận danh tính của bạn bằng cách xác minh đăng nhập này từ một trong các phiên khác của bạn, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. - Xác minh tương tác bằng Emoji + Xác thực danh tính của bạn bằng cách xác nhận đăng nhập này từ một trong các phiên khác của bạn, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. + Xác thực tương tác bằng Emoji Xác minh đăng nhập - Xác minh thủ công bằng Văn bản - Xác minh thông tin đăng nhập mới truy cập vào tài khoản của bạn: %1$s + Xác thực thủ công bằng Văn bản + Xác thực thông tin đăng nhập mới truy cập vào tài khoản của bạn: %1$s Được mã hóa bởi một thiết bị chưa được xác minh Không được mã hóa gửi tuyết rơi ❄️ @@ -1479,7 +1479,7 @@ Gửi tin nhắn đã cho với hoa giấy Bạn sẽ khởi động lại mà không có lịch sử, không có tin nhắn, thiết bị đáng tin cậy hoặc người dùng đáng tin cậy Nếu bạn đặt lại mọi thứ - Chỉ làm điều này nếu bạn không có thiết bị nào khác mà bạn có thể xác minh thiết bị này. + Chỉ làm điều này nếu bạn không có thiết bị nào khác mà bạn có thể xác thực thiết bị này. Đặt lại mọi thứ Quên hoặc mất tất cả các tùy chọn phục hồi\? Đặt lại mọi thứ Không truy nhập được dung lượng lưu trữ an toàn @@ -1514,7 +1514,7 @@ Nó không phải là một khóa phục hồi hợp lệ Sử dụng Tệp Nhập %s của bạn để tiếp tục - Xác minh bản thân và người khác để giữ an toàn cho cuộc trò chuyện của bạn + Xác thực bản thân và người khác để giữ an toàn cho cuộc trò chuyện của bạn Nâng cấp mã hóa có sẵn Tin nhắn… Tài khoản này đã bị vô hiệu hóa. @@ -1560,7 +1560,7 @@ Mã hóa được sử dụng bởi phòng này không được hỗ trợ Mã hóa không được bật Tin nhắn trong phòng này được mã hóa đầu cuối. - Tin nhắn trong phòng này được mã hóa đầu cuối. Tìm hiểu thêm và xác minh người dùng trong hồ sơ của họ. + Tin nhắn trong phòng này được mã hóa đầu cuối. Tìm hiểu thêm và xác thực người dùng trong hồ sơ của họ. Mã hóa được bật Nếu bạn hủy ngay bây giờ, bạn có thể mất tin nhắn và dữ liệu được mã hóa nếu bạn mất quyền truy cập vào thông tin đăng nhập của mình. \n @@ -1597,7 +1597,7 @@ So sánh mã với mã được hiển thị trên màn hình của người dùng khác. So sánh biểu tượng cảm xúc độc đáo, đảm bảo chúng xuất hiện theo cùng một thứ tự. Để được an toàn, hãy làm điều này trực tiếp hoặc sử dụng một cách khác để giao tiếp. - Để an toàn, hãy xác minh %s bằng cách kiểm tra mã một lần. + Để an toàn, hãy xác thực %s bằng cách kiểm tra mã một lần. Bật mã hóa Sau khi được bật, mã hóa cho một căn phòng không thể bị vô hiệu hóa. Tin nhắn được gửi trong một căn phòng được mã hóa không thể được nhìn thấy bởi máy chủ, chỉ bởi những người tham gia của căn phòng. Cho phép mã hóa có thể ngăn nhiều bot và cầu hoạt động chính xác. Bật mã hóa\? @@ -1607,8 +1607,8 @@ Dòng thời gian Gửi emote đã cho màu cầu vồng Gửi tin nhắn đã cho có màu cầu vồng - Phiên này không thể chia sẻ xác minh này với các phiên khác của bạn. -\nViệc xác minh sẽ được lưu cục bộ và chia sẻ trong phiên bản tương lai của ứng dụng. + Phiên này không thể chia sẻ xác thực này với các phiên khác của bạn. +\nViệc xác thực sẽ được lưu cục bộ và chia sẻ trong phiên bản tương lai của ứng dụng. Hủy bỏ qua ${app_name} gặp sự cố khi hiển thị nội dung sự kiện với id \'%1$s\' ${app_name} không xử lý các sự kiện thuộc loại \'%1$s\' @@ -1644,16 +1644,16 @@ Tin nhắn trong phòng này không được mã hóa đầu cuối. Đang chờ %s… Đã xác minh %s - Xác minh %s - Xác minh bằng cách so sánh biểu tượng cảm xúc - Thay vào đó, xác minh bằng cách so sánh biểu tượng cảm xúc + Xác thực %s + Xác thực bằng cách so sánh biểu tượng cảm xúc + Thay vào đó, xác thực bằng cách so sánh biểu tượng cảm xúc Nếu bạn không trực tiếp, hãy so sánh biểu tượng cảm xúc thay thế Không thể quét Quét bằng thiết bị này Quét mã của họ Quét mã bằng thiết bị khác của bạn hoặc chuyển đổi và quét bằng thiết bị này - Quét mã bằng thiết bị của người dùng khác để xác minh lẫn nhau một cách an toàn - Xác minh phiên này + Quét mã bằng thiết bị của người dùng khác để xác thực lẫn nhau một cách an toàn + Xác thực phiên này Yêu cầu xác minh Xác minh đã gửi Bạn đã chấp nhận @@ -1672,8 +1672,8 @@ Video. Một trong những điều sau đây có thể bị xâm phạm: \n -\n - Homeserver của bạn -\n - Homeserver mà bạn đang xác minh được kết nối với +\n - Máy chủ nhà của bạn +\n - Máy chủ nhà mà bạn đang xác minh được kết nối với \n - Kết nối internet của bạn hoặc của người dùng khác \n - Thiết bị của bạn hoặc thiết bị của người dùng khác Không bảo mật @@ -1862,7 +1862,7 @@ Có vẻ như bạn đang cố gắng kết nối với một homeserver khác. Bạn có muốn đăng xuất không\? Bạn không sử dụng bất kỳ máy chủ xác thực nào Lỗi không xác định - %s muốn xác minh phiên của bạn + %s muốn xác thực phiên của bạn Yêu cầu xác minh Đã nhận được Đã xác minh! @@ -1968,17 +1968,17 @@ Một trong những điều sau đây có thể bị xâm phạm: \n \n- Mật khẩu của bạn -\n- Người ở nhà của anh -\n- Thiết bị này hoặc thiết bị khác +\n- Máy chủ nhà của bạn +\n- Thiết bị này hoặc thiết bị kia \n- Kết nối internet mà một trong hai thiết bị đang sử dụng \n \nChúng tôi khuyên bạn nên thay đổi mật khẩu và khóa khôi phục trong Cài đặt ngay lập tức. - Bạn sẽ không xác minh %1$s (%2$s) nếu bạn hủy ngay. Bắt đầu lại trong hồ sơ người dùng của họ. + Bạn sẽ không xác thực %1$s (%2$s) nếu bạn hủy ngay. Bắt đầu lại trong hồ sơ người dùng của họ. Nếu bạn hủy, bạn sẽ không thể đọc tin nhắn được mã hóa trên thiết bị mới của mình và những người dùng khác sẽ không tin tưởng nó Nếu bạn hủy, bạn sẽ không thể đọc tin nhắn được mã hóa trên thiết bị này và những người dùng khác sẽ không tin tưởng nó Tài khoản của bạn có thể bị xâm phạm Không phải tôi - Sử dụng phiên này để xác minh phiên mới của bạn, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. + Sử dụng phiên này để xác thực phiên mới của bạn, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. Đăng nhập mới. Đây có phải là bạn không\? Làm tươi Mở khóa lịch sử tin nhắn được mã hóa @@ -2011,7 +2011,7 @@ Mã QR Đặt lại khóa Khởi tạo xác thực chéo - Cho đến khi người dùng này tin tưởng phiên này, tin nhắn được gửi đến nó và từ nó đều mang nhãn cảnh báo. Ngoài ra, bạn có thể xác minh thủ công. + Cho đến khi người dùng này tin tưởng phiên này, tin nhắn được gửi đến nó và từ nó đều mang nhãn cảnh báo. Ngoài ra, bạn có thể xác thực thủ công. %1$s (%2$s) đã đăng nhập bằng phiên mới: Phiên này được tin cậy để nhắn tin an toàn vì %1$s (%2$s) đã xác minh: Không tin cậy @@ -2020,10 +2020,10 @@ Không nhận được phiên Cảnh báo Đã xác minh - Xác minh - Sử dụng phiên hiện có để xác minh phiên này, cấp cho nó quyền truy cập vào các thư được mã hóa. - Xác minh đăng nhập này - Xác minh phiên này để đánh dấu nó là đáng tin cậy và cấp cho nó quyền truy cập vào các thư được mã hóa. Nếu bạn không đăng nhập vào phiên này, tài khoản của bạn có thể bị xâm phạm: + Xác thực + Sử dụng phiên hiện có để xác thực phiên này, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. + Xác thực thiết bị này + Xác thực phiên này để đánh dấu nó là đáng tin cậy và cấp cho nó quyền truy cập vào các thư được mã hóa. Nếu bạn không đăng nhập vào phiên này, tài khoản của bạn có thể bị xâm phạm: Phiên này được tin cậy để nhắn tin an toàn vì bạn đã xác minh nó: Không có thông tin mật mã sẵn dùng bất ổn định @@ -2463,7 +2463,7 @@ Chọn máy chủ của bạn Bạn nắm quyền kiểm soát. Muốn chạy một máy chủ của mình\? - %s cần xác nhận tài khoản của bạn + %s cần xác thực tài khoản của bạn Đi thôi Bạn sẽ trò chuyện với ai nhiều nhất\? Chưa chắc chắn\? %s @@ -2477,7 +2477,7 @@ Chính sách máy chủ %s sẽ gửi bạn một liên kết xác nhận Làm chủ cuộc trò chuyện. - %s cần xác nhận tài khoản của bạn + %s cần xác thực tài khoản của bạn Chào mừng trở lại! Địa chỉ máy chủ của bạn là gì\? Chúng tôi sẽ giúp bạn kết nối From bbcea97120bdc263088bdd0cfe43bc7cc90a669c Mon Sep 17 00:00:00 2001 From: Kat Gerasimova Date: Fri, 30 Jun 2023 15:18:15 +0100 Subject: [PATCH 22/65] Send new issues to new triage board (#8567) Issues should go to the V2 triage board --- .github/workflows/triage-incoming.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml index 56bad8a2d7..4f9c239429 100644 --- a/.github/workflows/triage-incoming.yml +++ b/.github/workflows/triage-incoming.yml @@ -15,3 +15,11 @@ jobs: project: Issue triage column: Incoming repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} + + triage-new-issues: + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@main + with: + project-url: https://github.com/orgs/vector-im/projects/91 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} From 0c9ebfdab67e0180103d3ac7f4b7118d5c5b62c1 Mon Sep 17 00:00:00 2001 From: Someone Date: Sat, 1 Jul 2023 08:13:43 +0000 Subject: [PATCH 23/65] Translated using Weblate (Vietnamese) Currently translated at 88.1% (2324 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/vi/ --- .../src/main/res/values-vi/strings.xml | 217 +++++++++--------- 1 file changed, 110 insertions(+), 107 deletions(-) diff --git a/library/ui-strings/src/main/res/values-vi/strings.xml b/library/ui-strings/src/main/res/values-vi/strings.xml index c35964ee3c..d7f2c07a08 100644 --- a/library/ui-strings/src/main/res/values-vi/strings.xml +++ b/library/ui-strings/src/main/res/values-vi/strings.xml @@ -84,7 +84,7 @@ Bạn có chắc không\? Đang sao lưu khoá… Tôi không muốn các tin nhắn của tôi được mã hoá - Sao lưu Khoá Bảo mật nên được kích hoạt trên tất cả các phiên để tránh mất các tin nhắn được mã hoá. + Sao lưu bảo mật khoá nên được kích hoạt trên tất cả các phiên để tránh mất các tin nhắn được mã hoá. Đang sao lưu khoá. Nếu bạn đăng xuất bây giờ bạn sẽ không thể xem các tin nhắn được mã hoá. Bạn sẽ mất các tin nhắn được mã hoá nếu bạn đăng xuất ngay bây giờ Sử dụng Sao lưu Khóa @@ -99,7 +99,7 @@ Chủ đề tối Chủ đề sáng Quên mật khẩu\? - Đấy không giống một địa chỉ email hợp lệ + Đấy không giống một địa chỉ thư điện tử hợp lệ Tên người dùng hoặc mật khẩu không đúng Đăng nhập Xin lỗi, không có ứng dụng nào được tìm thấy có thể hoàn thành hành động này. @@ -196,7 +196,7 @@ Đã gửi Đang gửi Số điện thoại - Địa chỉ email + Địa chỉ thư điện tử Lỗi Matrix %1$s đã thay đổi cấp độ quyền lực của %2$s. Bạn đã thay đổi cấp độ quyền lực của %1$s. @@ -234,12 +234,12 @@ (ảnh đại diện cũng được thay đổi) 🎉 Tất cả máy chủ bị cấm không được tham gia! Phòng này không thể được sử dụng nữa. Không có thay đổi. - Bạn đã thay đổi ACL máy chủ cho phòng này. - %s đã thay đổi ACL máy chủ cho phòng này. + Bạn đã thay đổi danh sách máy chủ truy cập máy chủ cho phòng này. + %s đã thay đổi danh sách máy chủ truy cập máy chủ cho phòng này. • Những máy chủ khớp với %s được cho phép. • Những máy chủ khớp với %s bị cấm. - Bạn đã đặt ACL máy chủ cho phòng này. - %s đã đặt ACL máy chủ cho phòng này. + Bạn đã đặt danh sách máy chủ truy cập cho phòng này. + %s đã đặt danh sách máy chủ truy cập cho phòng này. Bạn đã nâng cấp ở đây. %s đã nâng cấp ở đây. Bạn đã nâng cấp phòng này. @@ -256,16 +256,16 @@ %1$s đã cấm %2$s Bạn đã bỏ cấm %1$s %1$s đã bỏ cấm %2$s - Bạn đã xoá tên hiển thị (nó đã là %1$s) - %1$s đã xoá tên hiển thị (nó đã là %2$s) + Bạn đã xoá tên hiển thị (từng là %1$s) + %1$s đã xoá tên hiển thị (từng là %2$s) Bạn đã đổi tên hiển thị từ %1$s thành %2$s %1$s đã đổi tên hiển thị từ %2$s thành %3$s Bạn đã đặt tên hiển thị thành %1$s %1$s đã đặt tên hiển thị thành %2$s Bạn đã thay đổi ảnh đại diện %1$s đã thay đổi ảnh đại diện - Bạn đã rút lại lời mời của %1$s - %1$s đã rút lại lời mời của %2$s + Bạn đã hủy lời mời của %1$s + %1$s đã hủy lời mời của %2$s Chủ đề của bạn Chủ đề Bạn đã kết thúc cuộc gọi. @@ -286,8 +286,8 @@ Bạn đã thay đổi chủ đề thành: %1$s Xoá các tin nhắn chưa được gửi Bạn có chắc bạn muốn xoá tất cả tin nhắn chưa được gửi trong phòng này không\? - Bạn đã kick %1$s - %1$s đã kick %2$s + Bạn đã loại bỏ %1$s + %1$s đã loại bỏ %2$s Bạn đã từ chối lời mời %1$s đã từ chối lời mời Bạn đã rời phòng @@ -329,7 +329,7 @@ %1$s đã rời phòng. Lý do: %2$s Máy chủ nhà này muốn chắc chắn bạn không phải rô bốt Số điện thoại này đã được định nghĩa rồi. - Địa chỉ email này đã được định nghĩa rồi. + Địa chỉ thư điện tử này đã được sử dụng rồi. Đăng nhập bằng đăng nhập một lần Sử dụng làm mặc định và không hỏi lại Luôn hỏi @@ -341,8 +341,8 @@ Xoá tiện ích thất bại Thêm tiện ích thất bại Chuyển - Đã xác minh - Chưa xác minh + Đã xác thực + Chưa xác thực Nhập %1$d/%2$d mã khoá thành công. @@ -402,14 +402,14 @@ %d thay đổi thành viên Vui lòng khởi chạy ${app_name} trên một thiết bị khác mà có thể giải mã tin nhắn để nó có thể gửi các mã khoá vào phiên làm việc này. - Yêu cầu lại các mã khoá mã hoá từ các phiên làm việc khác của bạn. + Yêu cầu lại các mã khoá mã hoá từ các phiên khác của bạn. Quá nhiều yêu cầu đã được gửi Đã không chứa JSON hợp lệ Không được uỷ quyền, thiếu thông tin xác thực hợp lệ Không thể kết nối đến máy chủ nhà tại URL này, vui lòng kiểm tra nó Vui lòng nhập URL hợp lệ Vui lòng xem xét và chấp nhận chính sách của máy chủ nhà này: - Xác minh địa chỉ email thất bại: hãy chắc chắn là bạn đã nhấn vào liên kết trong email + Xác nhận địa chỉ thư điện tử thất bại: hãy chắc chắn là bạn đã nhấn vào liên kết trong email Hiện tất cả phòng trong thư mục phòng, bao gồm cả các phòng có nội dung phản cảm. Hiện các phòng có nội dung phản cảm Danh sách phòng @@ -517,8 +517,8 @@ Lời mời vào phòng • Những máy chủ khớp với IP bây giờ sẽ bị cấm. • Những máy chủ khớp với IP bây giờ sẽ được cho phép. - • Những máy chủ khớp với IP bị cấm. - • Những máy chủ khớp với IP được cho phép. + • Những máy chủ chỉ có địa chỉ Internet (IP) bị cấm. + • Những máy chủ chỉ có địa chỉ Internet (IP) được cho phép. Hủy bỏ qua người dùng Bỏ qua Bỏ qua người dùng này sẽ xóa những tin nhắn của họ khỏi những phòng bạn chia sẻ. @@ -593,7 +593,7 @@ Hủy các giới hạn Hủy tài khoản Xem lại ngay - Chìa khóa phiên + Khóa phiên Mã phiên Tên công khai Lỗi giải mã @@ -790,7 +790,7 @@ Media Chọn quốc gia Quản lý địa chỉ thư điện tử và số điện thoại liên kết với tài khoản Matrix - Email và số điện thoại + Địa chỉ thư điện tử và số điện thoại Hiện tất cả tin nhắn từ %s\? Mật khẩu của bạn vừa được cập nhật Mật khẩu này không hợp lệ @@ -800,7 +800,7 @@ Đổi mật khẩu Mật khẩu Số điện thoại này đã được sử dụng. - Địa chỉ Email này đã được sử dụng. + Địa chỉ thư điện tử này đã được sử dụng. Chọn ngôn ngữ Ngôn ngữ Giao diện người dùng @@ -878,9 +878,9 @@ Thời gian chờ giữa 2 lần đồng bộ Mã nhập vào không hợp lệ. Vui lòng kiểm tra. - Chúng tôi vừa gửi email tới %1$s. -\nClick vào đường link trong email để tiếp tục quá trình tạo tài khoản. - Hãy kiểm tra email của bạn + Chúng tôi vừa gửi thư điện tử tới %1$s. +\nNhấp vào liên kết trong thư để tiếp tục quá trình tạo tài khoản. + Hãy kiểm tra hòm thư của bạn Chấp nhận điều khoản để tiếp tục Hãy thực hiện thách thức captcha Chọn một máy chủ khác @@ -892,7 +892,7 @@ Tiếp Mật khẩu Tên đăng nhập - Username hoặc email + Tên người dùng hoặc thư điện tử Đăng ký với %1$s Số điện thoại có vẻ không hợp lệ. Hãy kiểm tra lại Số điện thoại quốc tế phải bắt đầu với dấu \'+\' @@ -908,11 +908,11 @@ Vui lòng sử dụng mẫu quốc tế. Thêm số điện thoại để tùy chọn cho phép người khác tìm bạn qua số điện thoại. Thêm số điện thoại - Tiếp - Email (tùy chọn) - Email + Tiếp tục + Địa chỉ thư điện tử (tùy chọn) + Địa chỉ thư điện tử Thêm địa chỉ thư điện tử để có thể phục hồi tài khoản. Sau này bạn có thể tùy chọn cho phép người khác tìm mình qua thông tin này. - Thêm địa chỉ email + Thêm địa chỉ thư điện tử Mật khẩu chưa được thay đổi. \n \nBạn muốn ngừng tiến trình đổi mật khẩu\? @@ -921,18 +921,18 @@ Bạn vừa đăng xuất tất cả phiên đăng nhập và không còn nhận được thông báo đẩy. Đăng nhập lại để nhận thông báo trên thiết bị. Mật khẩu của bạn đã được đặt lại. Thành công! - Tôi đã xác minh địa chỉ email + Tôi đã xác nhận địa chỉ email Nhấp vào đường dẫn để xác nhận mật khẩu mới. Sau khi bạn nhâp vào đường dẫn, hãy nhấp vào bên dưới. - Email xác thực đã được gửi tới %1$s. + Thư xác nhận đã được gửi tới %1$s. Kiểm tra mailbox Địa chỉ thư điện tử này không được liên kết với tài khoản nào Tiếp tục Đổi mật khẩu sẽ đặt lại tất cả khóa bảo mật trên tất cả phiên của bạn, làm cho lịch sử chat mã hóa không đọc được. Vui lòng Sao lưu Khóa hoặc xuất khẩu tất cả khóa bảo mật các phòng từ một phiên đăng nhập khác trước khi đặt lại mật khẩu. Cảnh báo! Mật khẩu mới - Email + Địa chỉ thư điện tử Tiếp - Email xác thực thông tin đã được gửi tới bạn để xác nhận đặt lại mật khẩu mới. + Email xác nhận đã được gửi tới bạn để xác nhận đặt lại mật khẩu mới. Đặt lại mật khẩu ở %1$s Địa chỉ thư điện tử này không được liên kết với tài khoản nào. Địa chỉ @@ -954,7 +954,7 @@ Thiết lập tùy chỉnh và nâng cao Khác Xem thêm - Giống như email, tài khoản cần có nhà riêng, dù bạn có thể nói chuyện với bất kỳ ai + Giống như tài khoản thư điện tử, tài khoản cần có nhà riêng, dù bạn có thể nói chuyện với bất kỳ ai Thiết lập tùy chỉnh. Khả dụng Thông báo không được bật cho phiên này. @@ -982,16 +982,16 @@ Thông báo mặc định Mức quan trọng của thông báo theo sự kiện Thiết lập Thông báo nâng cao - Đảm bảo rằng bạn nhấp vào đường link trong email được gửi tới bạn. + Đảm bảo rằng bạn nhấp vào đường link trong thư điện tử được gửi tới bạn. Loại bỏ %s\? Số điện thoại Không có địa chỉ thư điện tử nào trong tài khoản của bạn - Địa chỉ email + Địa chỉ thư điện tử Hiển thị thông tin ứng dụng trong thiết lập hệ thống. Thông tin ứng dụng Thêm số điện thoại Chưa có số điện thoại trong tài khoản của bạn - Thêm địa chỉ email + Thêm địa chỉ thư điện tử Tên hiển thị Hình đại diện Thêm vào màn hình Home @@ -1060,7 +1060,7 @@ Ai có thể tìm và tham gia không gian Truy cập không gian Ai có quyền truy cập\? - Vui lòng kiểm tra email và bấm vào liên kết trong đó. Một khi xong, bấm tiếp tục. + Vui lòng kiểm tra thư điện tử và bấm vào liên kết trong đó. Một khi xong, bấm tiếp tục. Sử dụng trình quản lý chung để quản lý bot, các cầu nối, widget và các gói nhãn dán. \nTrình quản lý chung sẽ nhận được dữ liệu hiệu chỉnh, và sẽ có thể điều chỉnh các widget, gửi lời mời vào phòng và thiết lập các mốc quyền lợi theo ý bạn. ${app_name} sẽ đồng bộ hóa dưới nền trong một khoảng thời gian nhất định (có thể điều chỉnh thời gian). @@ -1115,9 +1115,9 @@ Đang chạy… (%1$d of %2$d) Chạy thử Chuẩn đoán khắc phục sự cố - Bật thông báo qua email cho %s + Bật thông báo qua thư điện tử cho %s Để được nhận thông báo qua thư điện tử, hãy liên kết một địa chỉ thư điện tử với tài khoản Matrix của bạn - Thông báo qua email + Thông báo qua thư điện tử Nâng cấp không gian Thay đổi tên không gian Bật mã hóa không gian @@ -1151,7 +1151,7 @@ Hiển thị %d thiết bị bạn có thể xác thực ngay bây giờ - %d phiên đang hoạt động + %d phiên hoạt động %1$d người @@ -1271,15 +1271,15 @@ Khám phá phòng Khám phá (%s) Hoàn tất cài đặt - Mời qua email, tìm liên hệ và hơn thế nữa… + Mời qua thư điện tử, tìm liên hệ và hơn thế nữa… Hoàn tất việc cài đặt khám phá. - Hiện tại bạn không sử dụng máy chủ xác thực. Để mời đồng đội và có thể khám phá bởi họ, hãy cấu hình một bên dưới. + Hiện tại bạn không sử dụng máy chủ định danh. Để mời đồng đội và có thể khám phá bởi họ, hãy cấu hình một bên dưới. Tham gia Space Tạo space Bỏ qua ngay bây giờ Gia nhập Space của tôi %1$s %2$s Mời theo tên người dùng hoặc thư - Mời qua email + Mời qua thư điện tử Chỉ có anh lúc này thôi. %s sẽ còn tốt hơn với những người khác. Mời đến %s Mời mọi người @@ -1424,19 +1424,19 @@ Quyền Đặt quyền Xác nhận - Nhập URL của máy chủ xác thực - Ngoài ra, bạn có thể nhập bất kỳ URL máy chủ xác thực nào khác + Nhập URL của máy chủ định danh + Ngoài ra, bạn có thể nhập bất kỳ URL máy chủ định danh nào khác Dùng %1$s - Homeerver của bạn (%1$s) đề xuất sử dụng %2$s cho máy chủ xác thực của bạn + Máy chủ nhà của bạn (%1$s) đề xuất sử dụng %2$s cho máy chủ định danh của bạn Sự đồng ý của người dùng chưa được cung cấp. - Không có mối liên hệ hiện tại với mã định danh này. - Sự kết hợp đã thất bại. + Không có mối liên hệ hiện tại với định danh này. + Không thể liên kết. Để đảm bảo quyền riêng tư cho bạn, ${app_name} chỉ hỗ trợ gửi địa chỉ thư điện tử và số điện thoại của người dùng khi đã được băm. - Trước tiên, vui lòng chấp nhận các điều khoản của máy chủ nhận dạng trong cài đặt. - Trước tiên, vui lòng cấu hình máy chủ nhận dạng. - Hoạt động này là không thể. Homeerver đã lỗi thời. - Máy chủ nhận dạng này đã lỗi thời. ${app_name} chỉ hỗ trợ API V2. - Ngắt kết nối khỏi máy chủ nhận dạng %s\? + Trước tiên, vui lòng chấp nhận các điều khoản của máy chủ định danh trong cài đặt. + Trước tiên, vui lòng cấu hình máy chủ định danh. + Hoạt động này là không thể. Máy chủ nhà đã lỗi thời. + Máy chủ định danh này đã lỗi thời. ${app_name} chỉ hỗ trợ API V2. + Ngắt kết nối khỏi máy chủ định danh %s\? Mở các điều khoản của %s Tải các ngôn ngữ có sẵn… Các ngôn ngữ có sẵn khác @@ -1468,10 +1468,10 @@ Xác nhận danh tính của bạn bằng cách xác minh đăng nhập này, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. Xác thực danh tính của bạn bằng cách xác nhận đăng nhập này từ một trong các phiên khác của bạn, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. Xác thực tương tác bằng Emoji - Xác minh đăng nhập + Xác thực đăng nhập Xác thực thủ công bằng Văn bản Xác thực thông tin đăng nhập mới truy cập vào tài khoản của bạn: %1$s - Được mã hóa bởi một thiết bị chưa được xác minh + Được mã hóa bởi một thiết bị chưa được xác thực Không được mã hóa gửi tuyết rơi ❄️ gửi hoa giấy 🎉 @@ -1579,9 +1579,9 @@ Phiên bản máy chủ Tên máy chủ Đăng xuất khỏi phiên này - Quản lý Phiên - Hiện Tất cả Phiên - Phiên Hoạt động + Quản lý phiên + Hiện tất cả phiên + Phiên hoạt động Người quản trị máy chủ của bạn đã vô hiệu hóa mã hóa đầu cuối theo mặc định trong phòng riêng và Tin nhắn trực tiếp. Xác thực chéo không được kích hoạt Xác thực chéo được kích hoạt. @@ -1592,7 +1592,7 @@ Xác thực chéo được kích hoạt \nKhóa riêng trên thiết bị. Xác thực chéo - Phiên mới của bạn hiện đã được xác minh. Nó có quyền truy cập vào các tin nhắn được mã hóa của bạn và những người dùng khác sẽ thấy nó đáng tin cậy. + Phiên mới của bạn hiện đã được xác thực. Nó có quyền truy cập vào các tin nhắn được mã hóa của bạn và những người dùng khác sẽ thấy nó đáng tin cậy. Tin nhắn với người dùng này được mã hóa đầu cuối và không thể được đọc bởi các bên thứ ba. So sánh mã với mã được hiển thị trên màn hình của người dùng khác. So sánh biểu tượng cảm xúc độc đáo, đảm bảo chúng xuất hiện theo cùng một thứ tự. @@ -1643,7 +1643,7 @@ Tin nhắn ở đây không được mã hóa đầu cuối. Tin nhắn trong phòng này không được mã hóa đầu cuối. Đang chờ %s… - Đã xác minh %s + Đã xác thực %s Xác thực %s Xác thực bằng cách so sánh biểu tượng cảm xúc Thay vào đó, xác thực bằng cách so sánh biểu tượng cảm xúc @@ -1673,14 +1673,14 @@ Một trong những điều sau đây có thể bị xâm phạm: \n \n - Máy chủ nhà của bạn -\n - Máy chủ nhà mà bạn đang xác minh được kết nối với +\n - Máy chủ nhà mà người bạn đang xác thực được kết nối với \n - Kết nối internet của bạn hoặc của người dùng khác \n - Thiết bị của bạn hoặc thiết bị của người dùng khác Không bảo mật Chúng không phù hợp Chúng phù hợp Đăng nhập không tin cậy - Tên miền email của bạn không được phép đăng ký trên máy chủ này + Tên miền địa chỉ thư điện tử của bạn không được phép đăng ký trên máy chủ này Tạo Space… Tạo phòng… Một số ký tự không được phép @@ -1749,7 +1749,7 @@ Đăng nhập bằng Matrix ID Homeerver này đang chạy một phiên bản cũ. Yêu cầu người quản trị homeerver của bạn nâng cấp. Bạn có thể tiếp tục, nhưng một số tính năng có thể không hoạt động chính xác. Homeerver lỗi thời - Không giống như một địa chỉ email hợp lệ + Không giống một địa chỉ thư điện tử hợp lệ Ứng dụng không thể tạo tài khoản trên homeerver này. \n \nBạn có muốn đăng ký bằng máy khách web không\? @@ -1769,8 +1769,8 @@ Vui lòng thử lại một khi bạn đã chấp nhận các điều khoản và điều kiện của homeserver của bạn. Nhật ký verbose sẽ giúp các nhà phát triển bằng cách cung cấp thêm nhật ký khi bạn lắc mạnh thiết bị. Ngay cả khi được bật, ứng dụng không ghi lại nội dung tin nhắn hoặc bất kỳ dữ liệu riêng tư nào khác. Bật nhật ký verbose. - Đồng ý với điều khoản dịch vụ của máy chủ xác thực (%s) để cho phép bản thân có thể khám phá bằng địa chỉ email hoặc số điện thoại. - Bạn hiện đang chia sẻ địa chỉ email hoặc số điện thoại trên máy chủ xác thực %1$s. Bạn sẽ cần kết nối lại với %2$s để ngừng chia sẻ chúng. + Đồng ý với điều khoản dịch vụ của máy chủ định danh (%s) để cho phép bản thân có thể khám phá bằng địa chỉ email hoặc số điện thoại. + Bạn hiện đang chia sẻ địa chỉ thư điện tử hoặc số điện thoại trên máy chủ định danh %1$s. Bạn sẽ cần kết nối lại với %2$s để ngừng chia sẻ chúng. Mã xác minh không chính xác. Có lỗi tra cứu số điện thoại Bàn phím số @@ -1790,24 +1790,24 @@ %1$s đã từ chối cuộc gọi này Bạn đã từ chối cuộc gọi này - Thư văn bản đã được gửi đến %s. Vui lòng nhập mã xác minh mà nó chứa. - Máy chủ xác thực bạn đã chọn không có bất kỳ điều khoản dịch vụ nào. Chỉ tiếp tục nếu bạn tin tưởng chủ sở hữu dịch vụ - Máy chủ xác thực không có điều khoản dịch vụ - Vui lòng nhập url máy chủ xác thực - Không thể kết nối với máy chủ xác thực - Nhập URL máy chủ xác thực + Tin nhắn văn bản đã được gửi đến %s. Vui lòng nhập mã xác minh mà nó chứa. + Máy chủ định danh bạn đã chọn không có bất kỳ điều khoản dịch vụ nào. Chỉ tiếp tục nếu bạn tin tưởng chủ sở hữu dịch vụ + Máy chủ định danh không có điều khoản dịch vụ + Vui lòng nhập đường dẫn máy chủ định danh + Không thể kết nối với máy chủ định danh + Nhập URL máy chủ định danh Bạn có đồng ý gửi thông tin này không\? Để khám phá các liên hệ hiện có, bạn cần gửi thông tin liên hệ (địa chỉ thư điện tử và số điện thoại) đến máy chủ định danh của mình. Chúng tôi băm dữ liệu của bạn trước khi gửi để đảm bảo quyền riêng tư. Gửi địa chỉ thư điện tử và số điện thoại đến %s Đồng ý Thu hồi sự đồng ý của tôi - Các liên hệ của bạn là riêng tư. Để khám phá người dùng từ danh bạ của bạn, chúng tôi cần sự cho phép của bạn để gửi thông tin liên hệ đến máy chủ xác thực của bạn. + Các liên hệ của bạn là riêng tư. Để khám phá người dùng từ danh bạ của bạn, chúng tôi cần sự cho phép của bạn để gửi thông tin liên hệ đến máy chủ định danh của bạn. Bạn đã đồng ý gửi địa chỉ thư điện tử và số điện thoại đến máy chủ định danh này để khám phá những người dùng khác từ danh bạ của bạn. - Gửi email và số điện thoại + Gửi thư điện tử và số điện thoại Chúng tôi đã gửi một thư đến %s, trước tiên vui lòng kiểm tra hòm thư của bạn và nhấp vào liên kết xác nhận Chúng tôi đã gửi một thư đến %s, kiểm tra hòm thư của bạn và nhấp vào liên kết xác nhận Số điện thoại có thể khám phá - Ngắt kết nối khỏi máy chủ xác thực của bạn sẽ có nghĩa là bạn sẽ không thể khám phá bởi những người dùng khác và bạn sẽ không thể mời người khác qua email hoặc điện thoại. + Ngắt kết nối khỏi máy chủ định danh của bạn sẽ có nghĩa là bạn sẽ không thể khám phá bởi những người dùng khác và bạn sẽ không thể mời người khác qua thư điện tử hoặc điện thoại. Tùy chọn Khám phá sẽ xuất hiện khi bạn đã thêm số điện thoại. Định danh ứng dụng (ID): Không có cổng Push đã đăng ký @@ -1860,12 +1860,12 @@ Các cuộc hội thoại tin nhắn trực tiếp của bạn sẽ được hiển thị tại đây. Nhấp vào dấu + dưới cùng bên phải để bắt đầu. Các cuộc hội thoại Có vẻ như bạn đang cố gắng kết nối với một homeserver khác. Bạn có muốn đăng xuất không\? - Bạn không sử dụng bất kỳ máy chủ xác thực nào + Bạn không sử dụng bất kỳ máy chủ định danh nào Lỗi không xác định %s muốn xác thực phiên của bạn - Yêu cầu xác minh + Yêu cầu xác thực Đã nhận được - Đã xác minh! + Đã xác thực! Chữ ký Thuật toán Phiên bản @@ -1882,16 +1882,16 @@ Xóa Sao lưu Kiểm tra trạng thái sao lưu Đang xóa bản sao lưu… - Để sử dụng Sao lưu Chính trong phiên này, hãy khôi phục bằng cụm mật khẩu hoặc khóa khôi phục của bạn ngay bây giờ. - Sao lưu có chữ ký không hợp lệ từ phiên chưa được xác minh %s - Sao lưu có chữ ký không hợp lệ từ phiên đã xác minh %s - Sao lưu có chữ ký hợp lệ từ phiên chưa được xác minh %s - Sao lưu có chữ ký hợp lệ từ phiên đã xác minh %s. + Để sử dụng Sao lưu khóa trong phiên này, hãy khôi phục bằng cụm mật khẩu hoặc khóa khôi phục của bạn ngay bây giờ. + Sao lưu có chữ ký không hợp lệ từ phiên chưa được xác thực %s + Sao lưu có chữ ký không hợp lệ từ phiên đã xác thực %s + Sao lưu có chữ ký hợp lệ từ phiên chưa được xác thực %s + Sao lưu có chữ ký hợp lệ từ phiên đã xác thực %s. Sao lưu có chữ ký hợp lệ từ phiên này. - Sao lưu có chữ ký từ phiên không xác định với ID %s. + Sao lưu có chữ ký từ phiên không xác định với định danh %s. Khóa của bạn không được sao lưu từ phiên này. - Sao lưu chính không hoạt động trong phiên này. - Key Backup đã được thiết lập chính xác cho phiên này. + Sao lưu khóa không hoạt động trong phiên này. + Sao lưu khóa đã được thiết lập chính xác cho phiên này. Xóa Sao lưu Khôi phục từ Sao lưu Thất bại trong việc nhận được phiên bản khóa khôi phục mới nhất (%s). @@ -2013,18 +2013,18 @@ Khởi tạo xác thực chéo Cho đến khi người dùng này tin tưởng phiên này, tin nhắn được gửi đến nó và từ nó đều mang nhãn cảnh báo. Ngoài ra, bạn có thể xác thực thủ công. %1$s (%2$s) đã đăng nhập bằng phiên mới: - Phiên này được tin cậy để nhắn tin an toàn vì %1$s (%2$s) đã xác minh: + Phiên này được tin cậy để nhắn tin an toàn vì %1$s (%2$s) đã xác thực: Không tin cậy Tin cậy Phiên Không nhận được phiên Cảnh báo - Đã xác minh + Đã xác thực Xác thực Sử dụng phiên hiện có để xác thực phiên này, cấp cho nó quyền truy cập vào các tin nhắn được mã hóa. Xác thực thiết bị này Xác thực phiên này để đánh dấu nó là đáng tin cậy và cấp cho nó quyền truy cập vào các thư được mã hóa. Nếu bạn không đăng nhập vào phiên này, tài khoản của bạn có thể bị xâm phạm: - Phiên này được tin cậy để nhắn tin an toàn vì bạn đã xác minh nó: + Phiên này được tin cậy để nhắn tin an toàn vì bạn đã xác thực nó: Không có thông tin mật mã sẵn dùng bất ổn định ổn định @@ -2058,9 +2058,9 @@ Vui lòng nhập tên người dùng. Hủy kích hoạt Tài khoản Vui lòng quên tất cả các tin nhắn tôi đã gửi khi tài khoản của tôi bị vô hiệu hóa (Cảnh báo: điều này sẽ khiến người dùng trong tương lai thấy chế độ xem cuộc hội thoại không đầy đủ) - Điều này sẽ làm cho tài khoản của bạn vĩnh viễn không thể sử dụng được. Bạn sẽ không thể đăng nhập và không ai có thể đăng ký lại cùng một ID người dùng. Điều này sẽ khiến tài khoản của bạn rời khỏi tất cả các phòng mà nó đang tham gia và nó sẽ xóa chi tiết tài khoản của bạn khỏi máy chủ nhận dạng của bạn. Điều này là không thể đảo ngược. + Điều này sẽ làm cho tài khoản của bạn vĩnh viễn không thể sử dụng được. Bạn sẽ không thể đăng nhập và không ai có thể đăng ký lại cùng một ID người dùng. Điều này sẽ khiến tài khoản của bạn rời khỏi tất cả các phòng mà nó đang tham gia và nó sẽ xóa chi tiết tài khoản của bạn khỏi máy chủ định danh của bạn. <b>Điều này là không thể đảo ngược</b>. \n -\nHủy kích hoạt tài khoản của bạn does không theo mặc định khiến chúng tôi quên tin nhắn bạn đã gửi. Nếu bạn muốn chúng tôi quên tin nhắn của bạn, vui lòng đánh dấu vào hộp bên dưới. +\nHủy kích hoạt tài khoản của bạn <b>does không theo mặc định khiến chúng tôi quên tin nhắn bạn đã gửi</b>. Nếu bạn muốn chúng tôi quên tin nhắn của bạn, vui lòng đánh dấu vào hộp bên dưới. \n \nKhả năng hiển thị tin nhắn trong Matrix tương tự như email. Chúng tôi quên tin nhắn của bạn có nghĩa là tin nhắn bạn đã gửi sẽ không được chia sẻ với bất kỳ người dùng mới hoặc chưa đăng ký nào, nhưng người dùng đã đăng ký đã có quyền truy cập vào các tin nhắn này vẫn sẽ có quyền truy cập vào bản sao của họ. Để tiếp tục sử dụng homeserver %1$s, bạn phải xem xét và đồng ý với các điều khoản và điều kiện. @@ -2102,11 +2102,11 @@ Lệnh không được nhận ra: %s Lỗi lệnh Yêu cầu Chia sẻ Khóa - Một phiên chưa được xác minh đang yêu cầu khóa mã hóa. + Một phiên chưa được xác thực đang yêu cầu khóa mã hóa. \nTên phiên: %1$s \nLần nhìn thấy lần cuối: %2$s \nNếu bạn không đăng nhập vào phiên khác, hãy bỏ qua yêu cầu này. - Phiên chưa được xác thực của bạn \'%s\' đang yêu cầu khóa mã hóa. + Phiên chưa được xác thực \'%s\' đang yêu cầu khóa mã hóa. Một phiên mới đang yêu cầu các khóa mã hóa. \nTên phiên: %1$s \nLần nhìn thấy lần cuối: %2$s @@ -2182,13 +2182,13 @@ Tất cả các phòng trên %s server Tên máy chủ Chọn thư mục phòng - Không bao giờ gửi tin nhắn được mã hóa đến các phiên chưa được xác minh từ phiên này. - Chỉ mã hóa cho các phiên đã xác minh + Không bao giờ gửi tin nhắn được mã hóa đến các phiên chưa được xác thực từ phiên này. + Chỉ mã hóa cho các phiên đã xác thực Bỏ đặt làm địa chỉ chính Đặt làm địa chỉ chính Máy chủ này không cung cấp bất kỳ chính sách nào. Thư viện bên thứ ba - Chính sách máy chủ xác thực của bạn + Chính sách máy chủ định danh của bạn Chính sách homeerver của bạn Chính sách ${app_name} Bạn có thể tắt tính năng này bất cứ lúc nào trong cài đặt @@ -2199,7 +2199,7 @@ \n \nBạn có thể đọc tất cả các thuật ngữ của chúng tôi %s. Giúp cải thiện ${app_name} - Thiết bị đã bị đăng xuất! + Phiên đã bị đăng xuất! Căn phòng đã bị bỏ lại! Chọn homeerver Không thể kết nối đến một homeserver tại URL %s. Vui lòng kiểm tra liên kết của bạn hoặc chọn homeerver thủ công. @@ -2207,11 +2207,11 @@ Kích hoạt Nghe thông báo Tùy chọn Khám phá sẽ xuất hiện sau khi bạn đã thêm địa chỉ thư điện tử. - Địa chỉ email có thể khám phá - Hiện tại bạn không sử dụng máy chủ xác thực. Để khám phá và có thể khám phá bởi các liên hệ hiện có mà bạn biết, hãy cấu hình một danh bạ dưới đây. - Không có chính sách được cung cấp bởi máy chủ xác thực - Ẩn chính sách máy chủ xác thực - Hiện chính sách máy chủ xác thực + Địa chỉ thư điện tử có thể khám phá + Hiện tại bạn không sử dụng máy chủ định danh. Để khám phá và có thể khám phá bởi các liên hệ hiện có mà bạn biết, hãy cấu hình một danh bạ dưới đây. + Máy chủ định danh không cung cấp chính sách nào + Ẩn chính sách máy chủ định danh + Hiện chính sách máy chủ định danh Mở Cài đặt Khám phá Thêm tab dành riêng cho các thông báo chưa đọc trên màn hình chính. Tìm kiếm theo tên, ID hoặc thư @@ -2334,7 +2334,7 @@ Bạn có thể đổi lại sau Tên hiển thị Chọn tên hiển thị - Tên người dùng / Thư điện tử / Số điện thoại + Tên người dùng / Địa chỉ thư điện tử / Số điện thoại Bạn có phải con người\? Làm theo chỉ dẫn gửi tới %s Đặt lại mật khẩu @@ -2376,7 +2376,7 @@ Gửi phản hồi Đã bật: Thẻ hồ sơ: - Định danh của phiên: + Định danh phiên: Đi Đang cập nhật dữ liệu của bạn… Mọi người @@ -2463,7 +2463,7 @@ Chọn máy chủ của bạn Bạn nắm quyền kiểm soát. Muốn chạy một máy chủ của mình\? - %s cần xác thực tài khoản của bạn + %s cần xác nhận tài khoản của bạn Đi thôi Bạn sẽ trò chuyện với ai nhiều nhất\? Chưa chắc chắn\? %s @@ -2551,4 +2551,7 @@ \'\'Chủ đề\'\' giúp giữ cho các cuộc trò chuyện của bạn theo chủ đề và dễ theo dõi. Máy chủ nhà của bạn chưa hỗ trợ cho việc liệt kê các chủ đề. \'\'Chủ đề\'\' giúp giữ cho các cuộc trò chuyện của bạn theo chủ đề và dễ theo dõi. %sBật chủ đề sẽ làm mới ứng dụng. Quá trình này có thể mất nhiều thời gian hơn đối với một số tài khoản. + Phiên + Thiết bị + Hoạt động cuối %1$s \ No newline at end of file From 1ab2bb9bf81da98be17f2c3fa746f70a1d9b6c8e Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Mon, 3 Jul 2023 20:46:15 +0000 Subject: [PATCH 24/65] Translated using Weblate (Latvian) Currently translated at 68.1% (1797 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/lv/ --- library/ui-strings/src/main/res/values-lv/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/library/ui-strings/src/main/res/values-lv/strings.xml b/library/ui-strings/src/main/res/values-lv/strings.xml index ad70dbafbc..0d0fcda97f 100644 --- a/library/ui-strings/src/main/res/values-lv/strings.xml +++ b/library/ui-strings/src/main/res/values-lv/strings.xml @@ -2067,4 +2067,10 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. 2 1 Izmēģināt + + %1$d atlasīti + %1$d atlasīts + %1$d atlasīti + + %1$s nomainīja attēlojamo vārdu uz %2$s \ No newline at end of file From 0573915a0a734e5c0ceab888d701877fa6f0876f Mon Sep 17 00:00:00 2001 From: Yoan Pintas Date: Tue, 4 Jul 2023 15:12:37 +0200 Subject: [PATCH 25/65] Update MSC3912 implementation: Redaction of related events (#8532) --- changelog.d/8481.misc | 1 + .../api/session/homeserver/HomeServerCapabilities.kt | 4 ++-- .../android/sdk/api/session/room/send/SendService.kt | 4 ++-- .../android/sdk/internal/auth/version/Versions.kt | 10 ++++------ .../sdk/internal/crypto/tasks/RedactEventTask.kt | 10 +++++----- .../database/mapper/HomeServerCapabilitiesMapper.kt | 2 +- .../homeserver/GetHomeServerCapabilitiesTask.kt | 4 ++-- .../internal/session/room/send/DefaultSendService.kt | 6 +++--- .../session/room/send/LocalEchoEventFactory.kt | 6 +++--- .../internal/session/room/send/RedactEventWorker.kt | 4 ++-- .../session/room/send/model/EventRedactBody.kt | 9 +++++++-- .../session/room/send/queue/EventSenderProcessor.kt | 4 ++-- .../room/send/queue/EventSenderProcessorCoroutine.kt | 8 ++++---- .../internal/session/room/send/queue/QueueMemento.kt | 2 +- .../session/room/send/queue/QueuedTaskFactory.kt | 4 ++-- .../session/room/send/queue/RedactQueuedTask.kt | 4 ++-- 16 files changed, 43 insertions(+), 39 deletions(-) create mode 100644 changelog.d/8481.misc diff --git a/changelog.d/8481.misc b/changelog.d/8481.misc new file mode 100644 index 0000000000..069c2e8b3f --- /dev/null +++ b/changelog.d/8481.misc @@ -0,0 +1 @@ +Update MSC3912 implementation: Redaction of related events diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index 4968df775a..78672adb00 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -77,9 +77,9 @@ data class HomeServerCapabilities( val canRemotelyTogglePushNotificationsOfDevices: Boolean = false, /** - * True if the home server supports event redaction with relations. + * True if the home server supports redaction of related events. */ - var canRedactEventWithRelations: Boolean = false, + var canRedactRelatedEvents: Boolean = false, /** * External account management url for use with MSC3824 delegated OIDC, provided in Wellknown. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index 07036f4b65..9eb0fa4097 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -158,10 +158,10 @@ interface SendService { * Redact (delete) the given event. * @param event the event to redact * @param reason optional reason string - * @param withRelations the list of relation types to redact with this event + * @param withRelTypes the list of relation types to redact with this event * @param additionalContent additional content to put in the event content */ - fun redactEvent(event: Event, reason: String?, withRelations: List? = null, additionalContent: Content? = null): Cancelable + fun redactEvent(event: Event, reason: String?, withRelTypes: List? = null, additionalContent: Content? = null): Cancelable /** * Schedule this message to be resent. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index 3fe5541b68..83186344bb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -59,8 +59,7 @@ private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882" private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771" private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773" private const val FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881" -private const val FEATURE_EVENT_REDACTION_WITH_RELATIONS = "org.matrix.msc3912" -private const val FEATURE_EVENT_REDACTION_WITH_RELATIONS_STABLE = "org.matrix.msc3912.stable" +private const val FEATURE_REDACTION_OF_RELATED_EVENT = "org.matrix.msc3912" /** * Return true if the SDK supports this homeserver version. @@ -162,9 +161,8 @@ internal fun Versions.doesServerSupportRemoteToggleOfPushNotifications(): Boolea /** * Indicate if the server supports MSC3912: https://github.com/matrix-org/matrix-spec-proposals/pull/3912. * - * @return true if event redaction with relations is supported + * @return true if redaction of related events is supported */ -internal fun Versions.doesServerSupportRedactEventWithRelations(): Boolean { - return unstableFeatures?.get(FEATURE_EVENT_REDACTION_WITH_RELATIONS).orFalse() || - unstableFeatures?.get(FEATURE_EVENT_REDACTION_WITH_RELATIONS_STABLE).orFalse() +internal fun Versions.doesServerSupportRedactionOfRelatedEvents(): Boolean { + return unstableFeatures?.get(FEATURE_REDACTION_OF_RELATED_EVENT).orFalse() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt index b060748a61..01d59a8c80 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt @@ -30,7 +30,7 @@ internal interface RedactEventTask : Task { val roomId: String, val eventId: String, val reason: String?, - val withRelations: List?, + val withRelTypes: List?, ) } @@ -41,9 +41,9 @@ internal class DefaultRedactEventTask @Inject constructor( ) : RedactEventTask { override suspend fun execute(params: RedactEventTask.Params): String { - val withRelations = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canRedactEventWithRelations.orFalse() && - !params.withRelations.isNullOrEmpty()) { - params.withRelations + val withRelTypes = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canRedactRelatedEvents.orFalse() && + !params.withRelTypes.isNullOrEmpty()) { + params.withRelTypes } else { null } @@ -55,7 +55,7 @@ internal class DefaultRedactEventTask @Inject constructor( eventId = params.eventId, body = EventRedactBody( reason = params.reason, - withRelations = withRelations, + unstableWithRelTypes = withRelTypes, ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 1c7a0591a1..b566cf7cb6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -47,7 +47,7 @@ internal object HomeServerCapabilitiesMapper { canLoginWithQrCode = entity.canLoginWithQrCode, canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications, canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices, - canRedactEventWithRelations = entity.canRedactEventWithRelations, + canRedactRelatedEvents = entity.canRedactEventWithRelations, externalAccountManagementUrl = entity.externalAccountManagementUrl, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index a368325793..71f6ebd6e3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -25,7 +25,7 @@ import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin -import org.matrix.android.sdk.internal.auth.version.doesServerSupportRedactEventWithRelations +import org.matrix.android.sdk.internal.auth.version.doesServerSupportRedactionOfRelatedEvents import org.matrix.android.sdk.internal.auth.version.doesServerSupportRemoteToggleOfPushNotifications import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreadUnreadNotifications import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads @@ -154,7 +154,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( homeServerCapabilitiesEntity.canRemotelyTogglePushNotificationsOfDevices = getVersionResult.doesServerSupportRemoteToggleOfPushNotifications() homeServerCapabilitiesEntity.canRedactEventWithRelations = - getVersionResult.doesServerSupportRedactEventWithRelations() + getVersionResult.doesServerSupportRedactionOfRelatedEvents() } if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 7176e36e0c..04749103c1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -140,11 +140,11 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } - override fun redactEvent(event: Event, reason: String?, withRelations: List?, additionalContent: Content?): Cancelable { + override fun redactEvent(event: Event, reason: String?, withRelTypes: List?, additionalContent: Content?): Cancelable { // TODO manage media/attachements? - val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason, withRelations, additionalContent) + val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason, withRelTypes, additionalContent) .also { createLocalEcho(it) } - return eventSenderProcessor.postRedaction(redactionEcho, reason, withRelations) + return eventSenderProcessor.postRedaction(redactionEcho, reason, withRelTypes) } override fun resendTextMessage(localEcho: TimelineEvent): Cancelable { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index c2bdec3596..9510f50420 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -812,12 +812,12 @@ internal class LocalEchoEventFactory @Inject constructor( } } */ - fun createRedactEvent(roomId: String, eventId: String, reason: String?, withRelations: List? = null, additionalContent: Content? = null): Event { + fun createRedactEvent(roomId: String, eventId: String, reason: String?, withRelTypes: List? = null, additionalContent: Content? = null): Event { val localId = LocalEcho.createLocalEchoId() - val content = if (reason != null || withRelations != null) { + val content = if (reason != null || withRelTypes != null) { EventRedactBody( reason = reason, - withRelations = withRelations, + unstableWithRelTypes = withRelTypes, ).toContent().plus(additionalContent.orEmpty()) } else { additionalContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt index 576f31c64c..270d3a228e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt @@ -43,7 +43,7 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters, ses val roomId: String, val eventId: String, val reason: String?, - val withRelations: List? = null, + val withRelTypes: List? = null, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -63,7 +63,7 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters, ses roomId = params.roomId, eventId = params.eventId, reason = params.reason, - withRelations = params.withRelations, + withRelTypes = params.withRelTypes, ) ) }.fold( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt index cf2bc0dc4f..2ed5c9f363 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt @@ -25,5 +25,10 @@ internal data class EventRedactBody( val reason: String? = null, @Json(name = "org.matrix.msc3912.with_relations") - val withRelations: List? = null, -) + val unstableWithRelTypes: List? = null, + + @Json(name = "with_rel_types") + val withRelTypes: List? = null, +) { + fun getBestWithRelTypes() = withRelTypes ?: unstableWithRelTypes +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt index b285e90c9a..90d78a51e0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt @@ -26,9 +26,9 @@ internal interface EventSenderProcessor : SessionLifecycleObserver { fun postEvent(event: Event, encrypt: Boolean): Cancelable - fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelations: List? = null): Cancelable + fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelTypes: List? = null): Cancelable - fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?, withRelations: List? = null): Cancelable + fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?, withRelTypes: List? = null): Cancelable fun postTask(task: QueuedTask): Cancelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt index 9ce29c3c08..a4e3773eb9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt @@ -101,8 +101,8 @@ internal class EventSenderProcessorCoroutine @Inject constructor( return postTask(task) } - override fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelations: List?): Cancelable { - return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason, withRelations) + override fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelTypes: List?): Cancelable { + return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason, withRelTypes) } override fun postRedaction( @@ -110,9 +110,9 @@ internal class EventSenderProcessorCoroutine @Inject constructor( eventToRedactId: String, roomId: String, reason: String?, - withRelations: List? + withRelTypes: List? ): Cancelable { - val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason, withRelations) + val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason, withRelTypes) return postTask(task) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt index a900e4ae5d..85238ae944 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt @@ -118,7 +118,7 @@ internal class QueueMemento @Inject constructor( eventId = it.redacts, roomId = it.roomId, reason = body?.reason, - withRelations = body?.withRelations, + withRelTypes = body?.getBestWithRelTypes(), ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt index 46df7e29f3..e79808ee3f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt @@ -43,13 +43,13 @@ internal class QueuedTaskFactory @Inject constructor( ) } - fun createRedactTask(redactionLocalEcho: String, eventId: String, roomId: String, reason: String?, withRelations: List? = null): QueuedTask { + fun createRedactTask(redactionLocalEcho: String, eventId: String, roomId: String, reason: String?, withRelTypes: List? = null): QueuedTask { return RedactQueuedTask( redactionLocalEchoId = redactionLocalEcho, toRedactEventId = eventId, roomId = roomId, reason = reason, - withRelations = withRelations, + withRelTypes = withRelTypes, redactEventTask = redactEventTask, localEchoRepository = localEchoRepository, cancelSendTracker = cancelSendTracker diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt index f484c24aae..b51a04f863 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt @@ -26,14 +26,14 @@ internal class RedactQueuedTask( val redactionLocalEchoId: String, private val roomId: String, private val reason: String?, - private val withRelations: List?, + private val withRelTypes: List?, private val redactEventTask: RedactEventTask, private val localEchoRepository: LocalEchoRepository, private val cancelSendTracker: CancelSendTracker ) : QueuedTask(queueIdentifier = roomId, taskIdentifier = redactionLocalEchoId) { override suspend fun doExecute() { - redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason, withRelations)) + redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason, withRelTypes)) } override fun onTaskFailed() { From 1b43087eb3ebc423a0136049f7a3ab4968caad1c Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Wed, 5 Jul 2023 08:41:00 +0000 Subject: [PATCH 26/65] Translated using Weblate (Latvian) Currently translated at 68.5% (1807 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/lv/ --- .../ui-strings/src/main/res/values-lv/strings.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/library/ui-strings/src/main/res/values-lv/strings.xml b/library/ui-strings/src/main/res/values-lv/strings.xml index 0d0fcda97f..d7bd13416b 100644 --- a/library/ui-strings/src/main/res/values-lv/strings.xml +++ b/library/ui-strings/src/main/res/values-lv/strings.xml @@ -2073,4 +2073,18 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. %1$d atlasīti %1$s nomainīja attēlojamo vārdu uz %2$s + Nav ļauts pievienoties šai istabai + Mainīt vietu + Sākotnējais sinhronizēšanas pieprasījums + Visas tērzēšanas + Izpētīt istabas + Izvērst %s apakšvienumus + + %d servera ACL izmaiņu + %d servera ACL izmaiņa + %d servera ACL izmaiņas + + Sakļaut %s apakšvienumus + Uzsākt tērzēšanu + Izveidot istabu \ No newline at end of file From 85d0837f3b2f8ee3f7422c9a531ca58b29508313 Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Wed, 5 Jul 2023 11:33:44 +0000 Subject: [PATCH 27/65] Translated using Weblate (Latvian) Currently translated at 68.6% (1810 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/lv/ --- library/ui-strings/src/main/res/values-lv/strings.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-lv/strings.xml b/library/ui-strings/src/main/res/values-lv/strings.xml index d7bd13416b..2760d06bc3 100644 --- a/library/ui-strings/src/main/res/values-lv/strings.xml +++ b/library/ui-strings/src/main/res/values-lv/strings.xml @@ -59,7 +59,7 @@ Jūs izveidojāt istabu %1$s izveidoja istabu Jūsu uzaicinājums - Jūs ieslēdzāt pilnīgu šifrēšanu (neatpazīts algoritms %1$s). + Ir ieslēgta pilnīga šifrēšana (neatpazīts algoritms %1$s). %1$s ieslēdza pilnīgu šifrēšanu (neatpazīts algoritms %2$s). Jūs ieslēdzāt pilnīgu šifrēšanu. %1$s ieslēdza pilnīgu šifrēšanu. @@ -2087,4 +2087,9 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Sakļaut %s apakšvienumus Uzsākt tērzēšanu Izveidot istabu + ${app_name} ir nepieciešams veikt kešatmiņas tīrīšanu, lai būtu pašreizēja, šī iemesla dēļ: +\n%s +\n +\nJāņem vērā, ka šī darbība pārsāknēs lietotni, un tas var aizņemt kādu laiku. + - atsevišķi lietotāji atkal tiek ņemti vērā \ No newline at end of file From fedbfe4931243afa3a83deb718678ba65eefe96e Mon Sep 17 00:00:00 2001 From: Dimitris Vagiakakos Date: Wed, 5 Jul 2023 21:49:54 +0000 Subject: [PATCH 28/65] Translated using Weblate (Greek) Currently translated at 16.0% (425 of 2640 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/el/ --- .../src/main/res/values-el/strings.xml | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-el/strings.xml b/library/ui-strings/src/main/res/values-el/strings.xml index f4973f4b95..5f7d5df4b8 100644 --- a/library/ui-strings/src/main/res/values-el/strings.xml +++ b/library/ui-strings/src/main/res/values-el/strings.xml @@ -383,4 +383,82 @@ Άνοιγμα με Αποστολή αυτοκόλλητου %1$s δημιούργησε το δωμάτιο - + Εισήλθες + Αποχώρησες από το δωμάτιο + %1$s αποχώρισε από το δωμάτιο + Έχετε απορρίψει την πρόσκληση + Ορίσατε το όνομά σας ω; %1$s + Η πρόσκλησή σου + Δημιούργησες αυτό το δωμάτιο + %1$s δημιούργησε την συζήτηση + "Προσκάλεσες τον χρήστη %1$s" + Έχετε αποκλείσει τον χρήστη %1$s + Αλλάξατε το άβατάρ σας + Άλλαξες το θέμα σε: %1$s + Δωμάτιο/Χώρος + Σύνδεση + + %1$d επιλέχθηκε + %1$d επιλέχθηκαν + + + %d ACLs του server άλλαξε + %d ACLs του server άλλαξαν + + Πραγματοποιείται σύνδεση στην συσκευή + Δεν ταιριάζει; + Προσπάθησε ξανά + "Οι servers που περιλαμβάνουν %s επιτρέπονται." + Δημιούργησες αυτή την συζήτηση + Αποχώρισες από το δωμάτιο + Ο χρήστης %1$s αφαίρεσε τον αποκλείσμο του χρήστη %2$s + Άλλαξες το όνομα του δωματίου σε: %1$s + Έχετε ξεκινήσει μία βιντεοκλήση. + Το μελλοντικό ιστορικό του δωματίου θα είναι εμφανή στο χρήστη %1$s + Έχετε ξεκινησει μία κλήση. + Ο χρήστης %s αναβάθμισε αυτό το δωμάτιο. + Η εύρεση server που περιλαμβάνει %s είναι απεκλεισμένη. + "• Οι servers οπού περιλαμβάνουν %s είναι τώρα απεκλεισμένοι." + Επιβεβαίωση + Παρακαλώ επιβεβαιώστε ότι γνωρίζετε την προέλευση του κώδικα αυτού. Όταν συνδέετε νέες συσκευές, ενδεχομένως να δίνετε σε κάποιον άλλο πλήρης πρόσβαση στον λογαριασμό σας. + Εκαθάρριση προσωπικών δεδομένων + Αφαιρέσατε τον χρήστη %1$s + Ο χρήστης %1$s άλλαξε το όνομά του σε %2$s + %1$s άλλαξε το άβαταρ του δωματίου + Άλλαξες το άβαταρ του δωματίου + "%s απέστειλε δεδομένα ώστε να πραγματοποιηθεί η κλήση." + Απέστειλες δεδομένα ώστε να ξεκινήσει η κλήση. + Απάντησες στην κλήση. + Ο χρήστης %1$s έκανε τα μελλοντικά μηνύματα διαθέσιμα στον χρήστη %2$s + Έχετε αναβαθμίσει αυτό το δωμάτιο. + %s αναβαθμίστηκε εδώ. + Αναβαθμίστηκες εδώ. + %s όρισε τα ACLs αυτού του server σε αυτό το δωμάτιο. + Έχεις ορίσει το ACL του server για αυτό το δωμάτιο. + "• Οι servers που περιλαμβάνονται αυτή την IP είναι απεκλεισμένοι." + %s άλλαξε τα ACLs του server γι αυτό το δωμάτιο. + Άλλαξες τα ACLs του server γι αυτό το δωμάτιο. + Κωδικός Πρόσβασης + Οι διαχειριστές του server σας (%1$s), σας έχουν αποσυνδέσει από τον λογαριασμό σας %2$s (%3$s). + Η πειργραφή είναι πολύ σύντομη + Έχετε αποσυνδεθεί + Επιπρόσθετες ρυθμίσεις + Λειτουργίες Προγραμματιστή + Τρέχουσα συνεδρία + Διαγραφή δεδομένων + Άλλες συνεδρίες + Πραγματοποιείται η σύνδεση + Συμμετέχεις σε αυτό το δωμάτιο + %1$s εισήλθε + Έχετε αφαιρέσει τον αποκλεισμό από τον χρήστη %1$s + Αφαιρέσατε την πρόσκληση του χρήστη %1$s + Άλλαξες το όνομά σου από %1$s σε %2$s + Αφαίρεσες το όνομά όπου εμφανιζόταν ο λογαριασμός σου ( προηγουμένως ήταν %1$s) + .Τερμάτισες την κλήση. + Έκανες τα μελλοντικά μηνύματα διαθέσιμα στον χρήστη %1$s + Ορισμός συνδέσμου + Συνδεθείτε ξανά + Σύνδεση + Διαγρφή όλων των δεδομένων + Ρυθμίσεις + \ No newline at end of file From 87f2a69fb1f3261949216bace264de9e96c6fc14 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 5 Jul 2023 17:10:27 +0000 Subject: [PATCH 29/65] Translated using Weblate (Greek) Currently translated at 16.0% (425 of 2640 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/el/ --- library/ui-strings/src/main/res/values-el/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/ui-strings/src/main/res/values-el/strings.xml b/library/ui-strings/src/main/res/values-el/strings.xml index 5f7d5df4b8..3b22a60a54 100644 --- a/library/ui-strings/src/main/res/values-el/strings.xml +++ b/library/ui-strings/src/main/res/values-el/strings.xml @@ -360,11 +360,11 @@ \nΜε την απενεργοποίηση του λογαρισμού σας δεν θα ξεχαστούν τα μηνύματά που έχετε στείλει. Εάν θα θέλατε να ξεχαστούν, τότε δηλώστε το στο κουτί πιο κάτω. \n \nΗ ορατότητα μηνυμάτων δουλεύει σαν το ηλεκτρονικό ταχυδρομείο. Το να ξεχαστούν τα μηνύματα σημαίνει οτι μηνύματα τα οποία έχετε στείλει δεν θα σταλούν σε νέους ή μη-εγγεγραμμένους χρήστες, αλλα εγγεγραμμένοι χρήστες οι οποίοι έχουν ήδη πρόσβαση σε αυτά τα μηνύματα θα μπορούν ακόμα να τα δούν. - Αυτό το δωμάτιο έχει αντικατασταθεί και δεν είναι πια ενεργό + Αυτό το δωμάτιο έχει αντικατασταθεί και δεν είναι πια ενεργό. Παράβλεψη Αποσύνδεση Δεν εμπιστεύομαι - Δεν έχετε εξουσιοδότηση να δημοσιεύσετε σε αυτό το δωμάτιο + Δεν έχετε εξουσιοδότηση να δημοσιεύσετε σε αυτό το δωμάτιο. %1$s & %2$s & άλλοι γράφουν… %s γράφει… %1$s & %2$s γράφουν… @@ -461,4 +461,6 @@ Σύνδεση Διαγρφή όλων των δεδομένων Ρυθμίσεις + Γενικά + Γενικά \ No newline at end of file From 1ef2de0356df7f52d1124b1bae60e4b239dd4aea Mon Sep 17 00:00:00 2001 From: walito-arch Date: Wed, 5 Jul 2023 08:53:04 +0000 Subject: [PATCH 30/65] Translated using Weblate (Swahili) Currently translated at 1.0% (28 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sw/ --- .../src/main/res/values-sw/strings.xml | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-sw/strings.xml b/library/ui-strings/src/main/res/values-sw/strings.xml index d1938c5896..923cfac661 100644 --- a/library/ui-strings/src/main/res/values-sw/strings.xml +++ b/library/ui-strings/src/main/res/values-sw/strings.xml @@ -11,4 +11,24 @@ %1$d Iliyochaguliwa %1$d Ziliyochaguliwa - + %1$s Andaa chumba + Umeandaa chumba + %1$s anzisha mjadala + Ulianzisha mjadala + %1$s Walioalikwa %2$s + %1$s jiunge na mjadala + %1$s Acha mjadala + Umetoka kwenye mjadala + %1$s Acha mjadala + Umeacha mjadala + %1$s hujazuiliwa %2$s + Hujazuiliwa %1$s + %1$s huruhusiwi %2$s + Huruhusiwi %1$s + Sitisha %1$s\'s mwaliko + %s\'s Mwaliko + Umejiunga na mjadala + %1$s sitisha %2$s\'s mmwaliko + Mwaliko wako + Umealika %1$s + \ No newline at end of file From c1e77c6dc9433b5b646b7d85840f9b99bbc9ed90 Mon Sep 17 00:00:00 2001 From: Nui Harime Date: Fri, 7 Jul 2023 08:46:33 +0000 Subject: [PATCH 31/65] Translated using Weblate (Russian) Currently translated at 99.6% (2628 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ru/ --- library/ui-strings/src/main/res/values-ru/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml index b48e72cbba..12b038129d 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -3032,7 +3032,7 @@ Сообщение в %s Сообщение в комнате Комната/Пространство - Аккаунт + Учётная запись Аватар профиля пользователя %1$s Продолжайте, только если вы уверены, что ваш ключ утерян, а доступ к другим активным устройствам отсустствует. Пока пользователь не верифицировал эту сессию, отправленные и полученные сообщения отмечаются предупреждениями. @@ -3046,13 +3046,13 @@ При изменении настроек уведомлений произошла ошибка. Попробуйте ещё раз. Когда приглашенные пользователи присоединятся к ${app_name}, вы сможете писать им с использованием сквозного шифрования Сброс ваших ключей верификации не может быть отменен. После сброса, вы не будете иметь доступа к старым зашифрованным сообщениям, а все ваши контакты, верифицировавшие вас ранее, увидят предупреждение о повторной верификации. - Зашифрованно неактивным устройством + Зашифровано прерванным сеансом Защищенный обмен сообщениями был обновлен. Пожалуйста, повторно верифицируйте ваше устройство. Не удается расшифровать голосовое сообщение. Обзор опроса во времени Сообщение - Подтвердить с помощью активного устройства + Сверить с другим сеансом Возобновить - %1$s изменил отображаемое имя на %2$s + %1$s изменил(а) имя на %2$s Запрос на верификацию не найден. Возможно, он был отменен или обработан другим сеансом. \ No newline at end of file From c3752f529a76753a00c0a9c3d4ca1e9f17b04c17 Mon Sep 17 00:00:00 2001 From: KuriakiMariaHere Date: Fri, 7 Jul 2023 21:36:13 +0000 Subject: [PATCH 32/65] Translated using Weblate (Greek) Currently translated at 16.7% (442 of 2640 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/el/ --- .../src/main/res/values-el/strings.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/library/ui-strings/src/main/res/values-el/strings.xml b/library/ui-strings/src/main/res/values-el/strings.xml index 3b22a60a54..028c73750e 100644 --- a/library/ui-strings/src/main/res/values-el/strings.xml +++ b/library/ui-strings/src/main/res/values-el/strings.xml @@ -463,4 +463,22 @@ Ρυθμίσεις Γενικά Γενικά + %1$s Έχει αφαιρεθεί το avatar του δωματίου + Έχεις αφαιρέσει το avatar του δωματίου + Καμία αλλαγή. + Έχεις αφαίρεσει το όνομα του δωματίου + Ο χρήστης %1$s προσκάλεσε τον χρήστη %2$s + Προσκάλεσες τον χρήστη %1$s + Διαχειριστής + Moderator + Άλλαξες το power του level %1$s. + Οι Servers που ταιριάζουν με %s έχουν αφαιρεθεί από την επιτρεπόμενη λίστα. + Οι Servers που ταιριάζουν με το IP έχουν αποκλειστικοί. + Έχεις αφαιρέσει το θέμα του δωματίου + Αποδέχτηκες την πρόσκληση για το %1$s + Προεπιλογή + Custom (%1$d) + Προσαρμοσμένο + 🎉Όλοι οι Servers έχουν αποκλειστεί από συμμετέχοντες!Το δωμάτιο αυτό δεν μπορεί πια να χρησιμοποιηθεί. + Έστειλες μια πρόσκληση στο %1$s για να συνδεθείς στο δωμάτιο \ No newline at end of file From 2bbd5ee7d9ec44cf20d610449dca921003736e4d Mon Sep 17 00:00:00 2001 From: KuriakiMariaHere Date: Sat, 8 Jul 2023 15:59:27 +0000 Subject: [PATCH 33/65] Translated using Weblate (Greek) Currently translated at 17.0% (451 of 2640 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/el/ --- .../ui-strings/src/main/res/values-el/strings.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/library/ui-strings/src/main/res/values-el/strings.xml b/library/ui-strings/src/main/res/values-el/strings.xml index 028c73750e..3d148ae2bf 100644 --- a/library/ui-strings/src/main/res/values-el/strings.xml +++ b/library/ui-strings/src/main/res/values-el/strings.xml @@ -481,4 +481,16 @@ Προσαρμοσμένο 🎉Όλοι οι Servers έχουν αποκλειστεί από συμμετέχοντες!Το δωμάτιο αυτό δεν μπορεί πια να χρησιμοποιηθεί. Έστειλες μια πρόσκληση στο %1$s για να συνδεθείς στο δωμάτιο + Το μήνυμα στάλθηκε + Δημιουργώ ένα δωμάτιο + Δεν επιτρέπεται να συνδεθείς σε αυτό το δωμάτιο + %1$s,%2$s και %3$s + Αλλάζω το Space + %1$s, %2$s, %3$s και %4$s + Ερευνάω τα δωμάτια + -Κάποιοι χρήστες έχουν αγνοηθεί + + %1$s, %2$s, %3$s και %4$d άλλος χρήστης + %1$s, %2$s, %3$s και %4$d άλλοι χρήστες + \ No newline at end of file From 99e171dbee6118ed81673638ccfb11105a7641fc Mon Sep 17 00:00:00 2001 From: Nui Harime Date: Sat, 8 Jul 2023 13:29:53 +0000 Subject: [PATCH 34/65] Translated using Weblate (Russian) Currently translated at 99.6% (2628 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ru/ --- library/ui-strings/src/main/res/values-ru/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml index 12b038129d..210d60a827 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -3020,7 +3020,7 @@ Не удалось записать голосовое сообщение Убедиться что Ваш аккаунт в безопасности Получить последнюю сборку (у вас могут быть проблемы со входом) - История опроса + Опросы Голосовая трансляция начата Ваш домашний сервер не поддерживает список обсуждений. Остановить From eef01ad8f9cc9941a584fd1bd6ddef61202b2dcd Mon Sep 17 00:00:00 2001 From: KuriakiMariaHere Date: Sat, 8 Jul 2023 21:05:33 +0000 Subject: [PATCH 35/65] Translated using Weblate (Greek) Currently translated at 17.1% (452 of 2640 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/el/ --- library/ui-strings/src/main/res/values-el/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-el/strings.xml b/library/ui-strings/src/main/res/values-el/strings.xml index 3d148ae2bf..807a62c10c 100644 --- a/library/ui-strings/src/main/res/values-el/strings.xml +++ b/library/ui-strings/src/main/res/values-el/strings.xml @@ -493,4 +493,5 @@ %1$s, %2$s, %3$s και %4$d άλλος χρήστης %1$s, %2$s, %3$s και %4$d άλλοι χρήστες + Το μήνυμα στέλνεται… \ No newline at end of file From 6fd589440d0956a0d1476851b615e100d8abdd32 Mon Sep 17 00:00:00 2001 From: KuriakiMariaHere Date: Sun, 9 Jul 2023 20:17:38 +0000 Subject: [PATCH 36/65] Translated using Weblate (Greek) Currently translated at 17.6% (465 of 2640 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/el/ --- .../src/main/res/values-el/strings.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/library/ui-strings/src/main/res/values-el/strings.xml b/library/ui-strings/src/main/res/values-el/strings.xml index 807a62c10c..3c8ec19a78 100644 --- a/library/ui-strings/src/main/res/values-el/strings.xml +++ b/library/ui-strings/src/main/res/values-el/strings.xml @@ -494,4 +494,21 @@ %1$s, %2$s, %3$s και %4$d άλλοι χρήστες Το μήνυμα στέλνεται… + Μύνημα + Δημιουργώ έναν σύνδεσμο + Σύνδεσμος + Στέλνω ένα αρχείο. + Μύνημα από τον χρήστη %s + Στέλνω ένα ηχητικό μύνημα. + Στέλνω μια εικόνα. + + Πρόσθεσες εναλλακτική διεύθυνση %1$ για αυτό το δωμάτιο. + Πρόσθεσες εναλλακτικές διευθύνσεις %1$ για αυτό το δωμάτιο. + + Στέλνω ένα βίντεο. + Στέλνω ένα αρχείο ήχου. + Επεξεργάζομαι ένα σύνδεσμο + Κείμενο + Στέλνω ένα αυτοκόλλητο. + Σκανάρω ένα QR code \ No newline at end of file From c08b99a4f1dd4cd29fb92a523417152f87e370f8 Mon Sep 17 00:00:00 2001 From: Nui Harime Date: Mon, 10 Jul 2023 08:27:08 +0000 Subject: [PATCH 37/65] Translated using Weblate (Russian) Currently translated at 99.7% (2629 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ru/ --- library/ui-strings/src/main/res/values-ru/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml index 210d60a827..71b95e0e28 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -3055,4 +3055,5 @@ Возобновить %1$s изменил(а) имя на %2$s Запрос на верификацию не найден. Возможно, он был отменен или обработан другим сеансом. + Цитата \ No newline at end of file From 841028774e596b4c14d9f5cc207819580f059f22 Mon Sep 17 00:00:00 2001 From: Nui Harime Date: Mon, 10 Jul 2023 13:46:44 +0000 Subject: [PATCH 38/65] Translated using Weblate (Russian) Currently translated at 99.8% (2633 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ru/ --- library/ui-strings/src/main/res/values-ru/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml index 71b95e0e28..33814ca5bf 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -3056,4 +3056,8 @@ %1$s изменил(а) имя на %2$s Запрос на верификацию не найден. Возможно, он был отменен или обработан другим сеансом. Цитата + Версия шифрования + Блок кода + Подпункт + Пункт \ No newline at end of file From 8eccae44e5eaea319e059ac92d5d5aeb7b3427ad Mon Sep 17 00:00:00 2001 From: Edgars Andersons Date: Tue, 11 Jul 2023 05:31:31 +0000 Subject: [PATCH 39/65] Translated using Weblate (Latvian) Currently translated at 69.9% (1844 of 2636 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/lv/ --- .../src/main/res/values-lv/strings.xml | 58 ++++++++++++++----- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/library/ui-strings/src/main/res/values-lv/strings.xml b/library/ui-strings/src/main/res/values-lv/strings.xml index 2760d06bc3..4570016bc6 100644 --- a/library/ui-strings/src/main/res/values-lv/strings.xml +++ b/library/ui-strings/src/main/res/values-lv/strings.xml @@ -312,7 +312,7 @@ Noraidīt Pāriet uz pirmo neizlasīto ziņu Pamest istabu - Vai tiešām vēlies pamest istabu\? + Vai tiešām pamest istabu\? TIEŠIE ČATI Uzaicināt Nobanot @@ -342,7 +342,7 @@ Istabas dalībnieku filtrs Rezultātu nav Visas ziņas - Pievienot uz galvenā ekrāna + Pievienot sākumekrānam Profila attēls Attēlojamais vārds Pievienot epasta adresi @@ -585,10 +585,10 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Drošība Ziņas šeit ir nodrošinātas ar pilnīgu šifrēšanu. \n -\nJūsu ziņas tiek nodrošinātas ar slēdzenēm, un tikai jums un saņēmējam ir unikālas atslēgas, lai tās atbloķētu. +\nZiņas tiek nodrošinātas ar slēdzenēm, un tikai sūtītājam un saņēmējam ir neatkārtojamas atslēgas, lai tās atslēgtu. Ziņas šeit ir nodrošinātas ar pilnīgu šifrēšanu. \n -\nJūsu ziņas tiek nodrošinātas ar slēdzenēm, un tikai jums un saņēmējam ir unikālas atslēgas, lai tās atbloķētu. +\nZiņas tiek nodrošinātas ar slēdzenēm, un tikai sūtītājam un saņēmējam ir neatkārtojamas atslēgas, lai tās atslēgtu. Ziņas šeit nav nodrošinātas ar pilnīgu šifrēšanu. Ziņām šajā istabā netiek piemērota pilnīga šifrēšana. Jūs akceptējāt @@ -694,7 +694,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Deaktivizēt kontu Nomaina jūsu parādāmo vārdu Padzen lietotāju ar norādīto id - Atstāj istabu + Pamest istabu Uzaicina lietotāju ar norādīto id uz pašreizējo istabu Atceļ operatora statusu lietotājam ar norādīto Id Definē lietotāja statusu @@ -751,7 +751,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Jūs nevarēsiet atcelt šīs izmaiņas pēc sava statusa pazemināšanas. Gadījumā, ja esat pēdējais priviliģētais lietotājs istabā, būs neiespējami atgūt šīs privilēģijas. Padzīt Atcelt uzaicinājumu - Šī istaba nav publiska un jūs nevarēsiet atkārtoti pievienoties bez uzaicinājuma. + Šī istaba nav publiska. Tai nebūs iespējams atkārtoti pievienoties bez uzaicinājuma. Atļaujiet piekļuvi savām kontaktpersonām. Lai skenētu QR kodu, jums jāatļauj piekļuve kamerai. Notiek video zvans… @@ -967,7 +967,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Istabu katalogs Jaunā vērtība Pārslēgt - Jūs nevarat uzsākt zvanu ar sevi, pagaidiet, kamēr dalībnieki akceptēs uzaicinājumu + Nevar uzsākt zvanu ar sevi, jāpagaida, līdz dalībnieki apstiprinās uzaicinājumu Jūs nevarat uzsākt zvanu ar sevi Sūtīt uzlīmi Sakarā ar pilnīgu šifrēšanu, jums var būt nepieciešams sagaidīt ziņu no kāda, jo šifrēšanas atslēgas netika pareizi nosūtītas jums. @@ -1007,7 +1007,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Izmantot atkopšanās frāzveida paroli vai atslēgu Izmantojiet citu sesiju šis sesijas verifikācijai, tādējādi dodot piekļuvi šifrētām ziņām. Iespējot šifrēšanu - Pēc iespējošanas šifrēšana istabai nevar tikt izslēgta. Šifrētā istabā sūtītas ziņas nav redzamas serverim, tikai istabas dalībniekiem. Šifrēšanas iespējošana var neļaut pareizi strādāt daudzus botiem un tiltiem. + Tiklīdz iespējots, istabas šifrēšanu nevar atspējot. Šifrētā istabā sūtītas ziņas nav redzamas serverim, tikai istabas dalībniekiem. Šifrēšanas iespējošana var neļaut pareizi strādāt daudzām robotprogrammatūrām un tiltiem. Jums nav atļaujas, lai iespējotu šifrēšanu šajā istabā. Viena persona @@ -1387,11 +1387,11 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Lūdzu ievadiet paroli. Lūdzu ievadiet lietotājvārdu. Lūdzu aizmirst visas ziņas, ko esmu nosūtījis, kad mans konts ir deaktivizēts (Brīdinājums: šis nākotnes lietotājiem neļaus redzēt visu sarunu kontekstu) - Tas padarīs jūsu kontu nelietojamu. Jūs nevarēsiet ierakstīties, un neviens nevarēs atkārtoti reģistrēt to pašu lietotāja ID. Tas liks jūsu kontu atstāt visas istabas, kurās piedalās, un tas noņems jūsu konta datus no jūsu identitātes servera. Šī darbība ir neatgriezeniska . + Tas padarīs kontu neizmantojamu. Nebūs iespējams pieteikties, un neviens nevarēs atkārtoti reģistrēties ar to pašu lietotāja identifikatoru. Tas liks kontam atstāt visas istabas, kurās piedalās, un tas noņems konta datus no identitātes servera. Šī darbība ir neatgriezeniska . \n -\nDeaktivizējot savu kontu , pēc noklusējuma, neliek mums aizmirst jūsu nosūtītās ziņas . Ja jūs vēlētos, lai mēs aizmirstu jūsu ziņas, lūdzu, atzīmējiet zemāk redzamo lodziņu. +\nKonta deaktivēšana pēc noklusējuma neliek mums aizmirst nosūtītās ziņas . Ja vēlme, lai mēs aizmirstu ziņas, lūgums atzīmēt zemāk esošo lodziņu. \n -\nZiņu redzamība Matrix ir līdzīga e-pastam. Mums aizmirst jūsu ziņas nozīmē, ka ziņas, ko esat nosūtījis, netiks rādītas jauniem vai nereģistrētiem lietotājiem, bet reģistrētiem lietotājiem, kuriem jau ir piekļuve šīm ziņām, joprojām būs piekļuve to kopijai. +\nZiņu redzamība Matrix ir līdzīga e-pastam. Ziņu aizmiršana nozīmē, ka nosūtītās ziņas netiks rādītas jauniem vai nereģistrētiem lietotājiem, bet reģistrētiem lietotājiem, kuriem jau ir piekļuve šīm ziņām, joprojām būs piekļuve to kopijai. Atslēgas Dalīšanās Pieprasījums Nav aktīvu logrīku Izmantot mikrofonu @@ -1399,7 +1399,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Bloķēt Visu Atļaut Šis logrīks vēlas izmantos tālāk norādītos resursus: - Iziet no pašreizējās konferences un pāriet uz citu\? + Pamest pašreizējo apspriedi un pārslēgties uz otru\? Atvainojiet, konferences zvani ar Jitsi netiek atbalstīti vecās ierīces (ierīces ar Android OS zem 6.0) Istabas ID Logrīka ID @@ -1661,7 +1661,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. Priekšskatīt failu pirms nosūtīšanas ${app_name} sinhronizēsies fonā periodiski, precīzi, noteiktā laikā (konfigurējams). \nTas ietekmēs radio un akumulatora izmantošanu, tiks parādīts pastāvīgs paziņojums par to, ka ${app_name} klausās notikumus. - Ja lietotājs kādu laiku atstāj ierīci atvienotu no tīkla un nekustīgu, ar izslēgtu ekrānu, ierīcē tiek ieslēgts dīkstāves režīms. Tas neļauj lietotnēm piekļūt tīklam un atliek to darbu izpildi, sinhronizāciju un standarta trauksmes signālus. + Ja lietotājs kādu laiku atstāj ierīci atvienotu no tīkla un nekustīgu, ar izslēgtu ekrānu, ierīcē tiek ieslēgts dīkstāves režīms. Tas neļauj lietotnēm piekļūt tīklam un atliek to darbu izpildi, sinhronizāciju un standarta brīdinājumus. Pakalpojums nesāksies, kad ierīce tiks restartēta, jūs nesaņemsiet paziņojumus, kamēr vismaz vienreiz nebūsiet atvēris ${app_name}. Lūdzu, noklikšķiniet uz paziņojuma. Ja paziņojums nav redzams, pārbaudiet sistēmas iestatījumus. @@ -2092,4 +2092,36 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka. \n \nJāņem vērā, ka šī darbība pārsāknēs lietotni, un tas var aizņemt kādu laiku. - atsevišķi lietotāji atkal tiek ņemti vērā + Ne tagad + Skatīt pavedienus + Jā, apturēt + Pamest + h + Nākamais + Nepamest neko + Izmēģināt + Uzzināt vairāk + Skatīt istabā + Ievietot pavediena saiti starpliktuvē + Atspējot + Nekad nesūtīt šifrētas ziņas uz neapstiprinātām šīs istabas sesijām. + Izkārtojuma uzstādījumi + Rādīt nesenos + Kārtot pēc + Vienalga atteikties + Iespējot + Atlasīt visu + Noņemt visas atzīmes + Pamest istabu ar norādīto identifikatoru (vai pašreizējo istabu, ja tāda nav) + Pamest visu + Vai tiešām pamest %s\? + Serveris nav sasniedzams. Ja vienalga tiks veikta atteikšanās, šī ierīce netiks izdzēsta no ierīču saraksta, tādēļ nepieciešamības gadījumā tā ir noņemama citā klientā. + Tu šeit esi vienīgais cilvēks. Ja pametīsi, neviens vairs nevarēs pievienoties, ieskaitot Tevi. + Aptauju vēsture + min. + sek. + Turpināt, lai atiestatītu + Skaidrs + Rādīt atlasītājus + Darbība \ No newline at end of file From 2a5df54ae42980924b659e72a4eafbfd484e0148 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Jun 2023 14:15:06 +0200 Subject: [PATCH 40/65] Fix crash: show an error message with a Retry button when there is no network when displaying the BootstrapBottomSheet. --- .../crypto/recover/BootstrapActions.kt | 1 + .../crypto/recover/BootstrapBottomSheet.kt | 5 ++ .../crypto/recover/BootstrapErrorFragment.kt | 54 ++++++++++++++ .../recover/BootstrapSharedViewModel.kt | 71 ++++++++++++------- .../features/crypto/recover/BootstrapStep.kt | 2 + .../res/layout/fragment_bootstrap_error.xml | 33 +++++++++ 6 files changed, 141 insertions(+), 25 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapErrorFragment.kt create mode 100644 vector/src/main/res/layout/fragment_bootstrap_error.xml diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt index 395b4d0475..fd61bdd5dd 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt @@ -20,6 +20,7 @@ import im.vector.app.core.platform.VectorViewModelAction import java.io.OutputStream sealed class BootstrapActions : VectorViewModelAction { + object Retry : BootstrapActions() // Navigation diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt index be02737ef8..586f60beaf 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt @@ -212,6 +212,11 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment { + views.bootstrapIcon.isVisible = true + views.bootstrapTitleText.text = getString(R.string.bottom_sheet_setup_secure_backup_title) + showFragment(BootstrapErrorFragment::class) + } } super.invalidate() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapErrorFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapErrorFragment.kt new file mode 100644 index 0000000000..26b29d449a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapErrorFragment.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.crypto.recover + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentBootstrapErrorBinding + +@AndroidEntryPoint +class BootstrapErrorFragment : + VectorBaseFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapErrorBinding { + return FragmentBootstrapErrorBinding.inflate(inflater, container, false) + } + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun invalidate() = withState(sharedViewModel) { state -> + when (state.step) { + is BootstrapStep.Error -> { + views.bootstrapDescriptionText.setTextOrHide(errorFormatter.toHumanReadable(state.step.error)) + } + else -> { + // Should not happen, show a generic error + views.bootstrapDescriptionText.setTextOrHide(getString(R.string.unknown_error)) + } + } + views.bootstrapRetryButton.onClick { + sharedViewModel.handle(BootstrapActions.Retry) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt index b3d83b948b..c9c2c5ce9a 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt @@ -54,6 +54,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromR import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import timber.log.Timber import java.io.OutputStream import kotlin.coroutines.Continuation import kotlin.coroutines.resumeWithException @@ -118,37 +119,48 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } SetupMode.NORMAL -> { - // need to check if user have an existing keybackup - setState { - copy(step = BootstrapStep.CheckingMigration) - } + checkMigration() + } + } + } - // We need to check if there is an existing backup - viewModelScope.launch(Dispatchers.IO) { - val version = tryOrNull { session.cryptoService().keysBackupService().getCurrentVersion() }?.toKeysVersionResult() - if (version == null) { - // we just resume plain bootstrap - doesKeyBackupExist = false + private fun checkMigration() { + // need to check if user have an existing keybackup + setState { + copy(step = BootstrapStep.CheckingMigration) + } + + // We need to check if there is an existing backup + viewModelScope.launch(Dispatchers.IO) { + try { + val version = tryOrNull { session.cryptoService().keysBackupService().getCurrentVersion() }?.toKeysVersionResult() + if (version == null) { + // we just resume plain bootstrap + doesKeyBackupExist = false + setState { + copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod)) + } + } else { + // we need to get existing backup passphrase/key and convert to SSSS + val keyVersion = tryOrNull { + session.cryptoService().keysBackupService().getVersion(version.version) + } + if (keyVersion == null) { + // strange case... just finish? + _viewEvents.post(BootstrapViewEvents.Dismiss(false)) + } else { + doesKeyBackupExist = true + isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null setState { copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod)) } - } else { - // we need to get existing backup passphrase/key and convert to SSSS - val keyVersion = tryOrNull { - session.cryptoService().keysBackupService().getVersion(version.version) - } - if (keyVersion == null) { - // strange case... just finish? - _viewEvents.post(BootstrapViewEvents.Dismiss(false)) - } else { - doesKeyBackupExist = true - isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null - setState { - copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod)) - } - } } } + } catch (failure: Throwable) { + Timber.e(failure, "Error while checking key backup") + setState { + copy(step = BootstrapStep.Error(failure)) + } } } } @@ -268,6 +280,9 @@ class BootstrapSharedViewModel @AssistedInject constructor( copy(step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.authentication_error))) } } + BootstrapActions.Retry -> { + checkMigration() + } } } @@ -568,6 +583,12 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } + is BootstrapStep.Error -> { + // do we let you cancel from here? + if (state.canLeave) { + _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt index 3975f0e9a2..b807acc0ba 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt @@ -105,6 +105,8 @@ sealed class BootstrapStep { object Initializing : BootstrapStep() data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep() object DoneSuccess : BootstrapStep() + + data class Error(val error: Throwable) : BootstrapStep() } fun BootstrapStep.GetBackupSecretForMigration.useKey(): Boolean { diff --git a/vector/src/main/res/layout/fragment_bootstrap_error.xml b/vector/src/main/res/layout/fragment_bootstrap_error.xml new file mode 100644 index 0000000000..02c944fed2 --- /dev/null +++ b/vector/src/main/res/layout/fragment_bootstrap_error.xml @@ -0,0 +1,33 @@ + + + + + +