Merge branch 'release/1.6.5' into main

This commit is contained in:
Benoit Marty 2023-07-25 14:56:37 +02:00
commit fd6a45a3ae
90 changed files with 5680 additions and 679 deletions

View file

@ -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 }}

View file

@ -1,3 +1,16 @@
Changes in Element v1.6.5 (2023-07-25)
======================================
Bugfixes 🐛
----------
- Fix several crashes observed when the device cannot reach the homeserver ([#8578](https://github.com/vector-im/element-android/issues/8578))
Other changes
-------------
- Update MSC3912 implementation: Redaction of related events ([#8481](https://github.com/vector-im/element-android/issues/8481))
- Include some source code in our project to remove our dependency to artifact hosted by bintray (Jcenter). ([#8556](https://github.com/vector-im/element-android/issues/8556))
Changes in Element v1.6.3 (2023-06-27)
======================================

View file

@ -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 {

View file

@ -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',
@ -234,18 +235,4 @@ ext.groups = [
'xml-apis',
]
],
jcenter : [
regex: [
],
group: [
'com.amulyakhare',
'com.otaliastudios',
'com.yqritc',
// https://github.com/cmelchior/realmfieldnameshelper/issues/42
'dk.ilios',
'im.dlg',
'me.dm7.barcodescanner',
'me.gujun.android',
]
]
]

View file

@ -0,0 +1,2 @@
Main changes in this version: corrective release.
Full changelog: https://github.com/vector-im/element-android/releases

View file

@ -0,0 +1 @@
Խմբային մեսինջեր - գաղտնագրված շփում, խմբային չատեր և վիդեո զանգեր

View file

@ -0,0 +1 @@
Element - Անվտանգ Մեսինջեր

View file

@ -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ẽ.
<b> Các tính năng của Element bao gồm: </b>
- Các công cụ giao tiếp trực tuyến tiên tiến
<b>Các tính năng của Element bao gồm:</b>
- 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ọ.
<b> Nhắn tin mã hóa và riêng tư </b>
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.
<b>Nhắn tin mã hóa và riêng tư</b>
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.
<b> Element có thể được tự lưu trữ </b>
Để 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.
<b>Element có thể được tự vận hành</b>
Để 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.
<b> Sở hữu dữ liệu của bạn </b>
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.
<b>Sở hữu dữ liệu của bạn</b>
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
<b> Nhắn tin và cộng tác mở </b>
<b>Nhắn tin và cộng tác mở</b>
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.
<b> Siêu bảo mật </b>
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.
<b>Siêu bảo mật</b>
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 thực chéo thiết bị.
<b> Giao tiếp và tích hợp hoàn chỉnh </b>
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.
<b>Giao tiếp và tích hợp hoàn chỉnh</b>
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.
<b> Tiếp tục nơi bạn đã dừng lại </b>
<b>Tiếp tục nơi bạn đã dừng lạ </b>
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
<b> Mã nguồn mở </b>
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
<b>Mã nguồn mở</b>
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

View file

@ -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
}
}

View file

@ -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<T> 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 <T> the data model
*/
public final static class Builder<T> {
private EditText source;
private AutocompletePresenter<T> presenter;
private AutocompletePolicy policy;
private AutocompleteCallback<T> 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<T> with(AutocompletePresenter<T> 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<T> with(AutocompleteCallback<T> 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<T> with(AutocompletePolicy policy) {
this.policy = policy;
return this;
}
/**
* Sets a background drawable for the popup.
*
* @param backgroundDrawable drawable
* @return this for chaining
*/
public Builder<T> 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<T> 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<T> 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<T>(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 <T> your data model
* @return a Builder for set up
*/
public static <T> Builder<T> on(EditText anchor) {
return new Builder<T>(anchor);
}
private AutocompletePolicy policy;
private AutocompletePopup popup;
private AutocompletePresenter<T> presenter;
private AutocompleteCallback<T> callback;
private EditText source;
private boolean block;
private boolean disabled;
private boolean openBefore;
private String lastQuery = "null";
private Autocomplete(Builder<T> 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<T>() {
@Override
public void click(@NonNull T item) {
AutocompleteCallback<T> 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) {}
}
}

View file

@ -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<T> {
/**
* 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);
}

View file

@ -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);
}

View file

@ -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.
*
* <p>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.
*
* <p>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.</p>
*
* @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);
}
}
/**
* <p>Builds the popup window's content and returns the height the popup
* should have. Returns -1 when the content already exists.</p>
*
* @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);
}
}

View file

@ -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<T> {
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<T> 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<T> {
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;
}
}

View file

@ -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)};
}
}

View file

@ -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 <T> your model object (the object displayed by the list)
*/
public abstract class RecyclerViewPresenter<T> extends AutocompletePresenter<T> {
private RecyclerView recycler;
private ClickProvider<T> clicks;
private Observer observer;
public RecyclerViewPresenter(@NonNull Context context) {
super(context);
}
@Override
protected final void registerClickProvider(@NonNull ClickProvider<T> 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();
}
}
}

View file

@ -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
}
}

View file

@ -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");
}
}
/**
* <p>Method that creates view that represents visual appearance of a barcode scanner</p>
* <p>Override it to provide your own view for visual appearance of a barcode scanner</p>
*
* @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;
}
}

View file

@ -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));
}
});
}
});
}
}

View file

@ -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<Camera.Size> 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);
}
}

View file

@ -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<String> 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;
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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. <br/>
* For example see: {@link ViewFinderView#setupViewFinder()}
*/
void setupViewFinder();
/**
* Provides {@link Rect} that identifies area where barcode scanner can detect visual codes
* <p>Note: This rect is a area representation in absolute pixel values. <br/>
* For example: <br/>
* If View's size is 1024x800 so framing rect might be 500x400</p>
*
* @return {@link Rect} that identifies barcode scanner area
*/
Rect getFramingRect();
/**
* Width of a {@link android.view.View} that implements this interface
* <p>Note: this is already implemented in {@link android.view.View},
* so you don't need to override method and provide your implementation</p>
*
* @return width of a view
*/
int getWidth();
/**
* Height of a {@link android.view.View} that implements this interface
* <p>Note: this is already implemented in {@link android.view.View},
* so you don't need to override method and provide your implementation</p>
*
* @return height of a view
*/
int getHeight();
}

View file

@ -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);
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="viewfinder_border_width">4</integer>
<integer name="viewfinder_border_length">60</integer>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="viewfinder_border_width">5</integer>
<integer name="viewfinder_border_length">80</integer>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="viewfinder_border_width">6</integer>
<integer name="viewfinder_border_length">100</integer>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BarcodeScannerView">
<attr name="shouldScaleToFill" format="boolean" />
<attr name="laserEnabled" format="boolean" />
<attr name="laserColor" format="color" />
<attr name="borderColor" format="color" />
<attr name="maskColor" format="color" />
<attr name="borderWidth" format="dimension" />
<attr name="borderLength" format="dimension" />
<attr name="roundedCorner" format="boolean" />
<attr name="cornerRadius" format="dimension" />
<attr name="squaredFinder" format="boolean" />
<attr name="borderAlpha" format="float" />
<attr name="finderOffset" format="dimension" />
</declare-styleable>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="viewfinder_mask">#60000000</color>
<color name="viewfinder_laser">#ffcc0000</color>
<color name="viewfinder_border">#ffafed44</color>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="viewfinder_border_width">4</integer>
<integer name="viewfinder_border_length">60</integer>
</resources>

View file

@ -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
}
}

View file

@ -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<BarcodeFormat> ALL_FORMATS = new ArrayList<>();
private List<BarcodeFormat> 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<BarcodeFormat> formats) {
mFormats = formats;
initMultiFormatReader();
}
public void setResultHandler(ResultHandler resultHandler) {
mResultHandler = resultHandler;
}
public Collection<BarcodeFormat> getFormats() {
if(mFormats == null) {
return ALL_FORMATS;
}
return mFormats;
}
private void initMultiFormatReader() {
Map<DecodeHintType,Object> 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;
}
}

View file

@ -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

View file

@ -0,0 +1,23 @@
apply plugin: 'kotlin'
apply plugin: 'java'
sourceCompatibility = versions.sourceCompat
targetCompatibility = versions.sourceCompat
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'
}

View file

@ -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<String, String?>() // <fieldName, linkedType or null>
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
}
}
}

View file

@ -0,0 +1,79 @@
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 "&lt;class&gt;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)
}
}

View file

@ -0,0 +1,77 @@
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 "&lt;class&gt;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<ClassData>): Boolean {
return fileData
.filter { !it.libraryClass }
.all { generateFile(it, fileData) }
}
private fun generateFile(classData: ClassData, classPool: Set<ClassData>): 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)
}
}

View file

@ -0,0 +1,197 @@
package dk.ilios.realmfieldnames
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.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
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<ClassData>()
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
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<TypeElement>, 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<String, ClassData>()
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
}
}

View file

@ -0,0 +1 @@
dk.ilios.realmfieldnames.RealmFieldNamesProcessor,aggregating

View file

@ -0,0 +1 @@
dk.ilios.realmfieldnames.RealmFieldNamesProcessor

20
library/external/span/build.gradle vendored Normal file
View file

@ -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'
}

View file

@ -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<Any> = ArrayList()
var style: Span = EMPTY_STYLE
private fun buildCharacterStyle(builder: ArrayList<Any>) {
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<Any>) {
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<Any>()
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)
}

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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")
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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);
}
}

View file

@ -2957,4 +2957,12 @@
<string name="crosssigning_verify_after_update">App aktualisiert</string>
<string name="sign_out_failed_dialog_message">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.</string>
<string name="sign_out_anyway">Dennoch abmelden</string>
<string name="create_room_unknown_users_dialog_submit">Dennoch Unterhaltung beginnen</string>
<string name="create_room_unknown_users_dialog_content">Konnte keine Profile für die folgenden Matrix-IDs finden. Möchtest du dennoch eine Unterhaltung beginnen\?
\n
\n%s</string>
<string name="invite_unknown_users_dialog_content">Konnte keine Profile für die folgenden Matrix-IDs finden. Möchtest du sie dennoch einladen\?
\n
\n%s</string>
<string name="invite_unknown_users_dialog_submit">Dennoch einladen</string>
</resources>

View file

