Comment désactiver le copier / coller de / vers EditText


131

Dans mon application, il y a un écran d'inscription, où je ne veux pas que l'utilisateur puisse copier / coller du texte dans le EditTextchamp. J'ai défini un onLongClickListenersur chacun EditTextpour que le menu contextuel montrant la méthode de copier / coller / d'entrée et d'autres options ne s'affiche pas. Ainsi, l'utilisateur ne pourra pas copier / coller dans les champs d'édition.

 OnLongClickListener mOnLongClickListener = new OnLongClickListener() {

        @Override
        public boolean onLongClick(View v) {
            // prevent context menu from being popped up, so that user
            // cannot copy/paste from/into any EditText fields.
            return true;
        }
    };

Mais le problème se pose si l'utilisateur a activé un clavier tiers autre que le clavier par défaut d'Android, qui peut avoir un bouton à copier / coller ou qui peut afficher le même menu contextuel. Alors, comment désactiver le copier / coller dans ce scénario?

Veuillez me faire savoir s'il existe d'autres moyens de copier / coller également. (et éventuellement comment les désactiver)

Toute aide serait appréciée.


Si l'opération "coller" provient d'un IME, vous n'avez aucun moyen standard de la distinguer des frappes normales. Une idée à essayer est de mesurer le temps entre l'arrivée de chaque personnage et si le temps est trop court, alors les caractères proviennent d'une opération de "coller".
BitBank

semble être une sale solution! ça vaut le coup d'œil.
rDroid

1
utiliser android: longClickable = "false"
Azay Gupta

Réponses:


112

Si vous utilisez le niveau d'API 11 ou supérieur, vous pouvez empêcher les menus contextuels de copier, coller, couper et personnalisés d'apparaître par.

edittext.setCustomSelectionActionModeCallback(new ActionMode.Callback() {

            public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
                return false;
            }

            public void onDestroyActionMode(ActionMode mode) {                  
            }

            public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                return false;
            }

            public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
                return false;
            }
        });

Renvoyer false depuis onCreateActionMode (ActionMode, Menu) empêchera le démarrage du mode action (actions Sélectionner tout, Couper, Copier et Coller).


1
qu'en est-il du niveau d'API inférieur à 13?
Jonathan

1
Je ne comprends pas les commentaires, cet exemple fonctionne api11 +, pré-api11 il n'y avait pas de copier-coller IIRC
Scottyab

28
Ne fonctionne pas pour moi.Le bouton Coller apparaîtra si vous appuyez sur l'indicateur de curseur bleu.
annulé

8
Ne fonctionne pas non plus pour moi. En appuyant deux fois, le menu de copier-coller s'affiche.
Android Killer

cela ne fonctionne plus sur Android 6.0, vérifiez cette réponse stackoverflow.com/questions/27869983/…
has19

132

La meilleure méthode consiste à utiliser:

etUsername.setLongClickable(false);

58
Ou, juste en xml android:longClickable="false":)
lomza

19
Le bouton Coller apparaîtra si vous appuyez sur l'indicateur de curseur bleu.
annulé

16
Cela empêchera certainement la vue d'être longtemps cliquable, mais les contrôles d'édition peuvent également être demandés en double tapant sur le texte, ce qui signifie que cette solution n'est pas complète. Gardez cela à l'esprit pour vos besoins.
Kevin Grant

1
En outre, les raccourcis clavier peuvent toujours fonctionner (Ctrl + C) avec des claviers externes.
Oleg Vaskevich

Cela ne fonctionne pas sur Ice Cream Sandwich car les options du presse-papiers peuvent être ouvertes en tapant deux fois sur le texte, ainsi qu'en appuyant longuement.
Paul Wintz

44

Vous pouvez le faire en désactivant la pression longue sur EditText

Pour l'implémenter, ajoutez simplement la ligne suivante dans le xml -

android:longClickable="false"

6
Le problème était que l'utilisateur de mon application dispose d'un clavier tiers doté d'un bouton copier-coller.
rDroid

3
un autre problème est que vous pouvez sélectionner le texte en appuyant deux fois et il affiche à nouveau copier / coller
Nikola

37

Je peux désactiver la fonctionnalité copier-coller avec les éléments suivants:

textField.setCustomSelectionActionModeCallback(new ActionMode.Callback() {

    public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
        return false;
    }

    public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
        return false;
    }

    public boolean onActionItemClicked(ActionMode actionMode, MenuItem item) {
        return false;
    }

    public void onDestroyActionMode(ActionMode actionMode) {
    }
});

