Tránh nút nhiều lần nhấp nhanh


86

Tôi gặp sự cố với ứng dụng của mình rằng nếu người dùng nhấp nhanh vào nút nhiều lần, thì nhiều sự kiện sẽ được tạo trước khi hộp thoại của tôi giữ nút biến mất

Tôi biết một giải pháp bằng cách đặt biến boolean làm cờ khi một nút được nhấp để có thể ngăn chặn các nhấp chuột trong tương lai cho đến khi hộp thoại đóng. Tuy nhiên, tôi có nhiều nút và phải làm điều này mọi lúc cho mọi nút dường như là một việc làm quá mức cần thiết. Không có cách nào khác trong android (hoặc có thể là một số giải pháp thông minh hơn) để chỉ cho phép hành động sự kiện được tạo mỗi lần nhấp vào nút?

Điều tồi tệ hơn nữa là nhiều nhấp chuột nhanh dường như tạo ra nhiều hành động sự kiện trước khi hành động đầu tiên được xử lý, vì vậy nếu tôi muốn tắt nút trong phương pháp xử lý nhấp chuột đầu tiên, thì đã có các hành động sự kiện hiện có trong hàng đợi được xử lý!

Xin vui lòng giúp đỡ Cảm ơn


chúng ta có thể triển khai mã bằng mã GreyBeardGeek không. Tôi không chắc bạn đã sử dụng lớp trừu tượng trong Hoạt động của bạn như thế nào?
CoDe

Hãy thử giải pháp này cũng như stackoverflow.com/questions/20971484/…
DmitryBoyko

Tôi giải quyết một vấn đề tương tự sử dụng backpressure trong RxJava
arekolek

Câu trả lời:


111

Đây là một trình nghe onClick 'đã gỡ lỗi' mà tôi đã viết gần đây. Bạn cho nó biết số mili giây tối thiểu có thể chấp nhận được giữa các lần nhấp là bao nhiêu. Thực hiện logic của bạn onDebouncedClickthay vìonClick

import android.os.SystemClock;
import android.view.View;

import java.util.Map;
import java.util.WeakHashMap;

/**
 * A Debounced OnClickListener
 * Rejects clicks that are too close together in time.
 * This class is safe to use as an OnClickListener for multiple views, and will debounce each one separately.
 */
public abstract class DebouncedOnClickListener implements View.OnClickListener {

    private final long minimumIntervalMillis;
    private Map<View, Long> lastClickMap;

    /**
     * Implement this in your subclass instead of onClick
     * @param v The view that was clicked
     */
    public abstract void onDebouncedClick(View v);

    /**
     * The one and only constructor
     * @param minimumIntervalMillis The minimum allowed time between clicks - any click sooner than this after a previous click will be rejected
     */
    public DebouncedOnClickListener(long minimumIntervalMillis) {
        this.minimumIntervalMillis = minimumIntervalMillis;
        this.lastClickMap = new WeakHashMap<>();
    }

    @Override 
    public void onClick(View clickedView) {
        Long previousClickTimestamp = lastClickMap.get(clickedView);
        long currentTimestamp = SystemClock.uptimeMillis();

        lastClickMap.put(clickedView, currentTimestamp);
        if(previousClickTimestamp == null || Math.abs(currentTimestamp - previousClickTimestamp) > minimumIntervalMillis) {
            onDebouncedClick(clickedView);
        }
    }
}

3
Ồ giá như mọi trang web có thể bị phiền khi triển khai một cái gì đó dọc theo những dòng này! Không còn "đừng nhấp vào gửi hai lần hoặc bạn sẽ bị tính phí hai lần"! Cảm ơn bạn.
Floris

2
Theo như tôi biết, không. OnClick phải luôn xảy ra trên chuỗi giao diện người dùng (chỉ có một). Vì vậy, nhiều lần nhấp, trong khi kết thúc thời gian, phải luôn tuần tự, trên cùng một chuỗi.
GreyBeardGeek

45
@FengDai Thú vị - "mớ hỗn độn" này thực hiện những gì thư viện của bạn thực hiện, trong khoảng 1/5 lượng mã. Bạn có một giải pháp thay thế thú vị, nhưng đây không phải là nơi để xúc phạm mã làm việc.
GreyBeardGeek