@ -360,11 +360,11 @@
\nΜε την απενεργοποίηση του λογαρισμού σας <b>δεν θα ξεχαστούν τα μηνύματά που έχετε στείλει</b>. Εάν θα θέλατε να ξεχαστούν, τότε δηλώστε το στο κουτί πιο κάτω.
\n
\nΗ ορατότητα μηνυμάτων δουλεύει σαν το ηλεκτρονικό ταχυδρομείο. Το να ξεχαστούν τα μηνύματα σημαίνει οτι μηνύματα τα οποία έχετε στείλει δεν θα σταλούν σε νέους ή μη-εγγεγραμμένους χρήστες, αλλα εγγεγραμμένοι χρήστες οι οποίοι έχουν ήδη πρόσβαση σε αυτά τα μηνύματα θα μπορούν ακόμα να τα δούν.</string>
<string name="room_tombstone_versioned_description">Αυτό το δωμάτιο έχει αντικατασταθεί και δεν είναι πια ενεργό</string>
<string name="room_tombstone_versioned_description">Αυτό το δωμάτιο έχει αντικατασταθεί και δεν είναι πια ενεργό.</string>
<string name="ssl_remain_offline">Παράβλεψη</string>
<string name="ssl_logout_account">Αποσύνδεση</string>
<string name="ssl_do_not_trust">Δεν εμπιστεύομαι</string>
<string name="room_do_not_have_permission_to_post">Δεν έχετε εξουσιοδότηση να δημοσιεύσετε σε αυτό το δωμάτιο</string>
<string name="room_do_not_have_permission_to_post">Δεν έχετε εξουσιοδότηση να δημοσιεύσετε σε αυτό το δωμάτιο.</string>
<string name="room_many_users_are_typing">%1$s &amp; %2$s &amp; άλλοι γράφουν…</string>
<string name="room_one_user_is_typing">%s γράφει…</string>
<string name="room_two_users_are_typing">%1$s &amp; %2$s γράφουν…</string>
@ -383,4 +383,132 @@
<string name="location_share_external">Άνοιγμα με</string>
<string name="tooltip_attachment_sticker">Αποστολή αυτοκόλλητου</string>
<string name="notice_room_created">%1$s δημιούργησε το δωμάτιο</string>
</resources>
<string name="notice_direct_room_join_by_you">Εισήλθες</string>
<string name="notice_room_leave_by_you">Αποχώρησες από το δωμάτιο</string>
<string name="notice_direct_room_leave">%1$s αποχώρισε από το δωμάτιο</string>
<string name="notice_room_reject_by_you">Έχετε απορρίψει την πρόσκληση</string>
<string name="notice_display_name_set_by_you">Ορίσατε το όνομά σας ω; %1$s</string>
<string name="notice_room_invite_no_invitee_by_you">Η πρόσκλησή σου</string>
<string name="notice_room_created_by_you">Δημιούργησες αυτό το δωμάτιο</string>
<string name="notice_direct_room_created">%1$s δημιούργησε την συζήτηση</string>
<string name="notice_room_invite_by_you">"Προσκάλεσες τον χρήστη %1$s"</string>
<string name="notice_room_ban_by_you">Έχετε αποκλείσει τον χρήστη %1$s</string>
<string name="notice_avatar_url_changed_by_you">Αλλάξατε το άβατάρ σας</string>
<string name="notice_room_topic_changed_by_you">Άλλαξες το θέμα σε: %1$s</string>
<string name="pill_message_unknown_room_or_space">Δωμάτιο/Χώρος</string>
<string name="soft_logout_signin_submit">Σύνδεση</string>
<plurals name="x_selected">
<item quantity="one">%1$d επιλέχθηκε</item>
<item quantity="other">%1$d επιλέχθηκαν</item>
</plurals>
<plurals name="notice_room_server_acl_changes">
<item quantity="one">%d ACLs του server άλλαξε</item>
<item quantity="other">%d ACLs του server άλλαξαν</item>
</plurals>
<string name="qr_code_login_connecting_to_device">Πραγματοποιείται σύνδεση στην συσκευή</string>
<string name="qr_code_login_status_no_match">Δεν ταιριάζει;</string>
<string name="qr_code_login_try_again">Προσπάθησε ξανά</string>
<string name="notice_room_server_acl_set_allowed">"Οι servers που περιλαμβάνουν %s επιτρέπονται."</string>
<string name="notice_direct_room_created_by_you">Δημιούργησες αυτή την συζήτηση</string>
<string name="notice_direct_room_leave_by_you">Αποχώρισες από το δωμάτιο</string>
<string name="notice_room_unban">Ο χρήστης %1$s αφαίρεσε τον αποκλείσμο του χρήστη %2$s</string>
<string name="notice_room_name_changed_by_you">Άλλαξες το όνομα του δωματίου σε: %1$s</string>
<string name="notice_placed_video_call_by_you">Έχετε ξεκινήσει μία βιντεοκλήση.</string>
<string name="notice_made_future_room_visibility_by_you">Το μελλοντικό ιστορικό του δωματίου θα είναι εμφανή στο χρήστη %1$s</string>
<string name="notice_placed_voice_call_by_you">Έχετε ξεκινησει μία κλήση.</string>
<string name="notice_room_update">Ο χρήστης %s αναβάθμισε αυτό το δωμάτιο.</string>
<string name="notice_room_server_acl_set_banned">Η εύρεση server που περιλαμβάνει %s είναι απεκλεισμένη.</string>
<string name="notice_room_server_acl_updated_banned">"• Οι servers οπού περιλαμβάνουν %s είναι τώρα απεκλεισμένοι."</string>
<string name="qr_code_login_confirm_security_code">Επιβεβαίωση</string>
<string name="qr_code_login_confirm_security_code_description">Παρακαλώ επιβεβαιώστε ότι γνωρίζετε την προέλευση του κώδικα αυτού. Όταν συνδέετε νέες συσκευές, ενδεχομένως να δίνετε σε κάποιον άλλο πλήρης πρόσβαση στον λογαριασμό σας.</string>
<string name="soft_logout_clear_data_title">Εκαθάρριση προσωπικών δεδομένων</string>
<string name="notice_room_remove_by_you">Αφαιρέσατε τον χρήστη %1$s</string>
<string name="notice_display_name_changed_to">Ο χρήστης %1$s άλλαξε το όνομά του σε %2$s</string>
<string name="notice_room_avatar_changed">%1$s άλλαξε το άβαταρ του δωματίου</string>
<string name="notice_room_avatar_changed_by_you">Άλλαξες το άβαταρ του δωματίου</string>
<string name="notice_call_candidates">"%s απέστειλε δεδομένα ώστε να πραγματοποιηθεί η κλήση."</string>
<string name="notice_call_candidates_by_you">Απέστειλες δεδομένα ώστε να ξεκινήσει η κλήση.</string>
<string name="notice_answered_call_by_you">Απάντησες στην κλήση.</string>
<string name="notice_made_future_direct_room_visibility">Ο χρήστης %1$s έκανε τα μελλοντικά μηνύματα διαθέσιμα στον χρήστη %2$s</string>
<string name="notice_room_update_by_you">Έχετε αναβαθμίσει αυτό το δωμάτιο.</string>
<string name="notice_direct_room_update">%s αναβαθμίστηκε εδώ.</string>
<string name="notice_direct_room_update_by_you">Αναβαθμίστηκες εδώ.</string>
<string name="notice_room_server_acl_set_title">%s όρισε τα ACLs αυτού του server σε αυτό το δωμάτιο.</string>
<string name="notice_room_server_acl_set_title_by_you">Έχεις ορίσει το ACL του server για αυτό το δωμάτιο.</string>
<string name="notice_room_server_acl_set_ip_literals_not_allowed">"• Οι servers που περιλαμβάνονται αυτή την IP είναι απεκλεισμένοι."</string>
<string name="notice_room_server_acl_updated_title">%s άλλαξε τα ACLs του server γι αυτό το δωμάτιο.</string>
<string name="notice_room_server_acl_updated_title_by_you">Άλλαξες τα ACLs του server γι αυτό το δωμάτιο.</string>
<string name="soft_logout_signin_password_hint">Κωδικός Πρόσβασης</string>
<string name="soft_logout_signin_notice">Οι διαχειριστές του server σας (%1$s), σας έχουν αποσυνδέσει από τον λογαριασμό σας %2$s (%3$s).</string>
<string name="bug_report_error_too_short">Η πειργραφή είναι πολύ σύντομη</string>
<string name="soft_logout_title">Έχετε αποσυνδεθεί</string>
<string name="settings_advanced_settings">Επιπρόσθετες ρυθμίσεις</string>
<string name="settings_developer_mode">Λειτουργίες Προγραμματιστή</string>
<string name="devices_current_device">Τρέχουσα συνεδρία</string>
<string name="soft_logout_clear_data_dialog_title">Διαγραφή δεδομένων</string>
<string name="devices_other_devices">Άλλες συνεδρίες</string>
<string name="qr_code_login_signing_in">Πραγματοποιείται η σύνδεση</string>
<string name="notice_room_join_by_you">Συμμετέχεις σε αυτό το δωμάτιο</string>
<string name="notice_direct_room_join">%1$s εισήλθε</string>
<string name="notice_room_unban_by_you">Έχετε αφαιρέσει τον αποκλεισμό από τον χρήστη %1$s</string>
<string name="notice_room_withdraw_by_you">Αφαιρέσατε την πρόσκληση του χρήστη %1$s</string>
<string name="notice_display_name_changed_from_by_you">Άλλαξες το όνομά σου από %1$s σε %2$s</string>
<string name="notice_display_name_removed_by_you">Αφαίρεσες το όνομά όπου εμφανιζόταν ο λογαριασμός σου ( προηγουμένως ήταν %1$s)</string>
<string name="notice_ended_call_by_you">.Τερμάτισες την κλήση.</string>
<string name="notice_made_future_direct_room_visibility_by_you">Έκανες τα μελλοντικά μηνύματα διαθέσιμα στον χρήστη %1$s</string>
<string name="rich_text_editor_link">Ορισμός συνδέσμου</string>
<string name="signed_out_submit">Συνδεθείτε ξανά</string>
<string name="soft_logout_signin_title">Σύνδεση</string>
<string name="soft_logout_clear_data_submit">Διαγρφή όλων των δεδομένων</string>
<string name="settings">Ρυθμίσεις</string>
<string name="settings_general_title">Γενικά</string>
<string name="create_spaces_default_public_room_name">Γενικά</string>
<string name="notice_room_avatar_removed">%1$s Έχει αφαιρεθεί το avatar του δωματίου</string>
<string name="notice_room_avatar_removed_by_you">Έχεις αφαιρέσει το avatar του δωματίου</string>
<string name="notice_room_server_acl_updated_no_change">Καμία αλλαγή.</string>
<string name="notice_room_name_removed_by_you">Έχεις αφαίρεσει το όνομα του δωματίου</string>
<string name="notice_direct_room_third_party_invite">Ο χρήστης %1$s προσκάλεσε τον χρήστη %2$s</string>
<string name="notice_direct_room_third_party_invite_by_you">Προσκάλεσες τον χρήστη %1$s</string>
<string name="power_level_admin">Διαχειριστής</string>
<string name="power_level_moderator">Moderator</string>
<string name="notice_power_level_changed_by_you">Άλλαξες το power του level %1$s.</string>
<string name="notice_room_server_acl_updated_was_allowed">Οι Servers που ταιριάζουν με %s έχουν αφαιρεθεί από την επιτρεπόμενη λίστα.</string>
<string name="notice_room_server_acl_updated_ip_literals_not_allowed">Οι Servers που ταιριάζουν με το IP έχουν αποκλειστικοί.</string>
<string name="notice_room_topic_removed_by_you">Έχεις αφαιρέσει το θέμα του δωματίου</string>
<string name="notice_room_third_party_registered_invite_by_you">Αποδέχτηκες την πρόσκληση για το %1$s</string>
<string name="power_level_default">Προεπιλογή</string>
<string name="power_level_custom">Custom (%1$d)</string>
<string name="power_level_custom_no_value">Προσαρμοσμένο</string>
<string name="notice_room_server_acl_allow_is_empty">🎉Όλοι οι Servers έχουν αποκλειστεί από συμμετέχοντες!Το δωμάτιο αυτό δεν μπορεί πια να χρησιμοποιηθεί.</string>
<string name="notice_room_third_party_invite_by_you">Έστειλες μια πρόσκληση στο %1$s για να συνδεθείς στο δωμάτιο</string>
<string name="event_status_sent_message">Το μήνυμα στάλθηκε</string>
<string name="create_room">Δημιουργώ ένα δωμάτιο</string>
<string name="room_error_access_unauthorized">Δεν επιτρέπεται να συνδεθείς σε αυτό το δωμάτιο</string>
<string name="room_displayname_3_members">%1$s,%2$s και %3$s</string>
<string name="change_space">Αλλάζω το Space</string>
<string name="room_displayname_4_members">%1$s, %2$s, %3$s και %4$s</string>
<string name="explore_rooms">Ερευνάω τα δωμάτια</string>
<string name="initial_sync_request_reason_unignored_users">-Κάποιοι χρήστες έχουν αγνοηθεί</string>
<plurals name="room_displayname_four_and_more_members">
<item quantity="one">%1$s, %2$s, %3$s και %4$d άλλος χρήστης</item>
<item quantity="other">%1$s, %2$s, %3$s και %4$d άλλοι χρήστες</item>
</plurals>
<string name="event_status_sending_message">Το μήνυμα στέλνεται…</string>
<string name="pill_message_from_unknown_user">Μύνημα</string>
<string name="set_link_create">Δημιουργώ έναν σύνδεσμο</string>
<string name="set_link_link">Σύνδεσμος</string>
<string name="message_reply_to_sender_sent_file">Στέλνω ένα αρχείο.</string>
<string name="pill_message_from_user">Μύνημα από τον χρήστη %s</string>
<string name="message_reply_to_sender_sent_voice_message">Στέλνω ένα ηχητικό μύνημα.</string>
<string name="message_reply_to_sender_sent_image">Στέλνω μια εικόνα.</string>
<plurals name="notice_room_canonical_alias_alternative_added_by_you">
<item quantity="one">Πρόσθεσες εναλλακτική διεύθυνση %1$ για αυτό το δωμάτιο.</item>
<item quantity="other">Πρόσθεσες εναλλακτικές διευθύνσεις %1$ για αυτό το δωμάτιο.</item>
</plurals>
<string name="message_reply_to_sender_sent_video">Στέλνω ένα βίντεο.</string>
<string name="message_reply_to_sender_sent_audio_file">Στέλνω ένα αρχείο ήχου.</string>
<string name="set_link_edit">Επεξεργάζομαι ένα σύνδεσμο</string>
<string name="set_link_text">Κείμενο</string>
<string name="message_reply_to_sender_sent_sticker">Στέλνω ένα αυτοκόλλητο.</string>
<string name="qr_code_login_scan_qr_code_button">Σκανάρω ένα QR code</string>
</resources>

File diff suppressed because it is too large Load diff

View file

@ -3077,4 +3077,12 @@
<string name="crosssigning_verify_after_update">Zaktualizowano aplikację</string>
<string name="sign_out_anyway">Wyloguj mimo to</string>
<string name="sign_out_failed_dialog_message">Nie można skontaktować się z serwerem domowym. Jeśli mimo to się wylogujesz, urządzenie nie zostanie usunięte z listy urządzeń. Usuń je za pomocą innego klienta.</string>
<string name="create_room_unknown_users_dialog_content">Nie można znaleźć profili dla poniższych ID Matrix. Czy chcesz rozpocząć czat mimo to\?
\n
\n%s</string>
<string name="invite_unknown_users_dialog_submit">Zaproś mimo to</string>
<string name="create_room_unknown_users_dialog_submit">Rozpocznij czat mimo to</string>
<string name="invite_unknown_users_dialog_content">Nie można znaleźć profili dla poniższych ID Matrix. Czy chcesz zaprosić je mimo to\?
\n
\n%s</string>
</resources>

View file

