Android EditText automatic MAC address formatting

I recently made a simple Android Wake-on-Lan app which includes an EditText MAC address input field. To make it easier for users to type in proper MAC’s (not having to type colons manually) I used TextWatcher to automatically format the input on text-changed event. This stackoverflow topic gave me a general idea on how to do it. What I wanted is for the application to insert a colon after every second character, remove the character preceding a colon if the user deletes the colon and to handle MAC address editing. For example if the user takes out a few characters from the middle of the string, the formatting should still be preserved and the cursor should stay in the right place and not go to the end or beginning of the string.

The only callback I used was onTextChanged(). On each text-change event, users input gets stripped of all non-MAC characters (this means only numbers 0-9 and letters A-F are left), then the colons are added and after that (potential) character deletion and cursor positioning are handled. Here is the code I came up with in the end:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
private EditText mMacEdit = (EditText)findViewById(R.id.edit_mac);

/**
 * Registers TextWatcher for MAC EditText field. Automatically adds colons,  
 * switches the MAC to upper case and handles the cursor position.
 */

private void registerAfterMacTextChangedCallback() {
    mMacEdit.addTextChangedListener(new TextWatcher() {
        String mPreviousMac = null;

        /* (non-Javadoc)
         * Does nothing.
         * @see android.text.TextWatcher#afterTextChanged(android.text.Editable)
         */

        @Override
        public void afterTextChanged(Editable arg0) {
        }

        /* (non-Javadoc)
         * Does nothing.
         * @see android.text.TextWatcher#beforeTextChanged(java.lang.CharSequence, int, int, int)
         */

        @Override
        public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
        }

        /* (non-Javadoc)
         * Formats the MAC address and handles the cursor position.
         * @see android.text.TextWatcher#onTextChanged(java.lang.CharSequence, int, int, int)
         */

        @SuppressLint("DefaultLocale")
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            String enteredMac = mMacEdit.getText().toString().toUpperCase();
            String cleanMac = clearNonMacCharacters(enteredMac);
            String formattedMac = formatMacAddress(cleanMac);

            int selectionStart = mMacEdit.getSelectionStart();
            formattedMac = handleColonDeletion(enteredMac, formattedMac, selectionStart);
            int lengthDiff = formattedMac.length() - enteredMac.length();

            setMacEdit(cleanMac, formattedMac, selectionStart, lengthDiff);
        }

        /**
         * Strips all characters from a string except A-F and 0-9.
         * @param mac       User input string.
         * @return          String containing MAC-allowed characters.
         */

        private String clearNonMacCharacters(String mac) {
            return mac.toString().replaceAll("[^A-Fa-f0-9]", "");
        }

        /**
         * Adds a colon character to an unformatted MAC address after
         * every second character (strips full MAC trailing colon)
         * @param cleanMac      Unformatted MAC address.
         * @return              Properly formatted MAC address.
         */

        private String formatMacAddress(String cleanMac) {
            int grouppedCharacters = 0;
            String formattedMac = "";

            for (int i = 0; i < cleanMac.length(); ++i) {
                formattedMac += cleanMac.charAt(i);
                ++grouppedCharacters;

                if (grouppedCharacters == 2) {
                    formattedMac += ":";
                    grouppedCharacters = 0;
                }
            }

            // Removes trailing colon for complete MAC address
            if (cleanMac.length() == 12)
                formattedMac = formattedMac.substring(0, formattedMac.length() - 1);

            return formattedMac;
        }

        /**
         * Upon users colon deletion, deletes MAC character preceding deleted colon as well.
         * @param enteredMac            User input MAC.
         * @param formattedMac          Formatted MAC address.
         * @param selectionStart        MAC EditText field cursor position.
         * @return                      Formatted MAC address.
         */

        private String handleColonDeletion(String enteredMac, String formattedMac, int selectionStart) {
            if (mPreviousMac != null && mPreviousMac.length() > 1) {
                int previousColonCount = colonCount(mPreviousMac);
                int currentColonCount = colonCount(enteredMac);

                if (currentColonCount < previousColonCount) {
                    formattedMac = formattedMac.substring(0, selectionStart - 1) + formattedMac.substring(selectionStart);
                    String cleanMac = clearNonMacCharacters(formattedMac);
                    formattedMac = formatMacAddress(cleanMac);
                }
            }
            return formattedMac;
        }

        /**
         * Gets MAC address current colon count.
         * @param formattedMac      Formatted MAC address.
         * @return                  Current number of colons in MAC address.
         */

        private int colonCount(String formattedMac) {
            return formattedMac.replaceAll("[^:]", "").length();
        }

        /**
         * Removes TextChange listener, sets MAC EditText field value,
         * sets new cursor position and re-initiates the listener.
         * @param cleanMac          Clean MAC address.
         * @param formattedMac      Formatted MAC address.
         * @param selectionStart    MAC EditText field cursor position.
         * @param lengthDiff        Formatted/Entered MAC number of characters difference.
         */

        private void setMacEdit(String cleanMac, String formattedMac, int selectionStart, int lengthDiff) {
            mMacEdit.removeTextChangedListener(this);
            if (cleanMac.length() <= 12) {
                mMacEdit.setText(formattedMac);
                mMacEdit.setSelection(selectionStart + lengthDiff);
                mPreviousMac = formattedMac;
            } else {
                mMacEdit.setText(mPreviousMac);
                mMacEdit.setSelection(mPreviousMac.length());
            }
            mMacEdit.addTextChangedListener(this);
        }
    });
}

One important thing to note is the removal and re-adding of TextChangedListener in the setMacEdit() method. Without this part the code results in stack-overflow since every setText() triggers the TextChangedListener over and over again.

Hope it helps, enjoy! :)