13
@GreyBeardGeek Xin lỗi vì đã dùng từ tệ hại đó.
Feng Dai

2
Đây là điều chỉnh, không phải gỡ lỗi.
Rewieer

71

Với RxBinding, nó có thể được thực hiện một cách dễ dàng. Đây là một ví dụ:

RxView.clicks(view).throttleFirst(500, TimeUnit.MILLISECONDS).subscribe(empty -> {
            // action on click
        });

Thêm dòng sau vào build.gradleđể thêm phụ thuộc RxBinding:

compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'

2
Quyết định tốt nhất. Chỉ cần lưu ý rằng điều này giữ liên kết chặt chẽ đến chế độ xem của bạn để phân đoạn sẽ không được thu thập sau khi hoàn tất vòng đời thông thường - hãy bọc nó vào thư viện Vòng đời của Trello. Sau đó, nó sẽ bỏ đăng ký và chế độ xem phóng thích sau khi phương pháp vòng đời chọn được gọi là (thường onDestroyView)
Den Drobiazko

2
Điều này không hoạt động với bộ điều hợp, vẫn có thể nhấp vào nhiều mục và nó sẽ thực hiện điều chỉnh Đầu tiên nhưng trên từng chế độ xem riêng lẻ.
desgraci.

1
Có thể thực hiện với Handler một cách dễ dàng, không cần sử dụng RxJava tại đây.
Miha_x64

20

Đây là phiên bản của tôi về câu trả lời được chấp nhận. Nó rất giống nhau, nhưng không cố gắng lưu trữ Lượt xem trong Bản đồ mà tôi không nghĩ là một ý tưởng hay. Nó cũng bổ sung một phương pháp bọc có thể hữu ích trong nhiều trường hợp.

/**
 * Implementation of {@link OnClickListener} that ignores subsequent clicks that happen too quickly after the first one.<br/>
 * To use this class, implement {@link #onSingleClick(View)} instead of {@link OnClickListener#onClick(View)}.
 */
public abstract class OnSingleClickListener implements OnClickListener {
    private static final String TAG = OnSingleClickListener.class.getSimpleName();

    private static final long MIN_DELAY_MS = 500;

    private long mLastClickTime;

    @Override
    public final void onClick(View v) {
        long lastClickTime = mLastClickTime;
        long now = System.currentTimeMillis();
        mLastClickTime = now;
        if (now - lastClickTime < MIN_DELAY_MS) {
            // Too fast: ignore
            if (Config.LOGD) Log.d(TAG, "onClick Clicked too quickly: ignored");
        } else {
            // Register the click
            onSingleClick(v);
        }
    }

    /**
     * Called when a view has been clicked.
     * 
     * @param v The view that was clicked.
     */
    public abstract void onSingleClick(View v);

    /**
     * Wraps an {@link OnClickListener} into an {@link OnSingleClickListener}.<br/>
     * The argument's {@link OnClickListener#onClick(View)} method will be called when a single click is registered.
     * 
     * @param onClickListener The listener to wrap.
     * @return the wrapped listener.
     */
    public static OnClickListener wrap(final OnClickListener onClickListener) {
        return new OnSingleClickListener() {
            @Override
            public void onSingleClick(View v) {
                onClickListener.onClick(v);
            }
        };
    }
}

Làm thế nào để sử dụng phương pháp quấn? Bạn có thể vui lòng cho một ví dụ.
Mehul Joisar

1
@MehulJoisar Như thế này:myButton.setOnClickListener(OnSingleClickListener.wrap(myOnClickListener))
BoD

10

Chúng tôi có thể làm điều đó mà không cần bất kỳ thư viện nào. Chỉ cần tạo một hàm tiện ích mở rộng duy nhất:

fun View.clickWithDebounce(debounceTime: Long = 600L, action: () -> Unit) {
    this.setOnClickListener(object : View.OnClickListener {
        private var lastClickTime: Long = 0

        override fun onClick(v: View) {
            if (SystemClock.elapsedRealtime() - lastClickTime < debounceTime) return
            else action()

            lastClickTime = SystemClock.elapsedRealtime()
        }
    })
}