@ -290,7 +290,7 @@
<string name="send_bug_report_progress">Прогресс (%s%%)</string>
<string name="send_bug_report_app_crashed">В прошлый раз приложение некорректно завершило работу. Хотите отправить отчёт о сбое\?</string>
<string name="join_room">Войти в Комнату</string>
<string name="username">Имя пользователя</string>
<string name="username">Псевдоним</string>
<string name="logout">Выйти</string>
<string name="hs_url">URL домашнего сервера</string>
<string name="search">Поиск</string>
@ -300,7 +300,7 @@
<string name="option_take_photo_video">Камера</string>
<string name="auth_login">Вход</string>
<string name="auth_submit">Подать</string>
<string name="auth_invalid_login_param">Неправильное имя пользователя и/или пароль</string>
<string name="auth_invalid_login_param">Неверный псевдоним и/или пароль</string>
<string name="auth_invalid_email">Это не похоже на действительный адрес электронной почты</string>
<string name="auth_email_already_defined">Этот адрес электронной почты уже используется.</string>
<string name="auth_forgot_password">Забыли пароль?</string>
@ -797,7 +797,7 @@
<string name="backup">Создание резервной копии</string>
<string name="sign_out_bottom_sheet_will_lose_secure_messages">Сделайте резервную копию ваших ключей или потеряете доступ к вашим зашифрованным сообщениям.</string>
<string name="action_sign_out_confirmation_simple">Уверены, что хотите выйти?</string>
<string name="error_empty_field_enter_user_name">Пожалуйста, введите имя пользователя.</string>
<string name="error_empty_field_enter_user_name">Пожалуйста, введите псевдоним.</string>
<string name="keys_backup_setup_step1_advanced">(Расширенный)</string>
<string name="keys_backup_setup_step1_manual_export">Ручной экспорт ключей</string>
<string name="keys_backup_setup_creating_backup">Создание резервной копии</string>
@ -1132,11 +1132,11 @@
<string name="login_msisdn_error_not_international">Международные телефонные номера должны начинаться с \'+\'</string>
<string name="login_msisdn_error_other">Номер телефона выглядит недействительным. Пожалуйста, проверьте его</string>
<string name="login_signup_to">Зарегистрироваться в %1$s</string>
<string name="login_signin_username_hint">Имя пользователя или электронная почта</string>
<string name="login_signup_username_hint">Имя пользователя</string>
<string name="login_signin_username_hint">Псевдоним или электронная почта</string>
<string name="login_signup_username_hint">Псевдоним</string>
<string name="login_signup_password_hint">Пароль</string>
<string name="login_signup_submit">Далее</string>
<string name="login_signup_error_user_in_use">Это имя пользователя занято</string>
<string name="login_signup_error_user_in_use">Этот псевдоним уже занят</string>
<string name="login_signup_cancel_confirmation_title">Предупреждение</string>
<string name="login_signup_cancel_confirmation_content">Ваш аккаунт еще не создан. Остановить процесс регистрации\?</string>
<string name="login_a11y_choose_matrix_org">Выбрать matrix.org</string>
@ -1144,7 +1144,7 @@
<string name="login_a11y_choose_other">Выбрать другой сервер</string>
<string name="login_a11y_captcha_container">Пожалуйста, пройдите проверку капчей</string>
<string name="login_terms_title">Примите условия для продолжения</string>
<string name="login_wait_for_email_title">Пожалуйста, проверьте ваш электронный почтовый ящик</string>
<string name="login_wait_for_email_title">Пожалуйста, проверьте свою электронную почту</string>
<string name="login_wait_for_email_notice">Мы только что отправили письмо на %1$s.
\nПожалуйста, нажмите на содержащуюся в нём ссылку, чтобы продолжить создание аккаунта.</string>
<string name="login_error_outdated_homeserver_title">Домашний сервер устарел</string>
@ -1191,7 +1191,7 @@
<string name="verification_conclusion_warning">Недоверенный вход</string>
<string name="room_profile_section_more_uploads">Вложения</string>
<string name="cross_signing_verify_by_emoji">Интерактивная проверка со смайлами</string>
<string name="error_empty_field_choose_user_name">Пожалуйста, выберите имя пользователя.</string>
<string name="error_empty_field_choose_user_name">Пожалуйста, выберите псевдоним.</string>
<string name="error_empty_field_choose_password">Пожалуйста, выберите пароль.</string>
<string name="message_action_item_redact">Удалить…</string>
<string name="seen_by">Просмотрено</string>
@ -1549,7 +1549,7 @@
<string name="settings_messages_in_e2e_group_chat">Зашифрованные сообщения в групповых чатах</string>
<string name="settings_when_rooms_are_upgraded">При обновлении комнат</string>
<string name="command_description_plain">Посылает сообщение в виде простого текста, не интерпретируя его как разметку</string>
<string name="auth_invalid_login_param_space_in_password">Неверное имя пользователя и/или пароль. Введенный пароль начинается или заканчивается пробелами, пожалуйста, проверьте.</string>
<string name="auth_invalid_login_param_space_in_password">Неверный псевдоним и/или пароль. Введённый пароль начинается или заканчивается пробелами, пожалуйста, проверьте его.</string>
<string name="auth_invalid_login_deactivated_account">Эта учётная запись была деактивирована.</string>
<string name="bootstrap_enter_recovery">Введите %s, чтобы продолжить</string>
<string name="use_file">Использовать файл</string>
@ -2195,7 +2195,7 @@
<string name="settings_group_messages">Групповых сообщениях</string>
<string name="settings_encrypted_direct_messages">Зашифрованных диалогах</string>
<string name="settings_messages_direct_messages">Диалогах</string>
<string name="settings_messages_containing_username">Мое имя пользователя</string>
<string name="settings_messages_containing_username">Мой псевдоним</string>
<string name="settings_messages_containing_display_name">Моё отображаемое имя</string>
<string name="settings_notification_notify_me_for">Уведомлять меня о</string>
<string name="settings_notification_other">Другое</string>
@ -2537,7 +2537,7 @@
<string name="settings_show_latest_profile">Последняя информация о пользователе</string>
<string name="space_explore_filter_no_result_title">Не найдено</string>
<string name="a11y_presence_busy">Занят</string>
<string name="error_forbidden_digits_only_username">Домашний сервер не принимает имя пользователя, состоящее только из цифр.</string>
<string name="error_forbidden_digits_only_username">Домашний сервер не принимает псевдонимы, состоящие только из цифр.</string>
<string name="ftue_personalize_skip_this_step">Пропустить этот шаг</string>
<string name="ftue_personalize_submit">Сохранить и продолжить</string>
<string name="ftue_personalize_complete_subtitle">Ваши предпочтения были сохранены</string>
@ -2633,7 +2633,7 @@
<string name="ftue_auth_create_account_password_entry_footer">Должно быть 8 или более символов</string>
<string name="crosssigning_cannot_verify_this_session">Не удалось заверить этот сеанс</string>
<string name="permalink_unsupported_groups">Невозможно открыть эту ссылку: сообщества были заменены пространствами</string>
<string name="ftue_auth_login_username_entry">Имя пользователя / Почта / Телефон</string>
<string name="ftue_auth_login_username_entry">Псевдоним / Почта / Телефон</string>
<string name="ftue_auth_password_reset_email_confirmation_subtitle">Следуйте инструкциям, отправленным на %s</string>
<string name="ftue_auth_forgot_password">Забыли пароль</string>
<string name="ftue_auth_email_verification_footer">Не получили письмо\?</string>
@ -3020,7 +3020,7 @@
<string name="error_voice_message_broadcast_in_progress">Не удалось записать голосовое сообщение</string>
<string name="review_unverified_sessions_description">Убедиться что Ваш аккаунт в безопасности</string>
<string name="settings_nightly_build_update">Получить последнюю сборку (у вас могут быть проблемы со входом)</string>
<string name="room_profile_section_more_polls">История опроса</string>
<string name="room_profile_section_more_polls">Опросы</string>
<string name="started_a_voice_broadcast">Голосовая трансляция начата</string>
<string name="thread_list_not_available">Ваш домашний сервер не поддерживает список обсуждений.</string>
<string name="action_stop">Остановить</string>
@ -3032,7 +3032,7 @@
<string name="pill_message_in_room">Сообщение в %s</string>
<string name="pill_message_in_unknown_room">Сообщение в комнате</string>
<string name="pill_message_unknown_room_or_space">Комната/Пространство</string>
<string name="settings_external_account_management_title">Аккаунт</string>
<string name="settings_external_account_management_title">Учётная запись</string>
<string name="avatar_of_user">Аватар профиля пользователя %1$s</string>
<string name="secure_backup_reset_danger_warning">Продолжайте, только если вы уверены, что ваш ключ утерян, а доступ к другим активным устройствам отсустствует.</string>
<string name="verification_profile_other_device_untrust_info">Пока пользователь не верифицировал эту сессию, отправленные и полученные сообщения отмечаются предупреждениями.</string>
@ -3046,13 +3046,18 @@
<string name="settings_notification_error_on_update">При изменении настроек уведомлений произошла ошибка. Попробуйте ещё раз.</string>
<string name="direct_room_encryption_enabled_waiting_users_tile_description">Когда приглашенные пользователи присоединятся к ${app_name}, вы сможете писать им с использованием сквозного шифрования</string>
<string name="secure_backup_reset_all_no_other_devices_long">Сброс ваших ключей верификации не может быть отменен. После сброса, вы не будете иметь доступа к старым зашифрованным сообщениям, а все ваши контакты, верифицировавшие вас ранее, увидят предупреждение о повторной верификации.</string>
<string name="encrypted_by_deleted">Зашифрованно неактивным устройством</string>
<string name="confirm_your_identity_after_update">Защищенный обмен сообщениями был обновлен. Пожалуйста, повторно верифицируйте ваше устройство.</string>
<string name="encrypted_by_deleted">Зашифровано прерванным сеансом</string>
<string name="confirm_your_identity_after_update">В последнем обновлении улучшили защищённую переписку. Пожалуйста, перезаверьте свой сеанс.</string>
<string name="error_voice_broadcast_unable_to_decrypt">Не удается расшифровать голосовое сообщение.</string>
<string name="room_poll_details_go_to_timeline">Обзор опроса во времени</string>
<string name="pill_message_from_unknown_user">Сообщение</string>
<string name="verification_verify_with_another_device">Подтвердить с помощью активного устройства</string>
<string name="verification_verify_with_another_device">Сверить с другим сеансом</string>
<string name="_resume">Возобновить</string>
<string name="notice_display_name_changed_to">%1$s изменил отображаемое имя на %2$s</string>
<string name="notice_display_name_changed_to">%1$s изменил(а) имя на %2$s</string>
<string name="verification_not_found">Запрос на верификацию не найден. Возможно, он был отменен или обработан другим сеансом.</string>
<string name="rich_text_editor_quote">Цитата</string>
<string name="settings_crypto_version">Версия шифрования</string>
<string name="rich_text_editor_code_block">Блок кода</string>
<string name="rich_text_editor_indent">Подпункт</string>
<string name="rich_text_editor_unindent">Пункт</string>
</resources>

View file

@ -3018,4 +3018,12 @@
<string name="crosssigning_verify_after_update">Aplikácia bola aktualizovaná</string>
<string name="sign_out_anyway">Aj tak sa odhlásiť</string>
<string name="sign_out_failed_dialog_message">Nie je možné sa spojiť s domovským serverom. Ak sa aj tak odhlásite, toto zariadenie nebude vymazané zo zoznamu zariadení, môžete ho odstrániť pomocou iného klienta.</string>
<string name="create_room_unknown_users_dialog_content">Nie je možné nájsť profily pre nižšie uvedené Matrix ID. Chceli by ste napriek tomu začať konverzáciu\?
\n
\n%s</string>
<string name="create_room_unknown_users_dialog_submit">Spustiť konverzáciu aj tak</string>
<string name="invite_unknown_users_dialog_content">Nie je možné nájsť profily pre nižšie uvedené Matrix ID. Chcete ich aj tak pozvať\?
\n
\n%s</string>
<string name="invite_unknown_users_dialog_submit">Napriek tomu pozvať</string>
</resources>

View file

@ -6,7 +6,7 @@
<string name="action_send">Pošlji</string>
<string name="notice_room_join_by_you">Pridružil si se v sobi</string>
<string name="notice_room_invite_you">%1$s te je povabil</string>
<string name="notice_answered_call_by_you">Odgovoril si na klic.</string>
<string name="notice_answered_call_by_you">Odgovorili ste na klic.</string>
<string name="notice_answered_call">%s je odgovoril na klic.</string>
<string name="notice_room_name_changed_by_you">Spremenil si ime sobe v: %1$s</string>
<string name="notice_room_name_changed">%1$s je spremenil ime sobe v: %2$s</string>
@ -40,11 +40,11 @@
<string name="notice_room_avatar_changed_by_you">Spremenil si avatarja sobe</string>
<string name="notice_placed_video_call_by_you">Začel si video klic.</string>
<string name="notice_placed_voice_call">%s je začel video klic.</string>
<string name="notice_placed_voice_call_by_you">Začel si video klic.</string>
<string name="notice_placed_voice_call_by_you">Začeli ste video klic.</string>
<string name="notice_ended_call">%s je končal klic.</string>
<string name="notice_ended_call_by_you">Končal si klic.</string>
<string name="notice_ended_call_by_you">Končali ste klic.</string>
<string name="notice_made_future_room_visibility">%1$s je %2$s omogočil ogled prihodnje zgodovine sobe</string>
<string name="notice_made_future_room_visibility_by_you">Omogočil si ogled prihodnje zgodovine sobe %1$s</string>
<string name="notice_made_future_room_visibility_by_you">Omogočili ste ogled prihodnje zgodovine sobe %1$s</string>
<plurals name="x_selected">
<item quantity="one">%1$d izbran</item>
<item quantity="two">%1$d izbrana</item>
@ -62,7 +62,17 @@
<string name="notice_display_name_removed_by_you">Odstranil si svoje prikazno ime (bilo je %1$s)</string>
<string name="notice_room_topic_changed_by_you">Temo si spremenil v: %1$s</string>
<string name="notice_placed_video_call">%s je začel video klic.</string>
<string name="notice_made_future_direct_room_visibility">%1$s je omočil %2$s ogled prihodnjih sporočil</string>
<string name="notice_made_future_direct_room_visibility_by_you">%1$s si omogočil ogled prihodnjih sporočil</string>
<string name="notice_made_future_direct_room_visibility">%1$s je omogočil %2$s ogled prihodnjih sporočil</string>
<string name="notice_made_future_direct_room_visibility_by_you">%1$s ste omogočili ogled prihodnjih sporočil</string>
<string name="notice_room_visibility_invited">vsi člani sobe, od trenutka povabila.</string>
<string name="notice_room_invite_no_invitee">Povabilo uporabnika %s</string>
<string name="notice_call_candidates">%s je poslal(a) podatke za začetek klica.</string>
<string name="notice_call_candidates_by_you">Poslali ste podatke za začetek klica.</string>
<string name="notice_direct_room_update_by_you">Tu ste izvedli nadgradnjo.</string>
<string name="notice_room_visibility_joined">vsi člani te sobe od trenutka ko so se pridružili.</string>
<string name="notice_room_visibility_shared">vsi člani sobe.</string>
<string name="notice_room_visibility_world_readable">kdorkoli.</string>
<string name="notice_room_update">%s je nadgradil to sobo.</string>
<string name="notice_room_update_by_you">Vi ste nadgradili to sobo.</string>
<string name="notice_direct_room_update">%s je tu izvedel(a) nadgradnjo.</string>
</resources>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="notice_room_remove_by_you">umeondoa %1$s</string>
<string name="notice_room_remove">%1$s kuondolewa %2$s</string>
<string name="notice_room_reject_by_you">Ulikataa mwaliko</string>
@ -11,4 +11,34 @@
<item quantity="one">%1$d Iliyochaguliwa</item>
<item quantity="other">%1$d Ziliyochaguliwa</item>
</plurals>
</resources>
<string name="notice_room_created">%1$s Andaa chumba</string>
<string name="notice_room_created_by_you">Umeandaa chumba</string>
<string name="notice_direct_room_created">%1$s anzisha mjadala</string>
<string name="notice_direct_room_created_by_you">Ulianzisha mjadala</string>
<string name="notice_room_invite">%1$s Walioalikwa %2$s</string>
<string name="notice_room_join">%1$s jiunge na mjadala</string>
<string name="notice_room_leave">%1$s Acha mjadala</string>
<string name="notice_room_leave_by_you">Umetoka kwenye mjadala</string>
<string name="notice_direct_room_leave">%1$s Acha mjadala</string>
<string name="notice_direct_room_leave_by_you">Umeacha mjadala</string>
<string name="notice_room_unban">%1$s hujazuiliwa %2$s</string>
<string name="notice_room_unban_by_you">Hujazuiliwa %1$s</string>
<string name="notice_room_ban">%1$s huruhusiwi %2$s</string>
<string name="notice_room_ban_by_you">Huruhusiwi %1$s</string>
<string name="notice_room_withdraw_by_you">Sitisha %1$s\'s mwaliko</string>
<string name="notice_room_invite_no_invitee">%s\'s Mwaliko</string>
<string name="notice_room_join_by_you">Umejiunga na mjadala</string>
<string name="notice_room_withdraw">%1$s sitisha %2$s\'s mmwaliko</string>
<string name="notice_room_invite_no_invitee_by_you">Mwaliko wako</string>
<string name="notice_room_invite_by_you">Umealika %1$s</string>
<string name="notice_avatar_url_changed">%1$s wamebadilisha avatar</string>
<string name="notice_avatar_url_changed_by_you">Umebadilisha avatar yako</string>
<string name="notice_display_name_set_by_you">Unaweka jina lako la kuonyesha %1$s</string>
<string name="notice_display_name_changed_from_by_you">ulibadilisha jina lako la kuonyesha kutoka %1$s kwenda %2$s</string>
<string name="notice_display_name_removed_by_you">Umeondoa jina lako la kuonyesha(lilikuwa %1$s)</string>
<string name="notice_room_topic_changed">%1$s alibadilisha mada kuwa: %2$s</string>
<string name="notice_display_name_set">%1$s weka jina lao la kuonyesha %2$s</string>
<string tools:ignore="UnusedResources" name="notice_display_name_changed_from">%1$s badilisha majina yao kuonyesha yalitoka %2$s kwenda %3$s</string>
<string name="notice_display_name_changed_to">%1$s badilisha majina yao kwenda %2$s</string>
<string name="notice_display_name_removed">%1$s wameondoa ma jina yao yaliyonyeshwa (yalikuwa %2$s)</string>
</resources>

