Implementing a Non-CopyPaste EditText control with Xamarin.Android

Recently, I am using Xamarin to develop a mobile app for Android. In the app, there is a page that asks the user to input phone numbers twice to make sure the number has been typed correctly. The user is not allowed to copy/paste. So I need to implement a custom control that can disable copy/paste features.

Even we can use C# to develop Android with Xamarin, we have to gain enough native development skills to complete this goal. I also understand for a new Xamarin developer, it would be tricky to find a solution related to native functionalities. So I want to show you how I use some tools (Google and StackOverflow, etc) to find the answer then make it. Here is a simple note to record what I have done for this control and hope it would be useful for you.

Creating a custom control

The first step obviously is to create a new custom control that inherits from the default EditText control. So I created a new class called NonCopyPasteEditText in the UserControls folder. It is just a simple class as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace MyApp.Droid.UserControls
{
public sealed class NonCopyPasteEditText : EditText
{
public NonCopyPasteEditText(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
}

public NonCopyPasteEditText(Context context) : base(context)
{
}

public NonCopyPasteEditText(Context context, IAttributeSet attrs) : base(context, attrs)
{
}
}
}

Disabling the selection action

The next step is to do something that can prevent copy/paste. But how? So I google android disable copy paste then click the first link How to disable copy/paste from/to EditText - Stack Overflow. That is great. The accepted answer looks like what I need. It shows we need to use setCustomSelectionActionModeCallback method to prevent the action mode for actions (Select All, Cut, Copy and Paste actions). If we disable the selection context menu, the user cannot copy/paste. Then please do not forget to vote the answer because it is helpful. ๐Ÿ‘

Ok. So let us figure out what it is. So I google setCustomSelectionActionModeCallback then I see the Android documentation here: https://developer.android.com/reference/android/widget/TextView#setCustomSelectionActionModeCallback(android.view.ActionMode.Callback).

Returning false from ActionMode.Callback.onCreateActionMode(ActionMode, android.view.Menu) will prevent the action mode from being started.

It is what we want. We can follow the same pattern for our Xamarin app. Because Xamarin runtime is a wrapper for the native APIs so we can find the corresponding APIs in Xamarin. But please keep in mind that they may have different naming styles.

I found the link from Xamarin.Android docs: TextView.CustomSelectionActionModeCallback Property. Unfortunately, it does not provide much information we need. But it supports get and set so we can just assign a new value.

To create a new CustomSelectionActionModeCallback, we need to create a class that inherits the interface ActionMode.Callback. But I could not find this interface in Xamarin. Instead, I found a class called ActionMode.Callback2. Actually it is an abstract class that implements this interface. FYI: ActionMode.Callback2.

Let us create a new callback as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class NonCopyPasteCustomSelectionActionModeCallback : ActionMode.Callback2
{
public override bool OnActionItemClicked(ActionMode mode, IMenuItem item)
{
return false;
}

public override bool OnCreateActionMode(ActionMode mode, IMenu menu)
{
return false;
}

public override void OnDestroyActionMode(ActionMode mode)
{
}

public override bool OnPrepareActionMode(ActionMode mode, IMenu menu)
{
return false;
}
}

Next we can assign this callback to our custom control:

1
2
3
4
public NonCopyPasteEditText(Context context, IAttributeSet attrs) : base(context, attrs)
{
CustomSelectionActionModeCallback = new NonCopyPasteCustomSelectionActionModeCallback();
}

Cool! It works! When the user long-presses the text in the EditText, no context menu appears. So the task is done?

Unfortunately, No.

Handling the Android IME

We forgot a thing that is most of Android IMEs provide the function to edit the text, including Select All, Select, Copy and Paste, etc. The user can use the IME to do copy/paste, as shown below:

Text Edit and Clipboard Manager on Samsung phones

So even we can disable the long-press action for the EditText, the user can still use IME to copy/paste.

My next idea is to find a way to clear the clipboard. Although it is hard to disable copy of IME, we can clear the clipboard when the user clicks the EditText for pasting.