Xem onClick bằng mã dưới đây:

buttonShare.clickWithDebounce { 
   // Do anything you want
}

Điều này thật tuyệt! Mặc dù tôi nghĩ thuật ngữ thích hợp hơn ở đây là "tiết lưu" chứ không phải "suy nhược". tức là clickWithThrottle ()
hopia

9

Bạn có thể sử dụng dự án này: https://github.com/fengdai/clickguard để giải quyết vấn đề này bằng một câu lệnh:

ClickGuard.guard(button);

CẬP NHẬT: Thư viện này không được khuyến khích nữa. Tôi thích giải pháp của Nikita hơn. Sử dụng RxBinding thay thế.


@Feng Đại, làm thế nào để sử dụng thư viện này choListView
CAMOBAP

Làm thế nào để sử dụng điều này trong studio android?
VVB

Thư viện này không được khuyến khích nữa. Vui lòng sử dụng RxBinding thay thế. Hãy xem câu trả lời của Nikita.
Feng Dai

7

Đây là một ví dụ đơn giản:

public abstract class SingleClickListener implements View.OnClickListener {
    private static final long THRESHOLD_MILLIS = 1000L;
    private long lastClickMillis;

    @Override public void onClick(View v) {
        long now = SystemClock.elapsedRealtime();
        if (now - lastClickMillis > THRESHOLD_MILLIS) {
            onClicked(v);
        }
        lastClickMillis = now;
    }

    public abstract void onClicked(View v);
}

1
mẫu và đẹp!
Chulo

5

Chỉ là một bản cập nhật nhanh về giải pháp GreyBeardGeek. Thay đổi mệnh đề if và thêm hàm Math.abs. Đặt nó như thế này:

  if(previousClickTimestamp == null || (Math.abs(currentTimestamp - previousClickTimestamp.longValue()) > minimumInterval)) {
        onDebouncedClick(clickedView);
    }

Người dùng có thể thay đổi thời gian trên thiết bị Android và đưa nó vào quá khứ, vì vậy nếu không có điều này, nó có thể dẫn đến lỗi.

PS: không có đủ điểm để nhận xét về giải pháp của bạn, vì vậy tôi chỉ đặt một đáp án khác.


5

Đây là một cái gì đó sẽ hoạt động với bất kỳ sự kiện nào, không chỉ các nhấp chuột. Nó cũng sẽ cung cấp sự kiện cuối cùng ngay cả khi nó là một phần của chuỗi sự kiện nhanh chóng (như rx debounce ).

class Debouncer(timeout: Long, unit: TimeUnit, fn: () -> Unit) {

    private val timeoutMillis = unit.toMillis(timeout)

    private var lastSpamMillis = 0L

    private val handler = Handler()

    private val runnable = Runnable {
        fn()
    }

    fun spam() {
        if (SystemClock.uptimeMillis() - lastSpamMillis < timeoutMillis) {
            handler.removeCallbacks(runnable)
        }
        handler.postDelayed(runnable, timeoutMillis)
        lastSpamMillis = SystemClock.uptimeMillis()
    }
}


// example
view.addOnClickListener.setOnClickListener(object: View.OnClickListener {
    val debouncer = Debouncer(1, TimeUnit.SECONDS, {
        showSomething()
    })

    override fun onClick(v: View?) {
        debouncer.spam()
    }
})

1) Xây dựng Debouncer trong một trường của trình nghe nhưng bên ngoài chức năng gọi lại, được định cấu hình với thời gian chờ và fn gọi lại mà bạn muốn điều chỉnh.

2) Gọi phương thức thư rác của Debouncer trong chức năng gọi lại của người nghe.


4

Giải pháp tương tự sử dụng RxJava

import android.view.View;

import java.util.concurrent.TimeUnit;

import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action1;
import rx.subjects.PublishSubject;

public abstract class SingleClickListener implements View.OnClickListener {
    private static final long THRESHOLD_MILLIS = 600L;
    private final PublishSubject<View> viewPublishSubject = PublishSubject.<View>create();

