Monitoring EditText Sessions

Dave Smith
Dave Smith

Something that has bothered many Android developers (including myself) since the dawn of the soft keyboard is having a way to track the simple event of when a user is done editing the text in a text field. Depending on your application, there may be actions you need to take when a user has completed text entry in a field; and these actions are too granular to wait for the Activity state to change and not granular enough to act with a TextWatcher on every keystroke the user makes. Sadly, there are no discreet callbacks that an application can register for to determine when Android's InputMethodManager and the associated InputConnection used to feed keyboard data into any given TextView changes state between active and inactive.

Oh, there are methods you can query such as InputMethodManager.isActive() to get this information, but no real cues from the system as to when a good time to call these methods might be. Also, these methods do not return information directly related to whether the soft input method is active (the isActive() implementation will return true as long as there is an active InputConnection —blinking cursor—in a given View).

A Simple Widget Solution

In lieu of good session tracking APIs; to solve this problem, I created a simple little widget that I call SessionMonitorEditText. It's a subclass of EditText that operates on the premise that there are three main events under which you can expect the user is done editing the data in the field:

  • The user has pressed the action button of the keyboard (Done, Next, etc.)
  • The user has pressed the back button to dismiss the keyboard
  • The user has tapped another EditText or focusable item to start editing

SessionMonitorEditText is designed to track each of these events, and fire a single callback in any of the events to notify the application that the user has finished editing that field. The full source for this widget is available at the end of this article, but here is a short discussion of the salient points.

First, a new interface is created so the application can register to be notified of these "session complete" events.

public interface OnEditSessionCompleteListener {
  public void onEditSessionComplete(TextView v);
}

To handle the first user case, we have the EditText implement OnEditorActionListener, which will tell us when the user hits the action button on their keyboard. We must also handle the special case where the action is simply a return to move to the next line in multi-line fields:

@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
  //Forward the event, if necessary
  boolean ret = false;
  if(mActionListener != null) {
    ret = mActionListener.onEditorAction(v, actionId, event);
  }

  //Any default editor action typically ends the session,
  // except for returns in a multi-line field
  boolean returnAction = (actionId == EditorInfo.IME_NULL);
  boolean multiLine = (getInputType() & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
  if(!returnAction || !multiLine) {
    notifySessionListener();
  }

  return ret;
}

To handle the second user case, we rely on overriding the TextView.onKeyPreIme() method. This method notifies us of a key event before it is dispatched to the InputMethod, and we use this to look for BACK presses:

@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
  //If it's a BACK key, this will end the session
  if(keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
    notifySessionListener();
  }

  return super.onKeyPreIme(keyCode, event);
}

Finally, the third user case is managed by monitoring the focus of the EditText. If the user taps on another EditText or focusable item, this view will lose focus:

@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
  //View will lose focus if use taps another field to edit
  if(!focused) {
    notifySessionListener();
  }

  super.onFocusChanged(focused, direction, previouslyFocusedRect);
}

All of these tracking methods call into the same place, which sends a notification to the registered listener that this particular view is no longer the active editing view.

private void notifySessionListener() {
  if(mSessionListener != null) {
    mSessionListener.onEditSessionComplete(this);
  }
}

Note that in all of these cases the callbacks do not consume the events and attempt to forward them on to the default implementations. We do not want to intercept normal behavior, which reduces the ubiquity of our custom widget, we simply want to monitor for certain user events to fire another event of our own.

Link: SessionMonitorEditText.java