I google android clear clipboard programmatically then I found Android has a ClipBoardManager to manage the clipboard. I also find the corresponding class in Xamarin.Android. FYI: ClipboardManager Class

So we can use this class to clear the clipboard when the user clicks the next EditText - then there is nothing to be pasted!

I felt very happy and wrote the below code quickly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public NonCopyPasteEditText(Context context, IAttributeSet attrs) : base(context, attrs)
{
CustomSelectionActionModeCallback = new NonCopyPasteCustomSelectionActionModeCallback();
FocusChange += NonCopyPasteEditText_FocusChange;
}

private void NonCopyPasteEditText_FocusChange(object sender, FocusChangeEventArgs e)
{
if (e.HasFocus)
{
ClipboardManager clipboard = (ClipboardManager)Application.Context.GetSystemService(Context.ClipboardService);
clipboard.ClearPrimaryClip();
}
}

It looks good! And it does work on my Huawei phone (Android 10)! So the task is done?

Unfortunately, No, either.

Handling the Samsung private APIs

Android is open-source platform so many manufactures can modify the original Android code to improve the user experience. (or making it worse, who knows!) For example, you may find the system setting menus of phones from different manufactures are not the same even they have the same Android version. So the challenge is not only to handle the Android APIs for various SDK versions, but also to deal with the various private APIs added by manufactures.

For example, Samsung phones have a clipboard manager. It can store clipboard history and you can manage them in the IME. Unfortunately, the ClipboardManager in the above section does not work for Samsung phones. When I call clipboard.ClearPrimaryClip() method, nothing happens. The copied text is still there.

Let us change to another approach. The goal is to disable copy/paste - more specifically, we hope the user cannot copy the text in the first EditText then paste it to the second EditText. In other words, if the user can copy the text, we should make sure the user cannot paste it. So we can detect the userโ€™s intent by checking the text-changed event. If the changed text is more than one letter, we would know the user wants to paste - then we can do something.

Here is the updated code to add TextChanged event handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public NonCopyPasteEditText(Context context, IAttributeSet attrs) : base(context, attrs)
{
CustomSelectionActionModeCallback = new NonCopyPasteCustomSelectionActionModeCallback();
FocusChange += NonCopyPasteEditText_FocusChange;
TextChanged += NonCopyPasteEditText_TextChanged;
}

private void NonCopyPasteEditText_TextChanged(object sender, Android.Text.TextChangedEventArgs e)
{
if (e.AfterCount - e.BeforeCount > 1)
{
Text = $"{Text.Substring(0, e.Start)}{Text.Substring(e.Start + e.AfterCount)}";
}
}

When the user clicks the second EditText to paste, we will check the changed text. If the text is longer than one letter, we will restore the previous text. We do not need to worry about one-letter scenario because if we do not have this condition the user cannot type anything to the EditText. Also, I do not think anyone would copy/paste one letter one time, 8-)

Clean up

It looks working so far. But we still have an issue actually. Because we need to set the initial value when loading this page for the existing phone number of the first EditText. It will also invoke the TextChanged event when we set its value so we will get an empty EditText. I need to use a flag to indicate if the control has been initialized:

1
2
3
4
5
6
7
8
9
10
11
12
private bool _isLoaded = false;
private void NonCopyPasteEditText_TextChanged(object sender, Android.Text.TextChangedEventArgs e)
{
if (!_isLoaded)
{
_isLoaded = true;
}
else if (e.AfterCount - e.BeforeCount > 1)
{
Text = $"{Text.Substring(0, e.Start)}{Text.Substring(e.Start + e.AfterCount)}";
}
}

Now this feature will not impact the data-binding to set the initial value.

Summary

This article is pretty trivial, and I just hope to show how we start to solve a problem we may not be familiar with. Xamarin provides us with fully wrapped implementations for native APIs but we still need to dive deep into that. Hope this article would be helpful for your Xamarin development. Thanks.