    public SingleClickListener() {
        viewPublishSubject.throttleFirst(THRESHOLD_MILLIS, TimeUnit.MILLISECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<View>() {
                    @Override
                    public void call(View view) {
                        onClicked(view);
                    }
                });
    }

    @Override
    public void onClick(View v) {
        viewPublishSubject.onNext(v);
    }

    public abstract void onClicked(View v);
}

4

Vì vậy, câu trả lời này được cung cấp bởi thư viện ButterKnife.

package butterknife.internal;

import android.view.View;

/**
 * A {@linkplain View.OnClickListener click listener} that debounces multiple clicks posted in the
 * same frame. A click on one button disables all buttons for that frame.
 */
public abstract class DebouncingOnClickListener implements View.OnClickListener {
  static boolean enabled = true;

  private static final Runnable ENABLE_AGAIN = () -> enabled = true;

  @Override public final void onClick(View v) {
    if (enabled) {
      enabled = false;
      v.post(ENABLE_AGAIN);
      doClick(v);
    }
  }

  public abstract void doClick(View v);
}

Phương pháp này chỉ xử lý các nhấp chuột sau khi nhấp chuột trước đó đã được xử lý và lưu ý rằng nó tránh nhiều nhấp chuột trong một khung.


3

Một bộ Handlerđiều chỉnh dựa trên Signal App .

import android.os.Handler;
import android.support.annotation.NonNull;

/**
 * A class that will throttle the number of runnables executed to be at most once every specified
 * interval.
 *
 * Useful for performing actions in response to rapid user input where you want to take action on
 * the initial input but prevent follow-up spam.
 *
 * This is different from a Debouncer in that it will run the first runnable immediately
 * instead of waiting for input to die down.
 *
 * See http://rxmarbles.com/#throttle
 */
public final class Throttler {

    private static final int WHAT = 8675309;

    private final Handler handler;
    private final long    thresholdMs;

    /**
     * @param thresholdMs Only one runnable will be executed via {@link #publish} every
     *                  {@code thresholdMs} milliseconds.
     */
    public Throttler(long thresholdMs) {
        this.handler     = new Handler();
        this.thresholdMs = thresholdMs;
    }

    public void publish(@NonNull Runnable runnable) {
        if (handler.hasMessages(WHAT)) {
            return;
        }

        runnable.run();
        handler.sendMessageDelayed(handler.obtainMessage(WHAT), thresholdMs);
    }

    public void clear() {
        handler.removeCallbacksAndMessages(null);
    }
}

Ví dụ sử dụng:

throttler.publish(() -> Log.d("TAG", "Example"));

Ví dụ sử dụng trong OnClickListener:

view.setOnClickListener(v -> throttler.publish(() -> Log.d("TAG", "Example")));

Ví dụ về cách sử dụng Kt:

view.setOnClickListener {
    throttler.publish {
        Log.d("TAG", "Example")
    }
}

Hoặc với phần mở rộng:

fun View.setThrottledOnClickListener(throttler: Throttler, function: () -> Unit) {
    throttler.publish(function)
}

Sau đó, sử dụng ví dụ:

view.setThrottledOnClickListener(throttler) {
    Log.d("TAG", "Example")
}

1

Tôi sử dụng lớp này cùng với databinding. Hoạt động tuyệt vời.

/**
 * This class will prevent multiple clicks being dispatched.
 */
class OneClickListener(private val onClickListener: View.OnClickListener) : View.OnClickListener {
    private var lastTime: Long = 0

    override fun onClick(v: View?) {
        val current = System.currentTimeMillis()
        if ((current - lastTime) > 500) {
            onClickListener.onClick(v)
            lastTime = current
        }
    }

    companion object {
        @JvmStatic @BindingAdapter("oneClick")
        fun setOnClickListener(theze: View, f: View.OnClickListener?) {
            when (f) {
                null -> theze.setOnClickListener(null)
                else -> theze.setOnClickListener(OneClickListener(f))
            }
        }
    }
}

Và bố cục của tôi trông như thế này