View file

@ -84,7 +84,7 @@
<string name="are_you_sure">Bạn có chắc không\?</string>
<string name="sign_out_bottom_sheet_backing_up_keys">Đang sao lưu khoá…</string>
<string name="sign_out_bottom_sheet_dont_want_secure_messages">Tôi không muốn các tin nhắn của tôi được mã hoá</string>
<string name="sign_out_bottom_sheet_warning_backup_not_active">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á.</string>
<string name="sign_out_bottom_sheet_warning_backup_not_active">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á.</string>
<string name="sign_out_bottom_sheet_warning_backing_up">Đ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á.</string>
<string name="sign_out_bottom_sheet_warning_no_backup">Bạn sẽ mất các tin nhắn được mã hoá nếu bạn đăng xuất ngay bây giờ</string>
<string name="title_activity_keys_backup_restore">Sử dụng Sao lưu Khóa</string>
@ -99,7 +99,7 @@
<string name="dark_theme">Chủ đề tối</string>
<string name="light_theme">Chủ đề sáng</string>
<string name="auth_forgot_password">Quên mật khẩu\?</string>
<string name="auth_invalid_email">Đấy không giống một địa chỉ email hợp lệ</string>
<string name="auth_invalid_email">Đấy không giống một địa chỉ thư điện tử hợp lệ</string>
<string name="auth_invalid_login_param">Tên người dùng hoặc mật khẩu không đúng</string>
<string name="auth_login">Đăng nhập</string>
<string name="error_no_external_application_found">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.</string>
@ -196,7 +196,7 @@
<string name="event_status_a11y_sent">Đã gửi</string>
<string name="event_status_a11y_sending">Đang gửi</string>
<string name="medium_phone_number">Số điện thoại</string>
<string name="medium_email">Địa chỉ email</string>
<string name="medium_email">Địa chỉ thư điện tử</string>
<string name="matrix_error">Lỗi Matrix</string>
<string name="notice_power_level_changed">%1$s đã thay đổi cấp độ quyền lực của %2$s.</string>
<string name="notice_power_level_changed_by_you">Bạn đã thay đổi cấp độ quyền lực của %1$s.</string>
@ -234,12 +234,12 @@
<string name="notice_avatar_changed_too">(ảnh đại diện cũng được thay đổi)</string>
<string name="notice_room_server_acl_allow_is_empty">🎉 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.</string>
<string name="notice_room_server_acl_updated_no_change">Không có thay đổi.</string>
<string name="notice_room_server_acl_updated_title_by_you">Bạn đã thay đổi ACL máy chủ cho phòng này.</string>
<string name="notice_room_server_acl_updated_title">%s đã thay đổi ACL máy chủ cho phòng này.</string>
<string name="notice_room_server_acl_updated_title_by_you">Bạn đã thay đổi danh sách máy chủ truy cập máy chủ cho phòng này.</string>
<string name="notice_room_server_acl_updated_title">%s đã thay đổi danh sách máy chủ truy cập máy chủ cho phòng này.</string>
<string name="notice_room_server_acl_set_allowed">• Những máy chủ khớp với %s được cho phép.</string>
<string name="notice_room_server_acl_set_banned">• Những máy chủ khớp với %s bị cấm.</string>
<string name="notice_room_server_acl_set_title_by_you">Bạn đã đặt ACL máy chủ cho phòng này.</string>
<string name="notice_room_server_acl_set_title">%s đã đặt ACL máy chủ cho phòng này.</string>
<string name="notice_room_server_acl_set_title_by_you">Bạn đã đặt danh sách máy chủ truy cập cho phòng này.</string>
<string name="notice_room_server_acl_set_title">%s đã đặt danh sách máy chủ truy cập cho phòng này.</string>
<string name="notice_direct_room_update_by_you">Bạn đã nâng cấp ở đây.</string>
<string name="notice_direct_room_update">%s đã nâng cấp ở đây.</string>
<string name="notice_room_update_by_you">Bạn đã nâng cấp phòng này.</string>
@ -256,16 +256,16 @@
<string name="notice_room_ban">%1$s đã cấm %2$s</string>
<string name="notice_room_unban_by_you">Bạn đã bỏ cấm %1$s</string>
<string name="notice_room_unban">%1$s đã bỏ cấm %2$s</string>
<string name="notice_display_name_removed_by_you">Bạn đã xoá tên hiển thị (nó đã là %1$s)</string>
<string name="notice_display_name_removed">%1$s đã xoá tên hiển thị (nó đã là %2$s)</string>
<string name="notice_display_name_removed_by_you">Bạn đã xoá tên hiển thị (từng là %1$s)</string>
<string name="notice_display_name_removed">%1$s đã xoá tên hiển thị (từng là %2$s)</string>
<string name="notice_display_name_changed_from_by_you">Bạn đã đổi tên hiển thị từ %1$s thành %2$s</string>
<string name="notice_display_name_changed_from">%1$s đã đổi tên hiển thị từ %2$s thành %3$s</string>
<string name="notice_display_name_set_by_you">Bạn đã đặt tên hiển thị thành %1$s</string>
<string name="notice_display_name_set">%1$s đã đặt tên hiển thị thành %2$s</string>
<string name="notice_avatar_url_changed_by_you">Bạn đã thay đổi ảnh đại diện</string>
<string name="notice_avatar_url_changed">%1$s đã thay đổi ảnh đại diện</string>
<string name="notice_room_withdraw_by_you">Bạn đã rút lại lời mời của %1$s</string>
<string name="notice_room_withdraw">%1$s đã rút lại lời mời của %2$s</string>
<string name="notice_room_withdraw_by_you">Bạn đã hủy lời mời của %1$s</string>
<string name="notice_room_withdraw">%1$s đã hủy lời mời của %2$s</string>
<string name="room_widget_permission_theme">Chủ đề của bạn</string>
<string name="settings_theme">Chủ đề</string>
<string name="notice_ended_call_by_you">Bạn đã kết thúc cuộc gọi.</string>
@ -286,8 +286,8 @@
<string name="notice_room_topic_changed_by_you">Bạn đã thay đổi chủ đề thành: %1$s</string>
<string name="event_status_delete_all_failed_dialog_title">Xoá các tin nhắn chưa được gửi</string>
<string name="event_status_delete_all_failed_dialog_message">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\?</string>
<string name="notice_room_remove_by_you">Bạn đã kick %1$s</string>
<string name="notice_room_remove">%1$s đã kick %2$s</string>
<string name="notice_room_remove_by_you">Bạn đã loại bỏ %1$s</string>
<string name="notice_room_remove">%1$s đã loại bỏ %2$s</string>
<string name="notice_room_reject_by_you">Bạn đã từ chối lời mời</string>
<string name="notice_room_reject">%1$s đã từ chối lời mời</string>
<string name="notice_direct_room_leave_by_you">Bạn đã rời phòng</string>
@ -329,7 +329,7 @@
<string name="notice_room_leave_with_reason">%1$s đã rời phòng. Lý do: %2$s</string>
<string name="auth_recaptcha_message">Máy chủ nhà này muốn chắc chắn bạn không phải rô bốt</string>
<string name="auth_msisdn_already_defined">Số điện thoại này đã được định nghĩa rồi.</string>
<string name="auth_email_already_defined">Địa chỉ email này đã được định nghĩa rồi.</string>
<string name="auth_email_already_defined">Địa chỉ thư điện tử này đã được sử dụng rồi.</string>
<string name="auth_login_sso">Đăng nhập bằng đăng nhập một lần</string>
<string name="use_as_default_and_do_not_ask_again">Sử dụng làm mặc định và không hỏi lại</string>
<string name="option_always_ask">Luôn hỏi</string>
@ -341,8 +341,8 @@
<string name="failed_to_remove_widget">Xoá tiện ích thất bại</string>
<string name="failed_to_add_widget">Thêm tiện ích thất bại</string>
<string name="action_switch">Chuyển</string>
<string name="encryption_information_verified">Đã xác minh</string>
<string name="encryption_information_not_verified">Chưa xác minh</string>
<string name="encryption_information_verified">Đã xác thực</string>
<string name="encryption_information_not_verified">Chưa xác thực</string>
<plurals name="encryption_import_room_keys_success">
<item quantity="other">Nhập %1$d/%2$d mã khoá thành công.</item>
</plurals>
@ -359,9 +359,9 @@
<string name="encryption_import_e2e_room_keys">Nhập các mã khoá phòng E2E</string>
<string name="encryption_settings_manage_message_recovery_summary">Quản lý bản sao lưu mã khoá</string>
<string name="encryption_information_unknown_ip">ip không xác định</string>
<string name="encryption_information_verify_device_warning2">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.</string>
<string name="encryption_information_verify_device_warning">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:</string>
<string name="encryption_information_verify">Xác minh</string>
<string name="encryption_information_verify_device_warning2">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.</string>
<string name="encryption_information_verify_device_warning">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:</string>
<string name="encryption_information_verify">Xác thực</string>
<string name="hs_url">URL máy chủ nhà</string>
<string name="room_participants_leave_prompt_msg">Bạn có chắc bạn muốn rời khỏi phòng không\?</string>
<string name="room_participants_leave_prompt_title">Rời khỏi phòng</string>
@ -402,14 +402,14 @@
<item quantity="other">%d thay đổi thành viên</item>
</plurals>
<string name="e2e_re_request_encryption_key_dialog_content">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.</string>
<string name="e2e_re_request_encryption_key">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.</string>
<string name="e2e_re_request_encryption_key">Yêu cầu lại các mã khoá mã hoá từ các phiên khác của bạn.</string>
<string name="login_error_limit_exceeded">Quá nhiều yêu cầu đã được gửi</string>
<string name="login_error_not_json">Đã không chứa JSON hợp lệ</string>
<string name="error_unauthorized">Không được uỷ quyền, thiếu thông tin xác thực hợp lệ</string>
<string name="login_error_homeserver_not_found">Không thể kết nối đến máy chủ nhà tại URL này, vui lòng kiểm tra nó</string>
<string name="login_error_invalid_home_server">Vui lòng nhập URL hợp lệ</string>
<string name="auth_accept_policies">Vui lòng xem xét và chấp nhận chính sách của máy chủ nhà này:</string>
<string name="auth_reset_password_error_unauthorized">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</string>
<string name="auth_reset_password_error_unauthorized">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</string>
<string name="settings_room_directory_show_all_rooms_summary">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.</string>
<string name="settings_room_directory_show_all_rooms">Hiện các phòng có nội dung phản cảm</string>
<string name="settings_category_room_directory">Danh sách phòng</string>
@ -517,8 +517,8 @@
<string name="room_displayname_room_invite">Lời mời vào phòng</string>
<string name="notice_room_server_acl_updated_ip_literals_not_allowed">• Những máy chủ khớp với IP bây giờ sẽ bị cấm.</string>
<string name="notice_room_server_acl_updated_ip_literals_allowed">• Những máy chủ khớp với IP bây giờ sẽ được cho phép.</string>
<string name="notice_room_server_acl_set_ip_literals_not_allowed">• Những máy chủ khớp với IP bị cấm.</string>
<string name="notice_room_server_acl_set_ip_literals_allowed">• Những máy chủ khớp với IP được cho phép.</string>
<string name="notice_room_server_acl_set_ip_literals_not_allowed">• Những máy chủ chỉ có địa chỉ Internet (IP) bị cấm.</string>
<string name="notice_room_server_acl_set_ip_literals_allowed">• Những máy chủ chỉ có địa chỉ Internet (IP) được cho phép.</string>
<string name="room_participants_action_unignore_title">Hủy bỏ qua người dùng</string>
<string name="room_participants_action_ignore">Bỏ qua</string>
<string name="room_participants_action_ignore_prompt_msg">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 @@
<string name="settings_troubleshoot_test_bg_restricted_quickfix">Hủy các giới hạn</string>
<string name="deactivate_account_title">Hủy tài khoản</string>
<string name="dialog_user_consent_submit">Xem lại ngay</string>
<string name="encryption_information_device_key">Chìa khóa phiên</string>
<string name="encryption_information_device_key">Khóa phiên</string>
<string name="device_manager_session_details_session_id">Mã phiên</string>
<string name="encryption_information_device_name">Tên công khai</string>
<string name="encryption_information_decryption_error">Lỗi giải mã</string>
@ -790,7 +790,7 @@
<string name="settings_media">Media</string>
<string name="settings_select_country">Chọn quốc gia</string>
<string name="settings_emails_and_phone_numbers_summary">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</string>
<string name="settings_emails_and_phone_numbers_title">Email và số điện thoại</string>
<string name="settings_emails_and_phone_numbers_title">Địa chỉ thư điện tử và số điện thoại</string>
<string name="settings_unignore_user">Hiện tất cả tin nhắn từ %s\?</string>
<string name="settings_password_updated">Mật khẩu của bạn vừa được cập nhật</string>
<string name="settings_fail_to_update_password_invalid_current_password">Mật khẩu này không hợp lệ</string>
@ -800,7 +800,7 @@
<string name="settings_change_password">Đổi mật khẩu</string>
<string name="settings_password">Mật khẩu</string>
<string name="account_phone_number_already_used_error">Số điện thoại này đã được sử dụng.</string>
<string name="account_email_already_used_error">Địa chỉ Email này đã được sử dụng.</string>
<string name="account_email_already_used_error">Địa chỉ thư điện tử này đã được sử dụng.</string>
<string name="settings_select_language">Chọn ngôn ngữ</string>
<string name="settings_interface_language">Ngôn ngữ</string>
<string name="settings_user_interface">Giao diện người dùng</string>
@ -878,9 +878,9 @@
</plurals>
<string name="settings_set_sync_delay">Thời gian chờ giữa 2 lần đồng bộ</string>
<string name="login_validation_code_is_not_correct">Mã nhập vào không hợp lệ. Vui lòng kiểm tra.</string>
<string name="login_wait_for_email_notice">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.</string>
<string name="login_wait_for_email_title">Hãy kiểm tra email của bạn</string>
<string name="login_wait_for_email_notice">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.</string>
<string name="login_wait_for_email_title">Hãy kiểm tra hòm thư của bạn</string>
<string name="login_terms_title">Chấp nhận điều khoản để tiếp tục</string>
<string name="login_a11y_captcha_container">Hãy thực hiện thách thức captcha</string>
<string name="login_a11y_choose_other">Chọn một máy chủ khác</string>
@ -892,7 +892,7 @@
<string name="login_signup_submit">Tiếp</string>
<string name="login_signup_password_hint">Mật khẩu</string>
<string name="login_signup_username_hint">Tên đăng nhập</string>
<string name="login_signin_username_hint">Username hoặc email</string>
<string name="login_signin_username_hint">Tên người dùng hoặc thư điện tử</string>
<string name="login_signup_to">Đăng ký với %1$s</string>
<string name="login_msisdn_error_other">Số điện thoại có vẻ không hợp lệ. Hãy kiểm tra lại</string>
<string name="login_msisdn_error_not_international">Số điện thoại quốc tế phải bắt đầu với dấu \'+\'</string>
@ -908,11 +908,11 @@
<string name="login_set_msisdn_notice2">Vui lòng sử dụng mẫu quốc tế.</string>
<string name="login_set_msisdn_notice">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.</string>
<string name="login_set_msisdn_title">Thêm số điện thoại</string>
<string name="login_set_email_submit">Tiếp</string>
<string name="login_set_email_optional_hint">Email (tùy chọn)</string>
<string name="login_set_email_mandatory_hint">Email</string>
<string name="login_set_email_submit">Tiếp tục</string>
<string name="login_set_email_optional_hint">Địa chỉ thư điện tử (tùy chọn)</string>
<string name="login_set_email_mandatory_hint">Địa chỉ thư điện tử</string>
<string name="login_set_email_notice">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.</string>
<string name="login_set_email_title">Thêm địa chỉ email</string>
<string name="login_set_email_title">Thêm địa chỉ thư điện tử</string>
<string name="login_reset_password_cancel_confirmation_content">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\?</string>
@ -921,18 +921,18 @@
<string name="login_reset_password_success_notice_2">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ị.</string>
<string name="login_reset_password_success_notice">Mật khẩu của bạn đã được đặt lại.</string>
<string name="login_reset_password_success_title">Thành công!</string>
<string name="login_reset_password_mail_confirmation_submit">Tôi đã xác minh địa chỉ email</string>
<string name="login_reset_password_mail_confirmation_submit">Tôi đã xác nhận địa chỉ email</string>
<string name="login_reset_password_mail_confirmation_notice_2">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.</string>
<string name="login_reset_password_mail_confirmation_notice">Email xác thực đã được gửi tới %1$s.</string>
<string name="login_reset_password_mail_confirmation_notice">Thư xác nhận đã được gửi tới %1$s.</string>
<string name="login_reset_password_mail_confirmation_title">Kiểm tra mailbox</string>
<string name="login_reset_password_error_not_found">Địa chỉ thư điện tử này không được liên kết với tài khoản nào</string>
<string name="login_reset_password_warning_submit">Tiếp tục</string>
<string name="login_reset_password_warning_content">Đổ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.</string>
<string name="login_reset_password_warning_title">Cảnh báo!</string>
<string name="login_reset_password_password_hint">Mật khẩu mới</string>
<string name="login_reset_password_email_hint">Email</string>
<string name="login_reset_password_email_hint">Địa chỉ thư điện tử</string>
<string name="login_reset_password_submit">Tiếp</string>
<string name="login_reset_password_notice">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.</string>
<string name="login_reset_password_notice">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.</string>
<string name="login_reset_password_on">Đặt lại mật khẩu ở %1$s</string>
<string name="login_login_with_email_error">Địa chỉ thư điện tử này không được liên kết với tài khoản nào.</string>
<string name="login_server_url_form_other_hint">Địa chỉ</string>
@ -954,7 +954,7 @@
<string name="login_server_other_text">Thiết lập tùy chỉnh và nâng cao</string>
<string name="login_server_other_title">Khác</string>
<string name="login_server_modular_learn_more">Xem thêm</string>
<string name="login_server_text">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</string>
<string name="login_server_text">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</string>
<string name="settings_troubleshoot_test_bing_settings_title">Thiết lập tùy chỉnh.</string>
<string name="settings_troubleshoot_test_device_settings_quickfix">Khả dụng</string>
<string name="settings_troubleshoot_test_device_settings_failed">Thông báo không được bật cho phiên này.
@ -982,16 +982,16 @@
<string name="settings_notification_default">Thông báo mặc định</string>
<string name="settings_notification_by_event">Mức quan trọng của thông báo theo sự kiện</string>
<string name="settings_notification_advanced">Thiết lập Thông báo nâng cao</string>
<string name="error_threepid_auth_failed">Đảm bảo rằng bạn nhấp vào đường link trong email được gửi tới bạn.</string>
<string name="error_threepid_auth_failed">Đả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.</string>
<string name="settings_remove_three_pid_confirmation_content">Loại bỏ %s\?</string>
<string name="settings_phone_numbers">Số điện thoại</string>
<string name="settings_emails_empty">Không có địa chỉ thư điện tử nào trong tài khoản của bạn</string>
<string name="settings_emails">Địa chỉ email</string>
<string name="settings_emails">Địa chỉ thư điện tử</string>
<string name="settings_app_info_link_summary">Hiển thị thông tin ứng dụng trong thiết lập hệ thống.</string>
<string name="settings_app_info_link_title">Thông tin ứng dụng</string>
<string name="settings_add_phone_number">Thêm số điện thoại</string>
<string name="settings_phone_number_empty">Chưa có số điện thoại trong tài khoản của bạn</string>
<string name="settings_add_email_address">Thêm địa chỉ email</string>
<string name="settings_add_email_address">Thêm địa chỉ thư điện tử</string>
<string name="settings_display_name">Tên hiển thị</string>
<string name="settings_profile_picture">Hình đại diện</string>
<string name="room_settings_add_homescreen_shortcut">Thêm vào màn hình Home</string>
@ -1027,7 +1027,7 @@
<string name="room_permissions_notice">Chọn vai trò được yêu cầu để thay đổi thiết lập của phòng</string>
<string name="room_permissions_title">Quyền hạn</string>
<string name="ssl_cert_not_trust">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.</string>
<string name="ssl_could_not_verify">Không thể xác minh danh tính của máy chủ trên mạng.</string>
<string name="ssl_could_not_verify">Không thể xác thực danh tính của máy chủ trên mạng.</string>
<string name="ssl_fingerprint_hash">Mã kiểm tra (%s):</string>
<string name="ssl_remain_offline">Bỏ qua</string>
<string name="ssl_logout_account">Đăng xuất</string>
@ -1060,7 +1060,7 @@
<string name="room_settings_space_access_public_description">Ai có thể tìm và tham gia không gian</string>
<string name="room_settings_space_access_title">Truy cập không gian</string>
<string name="room_settings_access_rules_pref_dialog_title">Ai có quyền truy cập\?</string>
<string name="account_email_validation_message">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.</string>
<string name="account_email_validation_message">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.</string>
<string name="settings_integrations_summary">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.</string>
<string name="settings_background_fdroid_sync_mode_real_time_description">${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 @@
<string name="settings_troubleshoot_diagnostic_running_status">Đang chạy… (%1$d of %2$d)</string>
<string name="settings_troubleshoot_diagnostic_run_button_title">Chạy thử</string>
<string name="settings_troubleshoot_diagnostic">Chuẩn đoán khắc phục sự cố</string>
<string name="settings_notification_emails_enable_for_email">Bật thông báo qua email cho %s</string>
<string name="settings_notification_emails_enable_for_email">Bật thông báo qua thư điện tử cho %s</string>
<string name="settings_notification_emails_no_emails">Để đượ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</string>
<string name="settings_notification_emails_category">Thông báo qua email</string>
<string name="settings_notification_emails_category">Thông báo qua thư điện tử</string>
<string name="room_permissions_upgrade_the_space">Nâng cấp không gian</string>
<string name="room_permissions_change_space_name">Thay đổi tên không gian</string>
<string name="room_permissions_enable_space_encryption">Bật mã hóa không gian</string>
@ -1148,10 +1148,10 @@
<item quantity="other">Các lời mời đã gửi tới %1$s và %2$d người nữa</item>
</plurals>
<plurals name="secure_backup_reset_devices_you_can_verify">
<item quantity="other">Hiển thị %d thiết bị bạn có thể xác minh ngay bây giờ</item>
<item quantity="other">Hiển thị %d thiết bị bạn có thể xác thực ngay bây giờ</item>
</plurals>
<plurals name="settings_active_sessions_count">
<item quantity="other">%d phiên đang hoạt động</item>
<item quantity="other">%d phiên hoạt động</item>
</plurals>
<plurals name="room_profile_section_more_member_list">
<item quantity="other">%1$d người</item>
@ -1271,15 +1271,15 @@
<string name="space_explore_activity_title">Khám phá phòng</string>
<string name="discovery_section">Khám phá (%s)</string>
<string name="finish_setup">Hoàn tất cài đặt</string>
<string name="discovery_invite">Mời qua email, tìm liên hệ và hơn thế nữa…</string>
<string name="discovery_invite">Mời qua thư điện tử, tìm liên hệ và hơn thế nữa…</string>
<string name="finish_setting_up_discovery">Hoàn tất việc cài đặt khám phá.</string>
<string name="create_space_identity_server_info_none">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.</string>
<string name="create_space_identity_server_info_none">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.</string>
<string name="join_space">Tham gia Space</string>
<string name="create_space">Tạo space</string>
<string name="skip_for_now">Bỏ qua ngay bây giờ</string>
<string name="share_space_link_message">Gia nhập Space của tôi %1$s %2$s</string>
<string name="invite_by_username_or_mail">Mời theo tên người dùng hoặc thư</string>
<string name="invite_by_email">Mời qua email</string>
<string name="invite_by_email">Mời qua thư điện tử</string>
<string name="invite_people_to_your_space_desc">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.</string>
<string name="invite_to_space">Mời đến %s</string>
<string name="invite_people_menu">Mời mọi người</string>
@ -1424,19 +1424,19 @@
<string name="power_level_title">Quyền</string>
<string name="power_level_edit_title">Đặt quyền</string>
<string name="identity_server_set_alternative_submit">Xác nhận</string>
<string name="identity_server_set_alternative_notice_no_default">Nhập URL của máy chủ xác thực</string>
<string name="identity_server_set_alternative_notice">Ngoài ra, bạn có thể nhập bất kỳ URL máy chủ xác thực nào khác</string>
<string name="identity_server_set_alternative_notice_no_default">Nhập URL của máy chủ định danh</string>
<string name="identity_server_set_alternative_notice">Ngoài ra, bạn có thể nhập bất kỳ URL máy chủ định danh nào khác</string>
<string name="identity_server_set_default_submit">Dùng %1$s</string>
<string name="identity_server_set_default_notice">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</string>
<string name="identity_server_set_default_notice">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</string>
<string name="identity_server_user_consent_not_provided">Sự đồng ý của người dùng chưa được cung cấp.</string>
<string name="identity_server_error_no_current_binding_error">Không có mối liên hệ hiện tại với định danh này.</string>
<string name="identity_server_error_binding_error">Sự kết hợp đã thất bại.</string>
<string name="identity_server_error_no_current_binding_error">Không có mối liên hệ hiện tại với định danh này.</string>
<string name="identity_server_error_binding_error">Không thể liên kết.</string>
<string name="identity_server_error_bulk_sha256_not_supported">Để đả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.</string>
<string name="identity_server_error_terms_not_signed">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.</string>
<string name="identity_server_error_no_identity_server_configured">Trước tiên, vui lòng cấu hình máy chủ nhận dạng.</string>
<string name="identity_server_error_outdated_home_server">Hoạt động này là không thể. Homeerver đã lỗi thời.</string>
<string name="identity_server_error_outdated_identity_server">Máy chủ nhận dạng này đã lỗi thời. ${app_name} chỉ hỗ trợ API V2.</string>
<string name="disconnect_identity_server_dialog_content">Ngắt kết nối khỏi máy chủ nhận dạng %s\?</string>
<string name="identity_server_error_terms_not_signed">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.</string>
<string name="identity_server_error_no_identity_server_configured">Trước tiên, vui lòng cấu hình máy chủ định danh.</string>
<string name="identity_server_error_outdated_home_server">Hoạt động này là không thể. Máy chủ nhà đã lỗi thời.</string>
<string name="identity_server_error_outdated_identity_server">Máy chủ định danh này đã lỗi thời. ${app_name} chỉ hỗ trợ API V2.</string>
<string name="disconnect_identity_server_dialog_content">Ngắt kết nối khỏi máy chủ định danh %s\?</string>
<string name="open_terms_of">Mở các điều khoản của %s</string>
<string name="choose_locale_loading_locales">Tải các ngôn ngữ có sẵn…</string>
<string name="choose_locale_other_locales_title">Các ngôn ngữ có sẵn khác</string>
@ -1466,12 +1466,12 @@
<string name="error_empty_field_choose_user_name">Vui lòng chọn tên người dùng.</string>
<string name="failed_to_initialize_cross_signing">Thất bại trong việc thiết lập Xác thực chéo</string>
<string name="confirm_your_identity_quad_s">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.</string>
<string name="confirm_your_identity">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.</string>
<string name="cross_signing_verify_by_emoji">Xác minh tương tác bằng Emoji</string>
<string name="crosssigning_verify_session">Xác minh đăng nhập</string>
<string name="cross_signing_verify_by_text">Xác minh thủ công bằng Văn bản</string>
<string name="verify_this_session">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</string>
<string name="encrypted_unverified">Được mã hóa bởi một thiết bị chưa được xác minh</string>
<string name="confirm_your_identity">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.</string>
<string name="cross_signing_verify_by_emoji">Xác thực tương tác bằng Emoji</string>
<string name="crosssigning_verify_session">Xác thực đăng nhập</string>
<string name="cross_signing_verify_by_text">Xác thực thủ công bằng Văn bản</string>
<string name="verify_this_session">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</string>
<string name="encrypted_unverified">Được mã hóa bởi một thiết bị chưa được xác thực</string>
<string name="unencrypted">Không được mã hóa</string>
<string name="default_message_emote_snow">gửi tuyết rơi ❄️</string>
<string name="default_message_emote_confetti">gửi hoa giấy 🎉</string>
@ -1479,7 +1479,7 @@
<string name="command_confetti">Gửi tin nhắn đã cho với hoa giấy</string>
<string name="secure_backup_reset_no_history">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</string>
<string name="secure_backup_reset_if_you_reset_all">Nếu bạn đặt lại mọi thứ</string>
<string name="secure_backup_reset_all_no_other_devices">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.</string>
<string name="secure_backup_reset_all_no_other_devices">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.</string>
<string name="secure_backup_reset_all">Đặt lại mọi thứ</string>
<string name="bad_passphrase_key_reset_all_action">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ứ</string>
<string name="failed_to_access_secure_storage">Không truy nhập được dung lượng lưu trữ an toàn</string>
@ -1514,7 +1514,7 @@
<string name="bootstrap_invalid_recovery_key">Nó không phải là một khóa phục hồi hợp lệ</string>
<string name="use_file">Sử dụng Tệp</string>
<string name="bootstrap_enter_recovery">Nhập %s của bạn để tiếp tục</string>
<string name="security_prompt_text">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</string>
<string name="security_prompt_text">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</string>
<string name="upgrade_security">Nâng cấp mã hóa có sẵn</string>
<string name="room_message_placeholder">Tin nhắn…</string>
<string name="auth_invalid_login_deactivated_account">Tài khoản này đã bị vô hiệu hóa.</string>
@ -1560,7 +1560,7 @@
<string name="encryption_unknown_algorithm_tile_description">Mã hóa được sử dụng bởi phòng này không được hỗ trợ</string>
<string name="encryption_not_enabled">Mã hóa không được bật</string>
<string name="direct_room_encryption_enabled_tile_description">Tin nhắn trong phòng này được mã hóa đầu cuối.</string>
<string name="encryption_enabled_tile_description">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ọ.</string>
<string name="encryption_enabled_tile_description">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ọ.</string>
<string name="encryption_enabled">Mã hóa được bật</string>
<string name="bootstrap_cancel_text">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
@ -1579,9 +1579,9 @@
<string name="settings_server_version">Phiên bản máy chủ</string>
<string name="settings_server_name">Tên máy chủ</string>
<string name="settings_active_sessions_signout_device">Đăng xuất khỏi phiên này</string>
<string name="settings_active_sessions_manage">Quản lý Phiên</string>
<string name="settings_active_sessions_show_all">Hiện Tất cả Phiên</string>
<string name="settings_active_sessions_list">Phiên Hoạt động</string>
<string name="settings_active_sessions_manage">Quản lý phiên</string>
<string name="settings_active_sessions_show_all">Hiện tất cả phiên</string>
<string name="settings_active_sessions_list">Phiên hoạt động</string>
<string name="settings_hs_admin_e2e_disabled">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.</string>
<string name="encryption_information_dg_xsigning_disabled">Xác thực chéo không được kích hoạt</string>
<string name="encryption_information_dg_xsigning_not_trusted">Xác thực chéo được kích hoạt.
@ -1592,12 +1592,12 @@
<string name="encryption_information_dg_xsigning_complete">Xác thực chéo được kích hoạt
\nKhóa riêng trên thiết bị.</string>
<string name="encryption_information_cross_signing_state">Xác thực chéo</string>
<string name="verification_conclusion_ok_self_notice">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.</string>
<string name="verification_conclusion_ok_self_notice">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.</string>
<string name="verification_conclusion_ok_notice">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.</string>
<string name="verification_code_notice">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.</string>
<string name="verification_emoji_notice">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ự.</string>
<string name="verification_request_start_notice">Để đượ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.</string>
<string name="verification_request_notice">Để an toàn, hãy xác minh %s bằng cách kiểm tra mã một lần.</string>
<string name="verification_request_notice">Để an toàn, hãy xác thực %s bằng cách kiểm tra mã một lần.</string>
<string name="room_settings_enable_encryption_dialog_submit">Bật mã hóa</string>
<string name="room_settings_enable_encryption_dialog_content">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.</string>
<string name="room_settings_enable_encryption_dialog_title">Bật mã hóa\?</string>
@ -1607,8 +1607,8 @@
<string name="settings_category_timeline">Dòng thời gian</string>
<string name="command_description_rainbow_emote">Gửi emote đã cho màu cầu vồng</string>
<string name="command_description_rainbow">Gửi tin nhắn đã cho có màu cầu vồng</string>
<string name="verify_cannot_cross_sign">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.</string>
<string name="verify_cannot_cross_sign">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.</string>
<string name="unignore">Hủy bỏ qua</string>
<string name="rendering_event_error_exception">${app_name} gặp sự cố khi hiển thị nội dung sự kiện với id \'%1$s\'</string>
<string name="rendering_event_error_type_of_event_not_handled">${app_name} không xử lý các sự kiện thuộc loại \'%1$s\'</string>
@ -1643,17 +1643,17 @@
<string name="direct_room_profile_not_encrypted_subtitle">Tin nhắn ở đây không được mã hóa đầu cuối.</string>
<string name="room_profile_not_encrypted_subtitle">Tin nhắn trong phòng này không được mã hóa đầu cuối.</string>
<string name="verification_request_waiting_for">Đang chờ %s…</string>
<string name="verification_verified_user">Đã xác minh %s</string>
<string name="verification_verify_user">Xác minh %s</string>
<string name="verification_no_scan_emoji_title">Xác minh bằng cách so sánh biểu tượng cảm xúc</string>
<string name="verification_scan_self_emoji_subtitle">Thay vào đó, xác minh bằng cách so sánh biểu tượng cảm xúc</string>
<string name="verification_verified_user">Đã xác thực %s</string>
<string name="verification_verify_user">Xác thực %s</string>
<string name="verification_no_scan_emoji_title">Xác thực bằng cách so sánh biểu tượng cảm xúc</string>
<string name="verification_scan_self_emoji_subtitle">Thay vào đó, xác thực bằng cách so sánh biểu tượng cảm xúc</string>
<string name="verification_scan_emoji_subtitle">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ế</string>
<string name="verification_scan_emoji_title">Không thể quét</string>
<string name="verification_scan_with_this_device">Quét bằng thiết bị này</string>
<string name="verification_scan_their_code">Quét mã của họ</string>
<string name="verification_scan_self_notice">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</string>
<string name="verification_scan_notice">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</string>
<string name="verification_verify_device">Xác minh phiên này</string>
<string name="verification_scan_notice">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</string>
<string name="verification_verify_device">Xác thực phiên này</string>
<string name="verification_request">Yêu cầu xác minh</string>
<string name="verification_sent">Xác minh đã gửi</string>
<string name="verification_request_you_accepted">Bạn đã chấp nhận</string>
@ -1672,15 +1672,15 @@
<string name="sent_a_video">Video.</string>
<string name="verification_conclusion_compromised">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à 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</string>
<string name="verification_conclusion_not_secure">Không bảo mật</string>
<string name="verification_sas_do_not_match">Chúng không phù hợp</string>
<string name="verification_sas_match">Chúng phù hợp</string>
<string name="verification_conclusion_warning">Đăng nhập không tin cậy</string>
<string name="login_error_threepid_denied">Tên miền email của bạn không được phép đăng ký trên máy chủ này</string>
<string name="login_error_threepid_denied">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</string>
<string name="create_space_in_progress">Tạo Space…</string>
<string name="create_room_in_progress">Tạo phòng…</string>
<string name="create_room_alias_invalid">Một số ký tự không được phép</string>
@ -1749,7 +1749,7 @@
<string name="login_connect_using_matrix_id_submit">Đăng nhập bằng Matrix ID</string>
<string name="login_error_outdated_homeserver_warning_content">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.</string>
<string name="login_error_outdated_homeserver_title">Homeerver lỗi thời</string>
<string name="does_not_look_like_valid_email">Không giống như một địa chỉ email hợp lệ</string>
<string name="does_not_look_like_valid_email">Không giống một địa chỉ thư điện tử hợp lệ</string>
<string name="login_registration_not_supported">Ứ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\?</string>
@ -1769,8 +1769,8 @@
<string name="error_terms_not_accepted">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.</string>
<string name="labs_allow_extended_logging_summary">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.</string>
<string name="labs_allow_extended_logging">Bật nhật ký verbose.</string>
<string name="settings_agree_to_terms">Đồ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.</string>
<string name="settings_discovery_disconnect_with_bound_pid">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.</string>
<string name="settings_agree_to_terms">Đồ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.</string>
<string name="settings_discovery_disconnect_with_bound_pid">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.</string>
<string name="settings_text_message_sent_wrong_code">Mã xác minh không chính xác.</string>
<string name="call_dial_pad_lookup_error">Có lỗi tra cứu số điện thoại</string>
<string name="call_dial_pad_title">Bàn phím số</string>
@ -1790,24 +1790,24 @@
<string name="call_tile_other_declined">%1$s đã từ chối cuộc gọi này</string>
<string name="call_tile_you_declined_this_call">Bạn đã từ chối cuộc gọi này</string>
<string name="settings_text_message_sent_hint"></string>
<string name="settings_text_message_sent">T văn bản đã được gửi đến %s. Vui lòng nhập mã xác minh mà nó chứa.</string>
<string name="settings_discovery_no_terms">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ụ</string>
<string name="settings_discovery_no_terms_title">Máy chủ xác thực không có điều khoản dịch vụ</string>
<string name="settings_discovery_please_enter_server">Vui lòng nhập url máy chủ xác thực</string>
<string name="settings_discovery_bad_identity_server">Không thể kết nối với máy chủ xác thực</string>
<string name="settings_discovery_enter_identity_server">Nhập URL máy chủ xác thực</string>
<string name="settings_text_message_sent">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.</string>
<string name="settings_discovery_no_terms">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ụ</string>
<string name="settings_discovery_no_terms_title">Máy chủ định danh không có điều khoản dịch vụ</string>
<string name="settings_discovery_please_enter_server">Vui lòng nhập đường dẫn máy chủ định danh</string>
<string name="settings_discovery_bad_identity_server">Không thể kết nối với máy chủ định danh</string>
<string name="settings_discovery_enter_identity_server">Nhập URL máy chủ định danh</string>
<string name="identity_server_consent_dialog_content_question">Bạn có đồng ý gửi thông tin này không\?</string>
<string name="identity_server_consent_dialog_content_3">Để 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ư.</string>
<string name="identity_server_consent_dialog_title_2">Gửi địa chỉ thư điện tử và số điện thoại đến %s</string>
<string name="settings_discovery_consent_action_give_consent">Đồng ý</string>
<string name="settings_discovery_consent_action_revoke">Thu hồi sự đồng ý của tôi</string>
<string name="settings_discovery_consent_notice_off_2">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.</string>
<string name="settings_discovery_consent_notice_off_2">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.</string>
<string name="settings_discovery_consent_notice_on">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.</string>
<string name="settings_discovery_consent_title">Gửi email và số điện thoại</string>
<string name="settings_discovery_consent_title">Gửi thư điện tử và số điện thoại</string>
<string name="settings_discovery_confirm_mail_not_clicked">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</string>
<string name="settings_discovery_confirm_mail">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</string>
<string name="settings_discovery_msisdn_title">Số điện thoại có thể khám phá</string>
<string name="settings_discovery_disconnect_identity_server_info">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.</string>
<string name="settings_discovery_disconnect_identity_server_info">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.</string>
<string name="settings_discovery_no_msisdn">Tùy chọn Khám phá sẽ xuất hiện khi bạn đã thêm số điện thoại.</string>
<string name="push_gateway_item_app_id">Định danh ứng dụng (ID):</string>
<string name="settings_push_gateway_no_pushers">Không có cổng Push đã đăng ký</string>
@ -1860,12 +1860,12 @@
<string name="room_list_people_empty_body">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.</string>
<string name="room_list_people_empty_title">Các cuộc hội thoại</string>
<string name="error_user_already_logged_in">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\?</string>
<string name="identity_server_not_defined">Bạn không sử dụng bất kỳ máy chủ xác thực nào</string>
<string name="identity_server_not_defined">Bạn không sử dụng bất kỳ máy chủ định danh nào</string>
<string name="sas_error_unknown">Lỗi không xác định</string>
<string name="sas_incoming_request_notif_content">%s muốn xác minh phiên của bạn</string>
<string name="sas_incoming_request_notif_title">Yêu cầu xác minh</string>
<string name="sas_incoming_request_notif_content">%s muốn xác thực phiên của bạn</string>
<string name="sas_incoming_request_notif_title">Yêu cầu xác thực</string>
<string name="sas_got_it">Đã nhận được</string>
<string name="sas_verified">Đã xác minh!</string>
<string name="sas_verified">Đã xác thực!</string>
<string name="keys_backup_info_title_signature">Chữ ký</string>
<string name="keys_backup_info_title_algorithm">Thuật toán</string>
<string name="keys_backup_info_title_version">Phiên bản</string>
@ -1882,16 +1882,16 @@
<string name="keys_backup_settings_delete_confirm_title">Xóa Sao lưu</string>
<string name="keys_backup_settings_checking_backup_state">Kiểm tra trạng thái sao lưu</string>
<string name="keys_backup_settings_deleting_backup">Đang xóa bản sao lưu…</string>
<string name="keys_backup_settings_untrusted_backup">Để 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ờ.</string>
<string name="keys_backup_settings_invalid_signature_from_unverified_device">Sao lưu có chữ ký không hợp lệ từ phiên chưa được xác minh %s</string>
<string name="keys_backup_settings_invalid_signature_from_verified_device">Sao lưu có chữ ký không hợp lệ từ phiên đã xác minh %s</string>
<string name="keys_backup_settings_valid_signature_from_unverified_device">Sao lưu có chữ ký hợp lệ từ phiên chưa được xác minh %s</string>
<string name="keys_backup_settings_valid_signature_from_verified_device">Sao lưu có chữ ký hợp lệ từ phiên đã xác minh %s.</string>
<string name="keys_backup_settings_untrusted_backup">Để 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ờ.</string>
<string name="keys_backup_settings_invalid_signature_from_unverified_device">Sao lưu có chữ ký không hợp lệ từ phiên chưa được xác thực %s</string>
<string name="keys_backup_settings_invalid_signature_from_verified_device">Sao lưu có chữ ký không hợp lệ từ phiên đã xác thực %s</string>
<string name="keys_backup_settings_valid_signature_from_unverified_device">Sao lưu có chữ ký hợp lệ từ phiên chưa được xác thực %s</string>
<string name="keys_backup_settings_valid_signature_from_verified_device">Sao lưu có chữ ký hợp lệ từ phiên đã xác thực %s.</string>
<string name="keys_backup_settings_valid_signature_from_this_device">Sao lưu có chữ ký hợp lệ từ phiên này.</string>
<string name="keys_backup_settings_signature_from_unknown_device">Sao lưu có chữ ký từ phiên không xác định với ID %s.</string>
<string name="keys_backup_settings_signature_from_unknown_device">Sao lưu có chữ ký từ phiên không xác định với định danh %s.</string>
<string name="keys_backup_settings_status_not_setup">Khóa của bạn không được sao lưu từ phiên này.</string>
<string name="keys_backup_settings_status_ko">Sao lưu chính không hoạt động trong phiên này.</string>
<string name="keys_backup_settings_status_ok">Key Backup đã được thiết lập chính xác cho phiên này.</string>
<string name="keys_backup_settings_status_ko">Sao lưu khóa không hoạt động trong phiên này.</string>
<string name="keys_backup_settings_status_ok">Sao lưu khóa đã được thiết lập chính xác cho phiên này.</string>
<string name="keys_backup_settings_delete_backup_button">Xóa Sao lưu</string>
<string name="keys_backup_settings_restore_backup_button">Khôi phục từ Sao lưu</string>
<string name="keys_backup_get_version_error">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).</string>
@ -1968,17 +1968,17 @@
<string name="verify_not_me_self_verification">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.</string>
<string name="verify_cancel_other">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ọ.</string>
<string name="verify_cancel_other">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ọ.</string>
<string name="verify_cancel_self_verification_from_trusted">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ó</string>
<string name="verify_cancel_self_verification_from_untrusted">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ó</string>
<string name="verify_new_session_compromized">Tài khoản của bạn có thể bị xâm phạm</string>
<string name="verify_new_session_was_not_me">Không phải tôi</string>
<string name="verify_new_session_notice">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.</string>
<string name="verify_new_session_notice">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.</string>
<string name="new_session">Đăng nhập mới. Đây có phải là bạn không\?</string>
<string name="refresh">Làm tươi</string>
<string name="e2e_use_keybackup">Mở khóa lịch sử tin nhắn được mã hóa</string>
@ -2011,20 +2011,20 @@
<string name="a11y_qr_code_for_verification">Mã QR</string>
<string name="reset_cross_signing">Đặt lại khóa</string>
<string name="initialize_cross_signing">Khởi tạo xác thực chéo</string>
<string name="verification_profile_device_untrust_info">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.</string>
<string name="verification_profile_device_untrust_info">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.</string>
<string name="verification_profile_device_new_signing">%1$s (%2$s) đã đăng nhập bằng phiên mới:</string>
<string name="verification_profile_device_verified_because">Phiên này được tin cậy để nhắn tin an toàn vì %1$s (%2$s) đã xác minh:</string>
<string name="verification_profile_device_verified_because">Phiên này được tin cậy để nhắn tin an toàn vì %1$s (%2$s) đã xác thực:</string>
<string name="not_trusted">Không tin cậy</string>
<string name="trusted">Tin cậy</string>
<string name="room_member_profile_sessions_section_title">Phiên</string>
<string name="room_member_profile_failed_to_get_devices">Không nhận được phiên</string>
<string name="verification_profile_warning">Cảnh báo</string>
<string name="verification_profile_verified">Đã xác minh</string>
<string name="verification_profile_verify">Xác minh</string>
<string name="verification_open_other_to_verify">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.</string>
<string name="crosssigning_verify_this_session">Xác minh đăng nhập này</string>
<string name="settings_active_sessions_unverified_device_desc">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:</string>
<string name="settings_active_sessions_verified_device_desc">Phiên này được tin cậy để nhắn tin an toàn vì bạn đã xác minh nó:</string>
<string name="verification_profile_verified">Đã xác thực</string>
<string name="verification_profile_verify">Xác thực</string>
<string name="verification_open_other_to_verify">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.</string>
<string name="crosssigning_verify_this_session">Xác thực thiết bị này</string>
<string name="settings_active_sessions_unverified_device_desc">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:</string>
<string name="settings_active_sessions_verified_device_desc">Phiên này được tin cậy để nhắn tin an toàn vì bạn đã xác thực nó:</string>
<string name="settings_failed_to_get_crypto_device_info">Không có thông tin mật mã sẵn dùng</string>
<string name="settings_server_room_version_unstable">bất ổn định</string>
<string name="settings_server_room_version_stable">ổn định</string>
@ -2058,9 +2058,9 @@
<string name="error_empty_field_enter_user_name">Vui lòng nhập tên người dùng.</string>
<string name="deactivate_account_submit">Hủy kích hoạt Tài khoản</string>
<string name="deactivate_account_delete_checkbox">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 đủ)</string>
<string name="deactivate_account_content">Đ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. <b>Điều này là không thể đảo ngược</b>.
<string name="deactivate_account_content">Đ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. &lt;b&gt;Điều này là không thể đảo ngược&lt;/b&gt;.
\n
\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.
\nHủy kích hoạt tài khoản của bạn &lt;b&gt;does không theo mặc định khiến chúng tôi quên tin nhắn bạn đã gửi&lt;/b&gt;. 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ọ.</string>
<string name="dialog_user_consent_content">Để 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.</string>
@ -2102,11 +2102,11 @@
<string name="unrecognized_command">Lệnh không được nhận ra: %s</string>
<string name="command_error">Lỗi lệnh</string>
<string name="key_share_request">Yêu cầu Chia sẻ Khóa</string>
<string name="your_unverified_device_requesting_with_info">Một phiên chưa được xác minh đang yêu cầu khóa mã hóa.
<string name="your_unverified_device_requesting_with_info">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.</string>
<string name="your_unverified_device_requesting">Phiên chưa được xác thực của bạn \'%s\' đang yêu cầu khóa mã hóa.</string>
<string name="your_unverified_device_requesting">Phiên chưa được xác thực \'%s\' đang yêu cầu khóa mã hóa.</string>
<string name="you_added_a_new_device_with_info">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 @@
<string name="directory_server_all_rooms_on_server">Tất cả các phòng trên %s server</string>
<string name="directory_server_placeholder">Tên máy chủ</string>
<string name="select_room_directory">Chọn thư mục phòng</string>
<string name="encryption_never_send_to_unverified_devices_summary">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.</string>
<string name="encryption_never_send_to_unverified_devices_title">Chỉ mã hóa cho các phiên đã xác minh</string>
<string name="encryption_never_send_to_unverified_devices_summary">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.</string>
<string name="encryption_never_send_to_unverified_devices_title">Chỉ mã hóa cho các phiên đã xác thực</string>
<string name="room_settings_unset_main_address">Bỏ đặt làm địa chỉ chính</string>
<string name="room_settings_set_main_address">Đặt làm địa chỉ chính</string>
<string name="legals_no_policy_provided">Máy chủ này không cung cấp bất kỳ chính sách nào.</string>
<string name="legals_third_party_notices">Thư viện bên thứ ba</string>
<string name="legals_identity_server_title">Chính sách máy chủ xác thực của bạn</string>
<string name="legals_identity_server_title">Chính sách máy chủ định danh của bạn</string>
<string name="legals_home_server_title">Chính sách homeerver của bạn</string>
<string name="legals_application_title">Chính sách ${app_name}</string>
<string name="analytics_opt_in_list_item_3">Bạn có thể tắt tính năng này bất cứ lúc nào trong cài đặt</string>
@ -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.</string>
<string name="analytics_opt_in_title">Giúp cải thiện ${app_name}</string>
<string name="shortcut_disabled_reason_sign_out">Thiết bị đã bị đăng xuất!</string>
<string name="shortcut_disabled_reason_sign_out">Phiên đã bị đăng xuất!</string>
<string name="shortcut_disabled_reason_room_left">Căn phòng đã bị bỏ lại!</string>
<string name="login_error_homeserver_from_url_not_found_enter_manual">Chọn homeerver</string>
<string name="login_error_homeserver_from_url_not_found">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.</string>
@ -2207,11 +2207,11 @@
<string name="action_enable">Kích hoạt</string>
<string name="notification_listening_for_notifications">Nghe thông báo</string>
<string name="settings_discovery_no_mails">Tùy chọn Khám phá sẽ xuất hiện sau khi bạn đã thêm địa chỉ thư điện tử.</string>
<string name="settings_discovery_emails_title">Địa chỉ email có thể khám phá</string>
<string name="settings_discovery_identity_server_info_none">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.</string>
<string name="settings_discovery_no_policy_provided">Không có chính sách được cung cấp bởi máy chủ xác thực</string>
<string name="settings_discovery_hide_identity_server_policy_title">Ẩn chính sách máy chủ xác thực</string>
<string name="settings_discovery_show_identity_server_policy_title">Hiện chính sách máy chủ xác thực</string>
<string name="settings_discovery_emails_title">Địa chỉ thư điện tử có thể khám phá</string>
<string name="settings_discovery_identity_server_info_none">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.</string>
<string name="settings_discovery_no_policy_provided">Máy chủ định danh không cung cấp chính sách nào</string>
<string name="settings_discovery_hide_identity_server_policy_title">Ẩn chính sách máy chủ định danh</string>
<string name="settings_discovery_show_identity_server_policy_title">Hiện chính sách máy chủ định danh</string>
<string name="open_discovery_settings">Mở Cài đặt Khám phá</string>
<string name="labs_show_unread_notifications_as_tab">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.</string>
<string name="user_directory_search_hint_2">Tìm kiếm theo tên, ID hoặc thư</string>
@ -2334,7 +2334,7 @@
<string name="ftue_display_name_entry_footer">Bạn có thể đổi lại sau</string>
<string name="ftue_display_name_entry_title">Tên hiển thị</string>
<string name="ftue_display_name_title">Chọn tên hiển thị</string>
<string name="ftue_auth_login_username_entry">Tên người dùng / Thư điện tử / Số điện thoại</string>
<string name="ftue_auth_login_username_entry">Tên người dùng / Địa chỉ thư điện tử / Số điện thoại</string>
<string name="ftue_auth_captcha_title">Bạn có phải con người\?</string>
<string name="ftue_auth_password_reset_email_confirmation_subtitle">Làm theo chỉ dẫn gửi tới %s</string>
<string name="ftue_auth_password_reset_confirmation">Đặt lại mật khẩu</string>
@ -2376,7 +2376,7 @@
<string name="give_feedback_threads">Gửi phản hồi</string>
<string name="push_gateway_item_enabled">Đã bật:</string>
<string name="push_gateway_item_profile_tag">Thẻ hồ sơ:</string>
<string name="push_gateway_item_device_id">Định danh của phiên:</string>
<string name="push_gateway_item_device_id">Định danh phiên:</string>
<string name="create_room_action_go">Đi</string>
<string name="updating_your_data">Đang cập nhật dữ liệu của bạn…</string>
<string name="room_list_filter_people">Mọi người</string>
@ -2477,7 +2477,7 @@
<string name="ftue_auth_terms_title">Chính sách máy chủ</string>
<string name="ftue_auth_reset_password_email_subtitle">%s sẽ gửi bạn một liên kết xác nhận</string>
<string name="ftue_auth_carousel_secure_title">Làm chủ cuộc trò chuyện.</string>
<string name="ftue_auth_phone_subtitle">%s cần xác nhận tài khoản của bạn</string>
<string name="ftue_auth_phone_subtitle">%s cần xác thực tài khoản của bạn</string>
<string name="ftue_auth_welcome_back_title">Chào mừng trở lại!</string>
<string name="ftue_auth_choose_server_sign_in_subtitle">Địa chỉ máy chủ của bạn là gì\?</string>
<string name="ftue_auth_use_case_subtitle">Chúng tôi sẽ giúp bạn kết nối</string>
@ -2531,4 +2531,27 @@
<string name="device_manager_other_sessions_hide_ip_address">Ẩn địa chỉ Internet (IP)</string>
<string name="device_manager_learn_more_sessions_verified_title">Phiên đã xác thực</string>
<string name="device_manager_session_details_device_operating_system">Hệ điều hành</string>
<string name="thread_list_modal_title">Bộ lọc</string>
<string name="room_threads_filter">Bộ lọc các chủ đề trong phòng</string>
<string name="action_thread_copy_link_to_thread">Sao chép liên kết vào cuộc trò chuyện</string>
<string name="thread_list_title">Các chủ đề</string>
<string name="threads_beta_enable_notice_title">Bản Beta của Chủ đề</string>
<string name="threads_labs_enable_notice_title">Bản Beta của Chủ đề</string>
<string name="threads_notice_migration_title">Các chủ đề đã chuyển sang Beta 🎉</string>
<string name="thread_list_modal_all_threads_subtitle">Hiển thị tất cả chủ đề trong phòng</string>
<string name="thread_list_modal_all_threads_title">Tất cả chủ đề</string>
<string name="thread_list_empty_title">Sắp xếp các cuộc thảo luận với các chủ đề</string>
<string name="thread_timeline_title">Chủ đề</string>
<string name="thread_list_modal_my_threads_subtitle">Hiển thị tất cả chủ đề bạn đã tham gia</string>
<string name="labs_enable_new_app_layout_summary">Element đã được đơn giản hóa với các mục tùy chọn</string>
<string name="labs_enable_deferred_dm_summary">Chỉ tạo cuộc trò chuyện riêng cho tin nhắn đầu tiên</string>
<string name="labs_enable_deferred_dm_title">Bật trì hoãn cho các tin nhắn riêng</string>
<string name="thread_list_modal_my_threads_title">Các chủ đề của tôi</string>
<string name="search_thread_from_a_thread">Từ một chủ đề</string>
<string name="thread_list_empty_subtitle">\'\'Chủ đề\'\' giúp giữ cho các cuộc trò chuyện của bạn theo chủ đề và dễ theo dõi.</string>
<string name="thread_list_not_available">Máy chủ nhà của bạn chưa hỗ trợ cho việc liệt kê các chủ đề.</string>
<string name="threads_beta_enable_notice_message">\'\'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.</string>
<string name="device_manager_session_title">Phiên</string>
<string name="device_manager_device_title">Thiết bị</string>
<string name="device_manager_session_last_activity">Hoạt động cuối %1$s</string>
</resources>