textField.setLongClickable(false);
textField.setTextIsSelectable(false);

J'espère que ça marche pour toi ;-)


C'est exactement la même solution que j'ai trouvée sur la base des autres réponses ci-dessus. Cela devrait être marqué comme la solution correcte car il gère les cas
extrêmes que

2
Cette option bloque la copie mais vous pouvez toujours coller en cliquant sur le curseur.
Mehul Kanzariya

12

voici un meilleur moyen de désactiver le copier-coller du travail d'édition de texte dans toutes les versions

if (android.os.Build.VERSION.SDK_INT < 11) {
        editText.setOnCreateContextMenuListener(new OnCreateContextMenuListener() {

            @Override
            public void onCreateContextMenu(ContextMenu menu, View v,
                    ContextMenuInfo menuInfo) {
                // TODO Auto-generated method stub
                menu.clear();
            }
        });
    } else {
        editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {

            public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
                // TODO Auto-generated method stub
                return false;
            }

            public void onDestroyActionMode(ActionMode mode) {
                // TODO Auto-generated method stub

            }

            public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                // TODO Auto-generated method stub
                return false;
            }

            public boolean onActionItemClicked(ActionMode mode,
                    MenuItem item) {
                // TODO Auto-generated method stub
                return false;
            }
        });
    }

Cela a fonctionné pour moi, je devais juste ajouter @TargetApi (Build.VERSION_CODES.HONEYCOMB)
Sheepdogsheep

11

En plus de setCustomSelectionActionModeCallback et des solutions de clic long désactivées , il est nécessaire d' empêcher les menus PASTE / REPLACE d'apparaître lorsque vous cliquez sur la poignée de sélection de texte, comme illustré ci-dessous:

Poignée de sélection de texte avec menu Coller

La solution consiste à empêcher le menu PASTE / REPLACE d'apparaître dans la show()méthode de la android.widget.Editorclasse (non documentée) . Avant que le menu n'apparaisse, une vérification est effectuée if (!canPaste && !canSuggest) return;. Les deux méthodes utilisées comme base pour définir ces variables sont toutes deux dans la EditTextclasse:

Une réponse plus complète est disponible ici .


Ceci est la solution CORRECT et COMPLETE
FireZenk

Dans certains appareils, au lieu de l'option Coller le Presse-papiers, elle est visible, agit uniquement comme coller. J'ai vérifié les liens mais je suis en mesure d'empêcher le collage mais pas le presse-papiers. une idée ?
Richa le

11

Solution Kotlin:

fun TextView.disableCopyPaste() {
    isLongClickable = false
    setTextIsSelectable(false)
    customSelectionActionModeCallback = object : ActionMode.Callback {
        override fun onCreateActionMode(mode: ActionMode?, menu: Menu): Boolean {
            return false
        }

        override fun onPrepareActionMode(mode: ActionMode?, menu: Menu): Boolean {
            return false
        }

        override fun onActionItemClicked(mode: ActionMode?, item: MenuItem): Boolean {
            return false
        }

        override fun onDestroyActionMode(mode: ActionMode?) {}
    }
}

Ensuite, vous pouvez simplement appeler cette méthode sur votre TextView:

override fun onCreate() {
    priceEditText.disableCopyPaste()
}

1
Salut, j'utilise cette approche, mais j'obtiens une Type mismatcherreur avec cette description Required:ActionMode.Callback! Found: sur cette partie object: ActionMode.Callback. Une idée pourquoi cela ne fonctionne pas?
Abdul Mateen

8

En utilisant d'autres solutions, l'API 26 (Oreo) affichait toujours la poignée du curseur en appuyant une seule fois sur le texte saisi, puis le menu pouvait être affiché. Seule une combinaison de solutions peut résoudre mon problème.

public class CustomEditText extends EditText {

    public CustomEditText(Context context) {
        super(context);
        init();
    }