<TextView
        app:layout_constraintTop_toBottomOf="@id/bla"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:gravity="center"
        android:textSize="18sp"
        app:oneClick="@{viewModel::myHandler}" />

1

Cách quan trọng hơn để xử lý tình huống này là sử dụng toán tử Điều chỉnh (Throttle First) với RxJava2. Các bước để đạt được điều này trong Kotlin:

1). Phụ thuộc: - Thêm phụ thuộc rxjava2 trong tệp cấp ứng dụng build.gradle.

implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.10'

2). Xây dựng một lớp trừu tượng triển khai View.OnClickListener & chứa toán tử đầu tiên điều tiết để xử lý phương thức OnClick () của chế độ xem. Đoạn mã như sau:

import android.util.Log
import android.view.View
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.subjects.PublishSubject
import java.util.concurrent.TimeUnit

abstract class SingleClickListener : View.OnClickListener {
   private val publishSubject: PublishSubject<View> = PublishSubject.create()
   private val THRESHOLD_MILLIS: Long = 600L

   abstract fun onClicked(v: View)

   override fun onClick(p0: View?) {
       if (p0 != null) {
           Log.d("Tag", "Clicked occurred")
           publishSubject.onNext(p0)
       }
   }

   init {
       publishSubject.throttleFirst(THRESHOLD_MILLIS, TimeUnit.MILLISECONDS)
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe { v -> onClicked(v) }
   }
}

3). Triển khai lớp SingleClickListener này khi nhấp vào xem trong hoạt động. Điều này có thể đạt được là:

override fun onCreate(savedInstanceState: Bundle?)  {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)

   val singleClickListener = object : SingleClickListener(){
      override fun onClicked(v: View) {
       // operation on click of xm_view_id
      }
  }
xm_viewl_id.setOnClickListener(singleClickListener)
}

Việc thực hiện các bước trên vào ứng dụng có thể đơn giản tránh được nhiều lần nhấp vào một chế độ xem cho đến 600mS. Chúc bạn viết mã vui vẻ!


1

Bạn có thể sử dụng Rxbinding3 cho mục đích đó. Chỉ cần thêm phụ thuộc này vào build.gradle

build.gradle

implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0'

Sau đó, trong hoạt động hoặc phân đoạn của bạn, hãy sử dụng mã dưới đây

your_button.clicks().throttleFirst(10000, TimeUnit.MILLISECONDS).subscribe {
    // your action
}

0

Việc này được giải quyết như thế này

Observable<Object> tapEventEmitter = _rxBus.toObserverable().share();
Observable<Object> debouncedEventEmitter = tapEventEmitter.debounce(1, TimeUnit.SECONDS);
Observable<List<Object>> debouncedBufferEmitter = tapEventEmitter.buffer(debouncedEventEmitter);

debouncedBufferEmitter.buffer(debouncedEventEmitter)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Action1<List<Object>>() {
      @Override
      public void call(List<Object> taps) {
        _showTapCount(taps.size());
      }
    });

0

Đây là một giải pháp khá đơn giản, có thể được sử dụng với lambdas:

view.setOnClickListener(new DebounceClickListener(v -> this::doSomething));

Đây là đoạn mã đã sẵn sàng sao chép / dán:

public class DebounceClickListener implements View.OnClickListener {

    private static final long DEBOUNCE_INTERVAL_DEFAULT = 500;
    private long debounceInterval;
    private long lastClickTime;
    private View.OnClickListener clickListener;

    public DebounceClickListener(final View.OnClickListener clickListener) {
        this(clickListener, DEBOUNCE_INTERVAL_DEFAULT);
    }

    public DebounceClickListener(final View.OnClickListener clickListener, final long debounceInterval) {
        this.clickListener = clickListener;
        this.debounceInterval = debounceInterval;
    }

    @Override
    public void onClick(final View v) {
        if ((SystemClock.elapsedRealtime() - lastClickTime) < debounceInterval) {
            return;
        }
        lastClickTime = SystemClock.elapsedRealtime();
        clickListener.onClick(v);
    }
}

Thưởng thức!


0