View file

@ -1734,7 +1734,7 @@
<string name="settings_show_emoji_keyboard_summary">在消息框添加打开 emoji 键盘的按钮</string>
<string name="settings_show_emoji_keyboard">显示 emoji 键盘</string>
<string name="settings_chat_effects_description">使用 /confetti 命令或发送包含 ❄️ 或 🎉 的消息</string>
<string name="settings_chat_effects_title">显示聊天效</string>
<string name="settings_chat_effects_title">显示聊天</string>
<string name="room_permissions_change_topic">更改话题</string>
<string name="room_permissions_upgrade_the_room">升级房间</string>
<string name="room_permissions_send_m_room_server_acl_events">发送 m.room.server_acl 事件</string>
@ -2846,4 +2846,23 @@
<string name="direct_room_user_list_only_invite_one_email">你一次仅能邀请一个电子邮件</string>
<string name="started_a_voice_broadcast">开始语音广播</string>
<string name="_resume">继续</string>
<string name="message_reply_to_ended_poll_preview">已结束投票</string>
<string name="message_reply_to_poll_preview">投票</string>
<string name="room_polls_ended">过去的投票</string>
<string name="pill_message_from_unknown_user">消息</string>
<string name="room_polls_load_more">加载更多投票</string>
<string name="set_link_link">链接</string>
<string name="set_link_create">创建链接</string>
<string name="set_link_edit">编辑链接</string>
<string name="rich_text_editor_bullet_list">切换无序列表</string>
<string name="pill_message_unknown_room_or_space">房间/空间</string>
<string name="room_polls_active">进行中的投票</string>
<string name="rich_text_editor_unindent">取消缩进</string>
<string name="set_link_text">文本</string>
<string name="message_reply_to_sender_ended_poll">已结束投票。</string>
<string name="rich_text_editor_indent">缩进</string>
<string name="rich_text_editor_link">设置链接</string>
<string name="room_polls_wait_for_display">显示投票</string>
<string name="rich_text_editor_quote">切换引用</string>
<string name="rich_text_editor_numbered_list">切换有序列表</string>
</resources>