    public CustomEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CustomEditText(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        this.setCustomSelectionActionModeCallback(new BlockedActionModeCallback());
        this.setLongClickable(false);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            this.setInsertionDisabled();
        }
        return super.onTouchEvent(event);
    }

    /**
    * This method sets TextView#Editor#mInsertionControllerEnabled field to false
    * to return false from the Editor#hasInsertionController() method to PREVENT showing
    * of the insertionController from EditText
    * The Editor#hasInsertionController() method is called in  Editor#onTouchUpEvent(MotionEvent event) method.
    */
    private void setInsertionDisabled() {
        try {
            Field editorField = TextView.class.getDeclaredField("mEditor");
            editorField.setAccessible(true);
            Object editorObject = editorField.get(this);

            Class editorClass = Class.forName("android.widget.Editor");
            Field mInsertionControllerEnabledField = editorClass.getDeclaredField("mInsertionControllerEnabled");
            mInsertionControllerEnabledField.setAccessible(true);
            mInsertionControllerEnabledField.set(editorObject, false);
        }
        catch (Exception ignored) {
            // ignore exception here
        }
    }

    @Override
    public boolean isSuggestionsEnabled() {
        return false;
    }

    private class BlockedActionModeCallback implements ActionMode.Callback {
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            return false;
        }

        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false;
        }

        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            return false;
        }

        public void onDestroyActionMode(ActionMode mode) {
        }
    }
}

5

Si vous ne souhaitez pas désactiver le clic long car vous devez exécuter certaines fonctionnalités sur un clic long, renvoyer true est une meilleure option pour le faire.

Votre clic long edittext sera comme ceci.

edittext.setOnLongClickListener(new View.OnLongClickListener() {
      @Override
      public boolean onLongClick(View v) {
            //  Do Something or Don't
            return true;
      }
});

Selon la documentation, retourner "True" indiquera qu'un clic long a été géré, donc pas besoin d'effectuer des opérations par défaut.

J'ai testé cela sur les niveaux API 16, 22 et 25. Cela fonctionne très bien pour moi. J'espère que cela aidera.


1
Bon. Alternativement, juste mis android:longClickable="false"en XML
Alex Semeniuk


3

Voici un hack pour désactiver le popup "coller". Vous devez remplacer la EditTextméthode:

@Override
public int getSelectionStart() {
    for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
        if (element.getMethodName().equals("canPaste")) {
            return -1;
        }
    }
    return super.getSelectionStart();
}

La même chose peut être faite pour les autres actions.


pouvez-vous dire pour la désactivation du Presse-papiers
Richa

3

J'ai testé cette solution et cela fonctionne

    mSubdomainEditText.setLongClickable(false);
    mSubdomainEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {

      public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
        return false;
      }

      public void onDestroyActionMode(ActionMode mode) {
      }

      public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        return false;
      }

      public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        return false;
      }
    });

1

Lisez le Presse-papiers, vérifiez par rapport à l'entrée et l'heure à laquelle l'entrée est "tapée". Si le Presse-papiers contient le même texte et qu'il est trop rapide, supprimez l'entrée collée.


1

@Zain Ali, votre réponse fonctionne sur l'API 11. Je voulais juste suggérer un moyen de le faire également sur l'API 10. Comme je devais maintenir mon API de projet sur cette version, je jouais constamment avec les fonctions disponibles dans la 2.3.3 et j'avais la possibilité de le faire. J'ai partagé l'extrait ci-dessous. J'ai testé le code et cela fonctionnait pour moi. J'ai fait cet extrait de code en urgence. N'hésitez pas à améliorer le code s'il y a des changements qui peuvent être apportés.

// A custom TouchListener is being implemented which will clear out the focus 
// and gain the focus for the EditText, in few milliseconds so the selection 
// will be cleared and hence the copy paste option wil not pop up.
// the respective EditText should be set with this listener 
// tmpEditText.setOnTouchListener(new MyTouchListener(tmpEditText, tmpImm));

public class MyTouchListener implements View.OnTouchListener {

    long click = 0;
    EditText mEtView;
    InputMethodManager imm;

    public MyTouchListener(EditText etView, InputMethodManager im) {
        mEtView = etView;
        imm = im;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {

        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            long curr = System.currentTimeMillis();
            if (click !=0 && ( curr - click) < 30) {

                mEtView.setSelected(false);
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mEtView.setSelected(true);
                        mEtView.requestFocusFromTouch();
                        imm.showSoftInput(mEtView, InputMethodManager.RESULT_SHOWN);
                    }
                },25);

            return true;
            }
            else {
                if (click == 0)
                    click = curr;
                else
                    click = 0;
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mEtView.requestFocusFromTouch();
                        mEtView.requestFocusFromTouch();
                        imm.showSoftInput(mEtView, InputMethodManager.RESULT_SHOWN);
                    }
                },25);
            return true;
            }

        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
            mEtView.setSelected(false);
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    mEtView.setSelected(true);
                    mEtView.requestFocusFromTouch();
                    mEtView.requestFocusFromTouch();
                    imm.showSoftInput(mEtView, InputMethodManager.RESULT_SHOWN);
                }
            },25);
            return true;
        }
        return false;
    }