Dựa trên câu trả lời của @GreyBeardGeek ,

  • Tạo debounceClick_last_Timestamptrên ids.xmlđể gắn thẻ dấu thời gian nhấp chuột trước đó.
  • Thêm khối mã này vào BaseActivity

    protected void debounceClick(View clickedView, DebouncedClick callback){
        debounceClick(clickedView,1000,callback);
    }
    
    protected void debounceClick(View clickedView,long minimumInterval, DebouncedClick callback){
        Long previousClickTimestamp = (Long) clickedView.getTag(R.id.debounceClick_last_Timestamp);
        long currentTimestamp = SystemClock.uptimeMillis();
        clickedView.setTag(R.id.debounceClick_last_Timestamp, currentTimestamp);
        if(previousClickTimestamp == null 
              || Math.abs(currentTimestamp - previousClickTimestamp) > minimumInterval) {
            callback.onClick(clickedView);
        }
    }
    
    public interface DebouncedClick{
        void onClick(View view);
    }
    
  • Sử dụng:

    view.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            debounceClick(v, 3000, new DebouncedClick() {
                @Override
                public void onClick(View view) {
                    doStuff(view); // Put your's click logic on doStuff function
                }
            });
        }
    });
    
  • Sử dụng lambda

    view.setOnClickListener(v -> debounceClick(v, 3000, this::doStuff));
    

0

Đặt một ví dụ nhỏ ở đây

view.safeClick { doSomething() }

@SuppressLint("CheckResult")
fun View.safeClick(invoke: () -> Unit) {
    RxView
        .clicks(this)
        .throttleFirst(300, TimeUnit.MILLISECONDS)
        .subscribe { invoke() }
}

1
vui lòng cung cấp liên kết hoặc mô tả RxView của bạn là gì
BekaBot

0

Giải pháp của tôi, cần gọi removeallkhi chúng ta thoát (tiêu diệt) khỏi phân mảnh và hoạt động:

import android.os.Handler
import android.os.Looper
import java.util.concurrent.TimeUnit

    //single click handler
    object ClickHandler {

        //used to post messages and runnable objects
        private val mHandler = Handler(Looper.getMainLooper())

        //default delay is 250 millis
        @Synchronized
        fun handle(runnable: Runnable, delay: Long = TimeUnit.MILLISECONDS.toMillis(250)) {
            removeAll()//remove all before placing event so that only one event will execute at a time
            mHandler.postDelayed(runnable, delay)
        }

        @Synchronized
        fun removeAll() {
            mHandler.removeCallbacksAndMessages(null)
        }
    }

0

Tôi có thể nói rằng cách dễ nhất là sử dụng một thư viện "đang tải" như KProgressHUD.

https://github.com/Kaopiz/KProgressHUD

Điều đầu tiên ở phương thức onClick là gọi hoạt ảnh đang tải, nó sẽ chặn ngay lập tức tất cả giao diện người dùng cho đến khi nhà phát triển quyết định giải phóng nó.

Vì vậy, bạn sẽ có cái này cho hành động onClick (cái này sử dụng Butterknife nhưng rõ ràng nó hoạt động với bất kỳ loại phương pháp nào):

Ngoài ra, đừng quên tắt nút sau khi nhấp.

@OnClick(R.id.button)
void didClickOnButton() {
    startHUDSpinner();
    button.setEnabled(false);
    doAction();
}

Sau đó:

public void startHUDSpinner() {
    stopHUDSpinner();
    currentHUDSpinner = KProgressHUD.create(this)
            .setStyle(KProgressHUD.Style.SPIN_INDETERMINATE)
            .setLabel(getString(R.string.loading_message_text))
            .setCancellable(false)
            .setAnimationSpeed(3)
            .setDimAmount(0.5f)
            .show();
}

public void stopHUDSpinner() {
    if (currentHUDSpinner != null && currentHUDSpinner.isShowing()) {
        currentHUDSpinner.dismiss();
    }
    currentHUDSpinner = null;
}

Và bạn có thể sử dụng phương thức stopHUDSpinner trong phương thức doAction () nếu bạn muốn:

private void doAction(){ 
   // some action
   stopHUDSpinner()
}

Bật lại nút theo logic ứng dụng của bạn: button.setEnabled (true);

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.