View file

@ -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()}\""
@ -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

View file

@ -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.

View file

@ -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<String>? = null, additionalContent: Content? = null): Cancelable
fun redactEvent(event: Event, reason: String?, withRelTypes: List<String>? = null, additionalContent: Content? = null): Cancelable
/**
* Schedule this message to be resent.

View file

@ -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()
}

View file

@ -30,7 +30,7 @@ internal interface RedactEventTask : Task<RedactEventTask.Params, String> {
val roomId: String,
val eventId: String,
val reason: String?,
val withRelations: List<String>?,
val withRelTypes: List<String>?,
)
}
@ -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,
)
)
}

View file

@ -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,
)
}

View file

@ -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) {

View file

@ -140,11 +140,11 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) }
}
override fun redactEvent(event: Event, reason: String?, withRelations: List<String>?, additionalContent: Content?): Cancelable {
override fun redactEvent(event: Event, reason: String?, withRelTypes: List<String>?, 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 {

View file

@ -812,12 +812,12 @@ internal class LocalEchoEventFactory @Inject constructor(
}
}
*/
fun createRedactEvent(roomId: String, eventId: String, reason: String?, withRelations: List<String>? = null, additionalContent: Content? = null): Event {
fun createRedactEvent(roomId: String, eventId: String, reason: String?, withRelTypes: List<String>? = 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

View file

@ -43,7 +43,7 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters, ses
val roomId: String,
val eventId: String,
val reason: String?,
val withRelations: List<String>? = null,
val withRelTypes: List<String>? = 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(

View file

@ -25,5 +25,10 @@ internal data class EventRedactBody(
val reason: String? = null,
@Json(name = "org.matrix.msc3912.with_relations")
val withRelations: List<String>? = null,
)
val unstableWithRelTypes: List<String>? = null,
@Json(name = "with_rel_types")
val withRelTypes: List<String>? = null,
) {
fun getBestWithRelTypes() = withRelTypes ?: unstableWithRelTypes
}

View file

@ -26,9 +26,9 @@ internal interface EventSenderProcessor : SessionLifecycleObserver {
fun postEvent(event: Event, encrypt: Boolean): Cancelable
fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelations: List<String>? = null): Cancelable
fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelTypes: List<String>? = null): Cancelable
fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?, withRelations: List<String>? = null): Cancelable
fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?, withRelTypes: List<String>? = null): Cancelable
fun postTask(task: QueuedTask): Cancelable