1

La solution est très simple

public class MainActivity extends AppCompatActivity {

EditText et_0;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    et_0 = findViewById(R.id.et_0);

    et_0.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            //to keep the text selection capability available ( selection cursor)
            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            //to prevent the menu from appearing
            menu.clear();
            return false;
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            return false;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {

        }
    });
   }
}

--------> aperçu <---------


1

Essayez de suivre la classe personnalisée pour le copier-coller existant Edittext

public class SegoeUiEditText extends AppCompatEditText {
private final Context context;


@Override
public boolean isSuggestionsEnabled() {
    return false;
}
public SegoeUiEditText(Context context) {
    super(context);
    this.context = context;
    init();
}

public SegoeUiEditText(Context context, AttributeSet attrs) {
    super(context, attrs);
    this.context = context;
    init();
}

public SegoeUiEditText(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    this.context = context;
    init();
}


private void setFonts(Context context) {
    this.setTypeface(Typeface.createFromAsset(context.getAssets(), "Fonts/Helvetica-Normal.ttf"));
}

private void init() {

        setTextIsSelectable(false);
        this.setCustomSelectionActionModeCallback(new ActionModeCallbackInterceptor());
        this.setLongClickable(false);

}
@Override
public int getSelectionStart() {

    for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
        if (element.getMethodName().equals("canPaste")) {
            return -1;
        }
    }
    return super.getSelectionStart();
}
/**
 * Prevents the action bar (top horizontal bar with cut, copy, paste, etc.) from appearing
 * by intercepting the callback that would cause it to be created, and returning false.
 */
private class ActionModeCallbackInterceptor implements ActionMode.Callback, android.view.ActionMode.Callback {
    private final String TAG = SegoeUiEditText.class.getSimpleName();

    public boolean onCreateActionMode(ActionMode mode, Menu menu) { return false; }
    public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; }
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) { return false; }
    public void onDestroyActionMode(ActionMode mode) {}

    @Override
    public boolean onCreateActionMode(android.view.ActionMode mode, Menu menu) {
        return false;
    }

    @Override
    public boolean onPrepareActionMode(android.view.ActionMode mode, Menu menu) {
        menu.clear();
        return false;
    }

    @Override
    public boolean onActionItemClicked(android.view.ActionMode mode, MenuItem item) {
        return false;
    }

    @Override
    public void onDestroyActionMode(android.view.ActionMode mode) {

    }
}

}


1

Pour un smartphone avec presse-papiers, il est possible d'éviter comme ça.

editText.setFilters(new InputFilter[]{new InputFilter() {
        @Override
        public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
            if (source.length() > 1) {
                return "";
            }  return null;
        }
    }});


0

J'ai trouvé que lorsque vous créez un filtre d'entrée pour éviter l'entrée de caractères indésirables, coller ces caractères dans le texte d'édition n'a aucun effet. Donc, cela résout aussi mon problème.



0

La solution qui a fonctionné pour moi a été de créer un Edittext personnalisé et de remplacer la méthode suivante:

public class MyEditText extends EditText {

private int mPreviousCursorPosition;

@Override
protected void onSelectionChanged(int selStart, int selEnd) {
    CharSequence text = getText();
    if (text != null) {
        if (selStart != selEnd) {
            setSelection(mPreviousCursorPosition, mPreviousCursorPosition);
            return;
        }
    }
    mPreviousCursorPosition = selStart;
    super.onSelectionChanged(selStart, selEnd);
}

}


0

Essayez d'utiliser.

myEditext.setCursorVisible(false);

       myEditext.setCustomSelectionActionModeCallback(new ActionMode.Callback() {

        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            // TODO Auto-generated method stub
            return false;
        }

        public void onDestroyActionMode(ActionMode mode) {
            // TODO Auto-generated method stub

        }

        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            // TODO Auto-generated method stub
            return false;
        }

        public boolean onActionItemClicked(ActionMode mode,
                MenuItem item) {
            // TODO Auto-generated method stub
            return false;
        }
    });

0

Qui cherche une solution dans Kotlin utilise la classe ci-dessous comme widget personnalisé et l'utilise dans le xml.

classe SecureEditText: TextInputEditText {

/** This is a replacement method for the base TextView class' method of the same name. This method
 * is used in hidden class android.widget.Editor to determine whether the PASTE/REPLACE popup
 * appears when triggered from the text insertion handle. Returning false forces this window
 * to never appear.
 * @return false
 */
override fun isSuggestionsEnabled(): Boolean {
    return false
}

override fun getSelectionStart(): Int {
    for (element in Thread.currentThread().stackTrace) {
        if (element.methodName == "canPaste") {
            return -1
        }
    }
    return super.getSelectionStart()
}

public override fun onSelectionChanged(start: Int, end: Int) {

    val text = text
    if (text != null) {
        if (start != text.length || end != text.length) {
            setSelection(text.length, text.length)
            return
        }
    }

    super.onSelectionChanged(start, end)
}

companion object {
    private val EDITTEXT_ATTRIBUTE_COPY_AND_PASTE = "isCopyPasteDisabled"
    private val PACKAGE_NAME = "http://schemas.android.com/apk/res-auto"
}

constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
    disableCopyAndPaste(context, attrs)
}

/**
 * Disable Copy and Paste functionality on EditText
 *
 * @param context Context object
 * @param attrs   AttributeSet Object
 */
private fun disableCopyAndPaste(context: Context, attrs: AttributeSet) {
    val isDisableCopyAndPaste = attrs.getAttributeBooleanValue(
        PACKAGE_NAME,
        EDITTEXT_ATTRIBUTE_COPY_AND_PASTE, true
    )
    if (isDisableCopyAndPaste && !isInEditMode()) {
        val inputMethodManager =
            context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        this.setLongClickable(false)
        this.setOnTouchListener(BlockContextMenuTouchListener(inputMethodManager))
    }
}

/**
 * Perform Focus Enabling Task to the widget with the help of handler object
 * with some delay
 * @param inputMethodManager is used to show the key board
 */
private fun performHandlerAction(inputMethodManager: InputMethodManager) {
    val postDelayedIntervalTime: Long = 25
    Handler().postDelayed(Runnable {
        this@SecureEditText.setSelected(true)
        this@SecureEditText.requestFocusFromTouch()
        inputMethodManager.showSoftInput(
            this@SecureEditText,
            InputMethodManager.RESULT_SHOWN
        )
    }, postDelayedIntervalTime)
}

/**
 * Class to Block Context Menu on double Tap
 * A custom TouchListener is being implemented which will clear out the focus
 * and gain the focus for the EditText, in few milliseconds so the selection
 * will be cleared and hence the copy paste option wil not pop up.
 * the respective EditText should be set with this listener
 *
 * @param inputMethodManager is used to show the key board
 */
private inner class BlockContextMenuTouchListener internal constructor(private val inputMethodManager: InputMethodManager) :
    View.OnTouchListener {
    private var lastTapTime: Long = 0
    val TIME_INTERVAL_BETWEEN_DOUBLE_TAP = 30
    override fun onTouch(v: View, event: MotionEvent): Boolean {
        if (event.getAction() === MotionEvent.ACTION_DOWN) {
            val currentTapTime = System.currentTimeMillis()
            if (lastTapTime != 0L && currentTapTime - lastTapTime < TIME_INTERVAL_BETWEEN_DOUBLE_TAP) {
                this@SecureEditText.setSelected(false)
                performHandlerAction(inputMethodManager)
                return true
            } else {
                if (lastTapTime == 0L) {
                    lastTapTime = currentTapTime
                } else {
                    lastTapTime = 0
                }
                performHandlerAction(inputMethodManager)
                return true
            }
        } else if (event.getAction() === MotionEvent.ACTION_MOVE) {
            this@SecureEditText.setSelected(false)
            performHandlerAction(inputMethodManager)
        }
        return false
    }
}

}


0

J'ai ajouté une fonction d'extension en langue Kotlin :

fun EditText.disableTextSelection() {
    this.setCustomSelectionActionModeCallback(object : android.view.ActionMode.Callback {
        override fun onActionItemClicked(mode: android.view.ActionMode?, item: MenuItem?): Boolean {
            return false
        }
        override fun onCreateActionMode(mode: android.view.ActionMode?, menu: Menu?): Boolean {
            return false
        }
        override fun onPrepareActionMode(mode: android.view.ActionMode?, menu: Menu?): Boolean {
            return false
        }
        override fun onDestroyActionMode(mode: android.view.ActionMode?) {
        }
    })
}

vous pouvez l'utiliser comme ceci:

edit_text.disableTextSelection()

également ajouté ci-dessous dans votre xml:

                android:longClickable="false"
                android:textIsSelectable="false"
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.