View file

@ -101,8 +101,8 @@ internal class EventSenderProcessorCoroutine @Inject constructor(
return postTask(task)
}
override fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelations: List<String>?): Cancelable {
return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason, withRelations)
override fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelTypes: List<String>?): 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<String>?
withRelTypes: List<String>?
): Cancelable {
val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason, withRelations)
val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason, withRelTypes)
return postTask(task)
}

View file

@ -118,7 +118,7 @@ internal class QueueMemento @Inject constructor(
eventId = it.redacts,
roomId = it.roomId,
reason = body?.reason,
withRelations = body?.withRelations,
withRelTypes = body?.getBestWithRelTypes(),
)
)
}

View file

@ -43,13 +43,13 @@ internal class QueuedTaskFactory @Inject constructor(
)
}
fun createRedactTask(redactionLocalEcho: String, eventId: String, roomId: String, reason: String?, withRelations: List<String>? = null): QueuedTask {
fun createRedactTask(redactionLocalEcho: String, eventId: String, roomId: String, reason: String?, withRelTypes: List<String>? = null): QueuedTask {
return RedactQueuedTask(
redactionLocalEchoId = redactionLocalEcho,
toRedactEventId = eventId,
roomId = roomId,
reason = reason,
withRelations = withRelations,
withRelTypes = withRelTypes,
redactEventTask = redactEventTask,
localEchoRepository = localEchoRepository,
cancelSendTracker = cancelSendTracker

View file

@ -26,14 +26,14 @@ internal class RedactQueuedTask(
val redactionLocalEchoId: String,
private val roomId: String,
private val reason: String?,
private val withRelations: List<String>?,
private val withRelTypes: List<String>?,
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() {

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.sync.handler
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorkerDataRepository
import org.matrix.android.sdk.internal.di.SessionId
@ -81,7 +82,9 @@ internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor(
}
}
if (hasUpdate) {
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats))
tryOrNull("Unable to update user account data") {
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats))
}
}
}

View file

@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmList
import io.realm.kotlin.where
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.failure.InitialSyncRequestReason
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
@ -122,7 +123,7 @@ internal class UserAccountDataSyncHandler @Inject constructor(
val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams(
directMessages = directChats
)
updateUserAccountDataTask.execute(updateUserAccountParams)
tryOrNull("Unable to update user account data") { updateUserAccountDataTask.execute(updateUserAccountParams) }
}
}

View file

@ -11,6 +11,12 @@ include ':library:multipicker'
include ':library:external:jsonviewer'
include ':library:external:diff-match-patch'
include ':library:external:dialpad'
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'

View file

@ -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'
@ -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'

View file

@ -116,6 +116,8 @@ 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:external:autocomplete")
implementation project(":library:ui-strings")
implementation project(":library:ui-styles")
implementation project(":library:core-utils")
@ -184,11 +186,8 @@ 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'
}
implementation project(":library:external:span")
implementation libs.markwon.core
implementation libs.markwon.extLatex
implementation libs.markwon.imageGlide
@ -210,8 +209,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'
@ -266,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

View file

@ -23,7 +23,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.otaliastudios.autocomplete.AutocompletePresenter
abstract class RecyclerViewPresenter<T>(context: Context?) : AutocompletePresenter<T>(context) {
abstract class RecyclerViewPresenter<T : Any>(context: Context) : AutocompletePresenter<T>(context) {
private var recyclerView: RecyclerView? = null
private var clicks: ClickProvider<T>? = null

View file

@ -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)
}
}

View file

@ -20,6 +20,7 @@ import im.vector.app.core.platform.VectorViewModelAction
import java.io.OutputStream
sealed class BootstrapActions : VectorViewModelAction {
object Retry : BootstrapActions()
// Navigation

View file

@ -212,6 +212,11 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetBoot
views.bootstrapTitleText.text = getString(R.string.upgrade_security)
showFragment(BootstrapMigrateBackupFragment::class)
}
is BootstrapStep.Error -> {
views.bootstrapIcon.isVisible = true
views.bootstrapTitleText.text = getString(R.string.bottom_sheet_setup_secure_backup_title)
showFragment(BootstrapErrorFragment::class)
}
}
super.invalidate()
}

View file

@ -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<FragmentBootstrapErrorBinding>() {
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)
}
}
}

View file

@ -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))
}
}
}
}

View file

@ -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 {

View file

@ -71,6 +71,7 @@ import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerC
import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.unwrap
import timber.log.Timber
import java.io.File
import java.net.URL
import java.util.UUID
@ -265,7 +266,17 @@ class VectorSettingsGeneralFragment :
// Disable it while updating the state, will be re-enabled by the account data listener.
it.isEnabled = false
lifecycleScope.launch {
session.integrationManagerService().setIntegrationEnabled(newValue as Boolean)
try {
session.integrationManagerService().setIntegrationEnabled(newValue as Boolean)
} catch (failure: Throwable) {
Timber.e(failure, "Failed to update integration manager state")
activity?.let { activity ->
Toast.makeText(activity, errorFormatter.toHumanReadable(failure), Toast.LENGTH_SHORT).show()
}
// Restore the previous state
it.isChecked = !it.isChecked
it.isEnabled = true
}
}
true
}

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="200dp"
android:padding="16dp">
<TextView
android:id="@+id/bootstrapDescriptionText"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/error_check_network"
android:textColor="?vctr_content_primary"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/bootstrapRetryButton"
style="@style/Widget.Vector.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/global_retry"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bootstrapDescriptionText"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>