Implementing accessability on custom view gives no verbal feedback - android

I have turned accessability on, and my device speaks as I navigate around.
I have a custom seekbar and have implemented the folllowing:
onTouchEvent excerpt:
...
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(true);
setTouchAngle(pointToAngle(touchX, touchY));
score = getScoreFromAngle(angleStart,touchAngle);
if (onScoreSetListener != null) {
onScoreSetListener.onScorePoll(this, score);
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
}
break;
...
onPopulateAccessibilityEvent method:
#Override
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
super.onPopulateAccessibilityEvent(event);
LogUtils.i(TAG,"onPopulateAccessibilityEvent()",null);
switch (event.getEventType()) {
case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
LogUtils.d(TAG,"dispatchPopulateAccessibilityEvent() TYPE_VIEW_TEXT_CHANGED",null);
event.getText().add(String.valueOf(getScore()));
break;
}
}
I can see onPopulateAccessibilityEvent being called in LogCat successfully, but the device is not giving any feedback. I expect the current score to be read back, but nothing.
Does anyone have any insight?

If you're extending ProgressBar, you can set the text for outgoing TYPE_VIEW_SELECTED events. These are sent automatically as the user adjusts the seek bar.
#Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
event.getText().add(...);
}
However, it looks like you may have extended View. In this case, you will need to use a slight workaround and trigger an announcement by sending a VIEW_FOCUSED event on ICS, or use the announceForAccessibility API on JellyBean and above. Which would require the support-v4 library and would look like this:
/** The parent context. Used to obtain string resources. */
private final Context mContext;
/**
* The accessibility manager for this context. This is used to check the
* accessibility enabled state, as well as to send raw accessibility events.
*/
private final AccessibilityManager mA11yManager;
/**
* Generates and dispatches an SDK-specific spoken announcement.
* <p>
* For backwards compatibility, we're constructing an event from scratch
* using the appropriate event type. If your application only targets SDK
* 16+, you can just call View.announceForAccessibility(CharSequence).
* </p>
*
* #param text The text to announce.
*/
private void announceForAccessibilityCompat(CharSequence text) {
if (!mA11yManager.isEnabled()) {
return;
}
// Prior to SDK 16, announcements could only be made through FOCUSED
// events. Jelly Bean (SDK 16) added support for speaking text verbatim
// using the ANNOUNCEMENT event type.
final int eventType;
if (Build.VERSION.SDK_INT < 16) {
eventType = AccessibilityEvent.TYPE_VIEW_FOCUSED;
} else {
eventType = AccessibilityEventCompat.TYPE_ANNOUNCEMENT;
}
// Construct an accessibility event with the minimum recommended
// attributes. An event without a class name or package may be dropped.
final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
event.getText().add(text);
event.setEnabled(isEnabled());
event.setClassName(getClass().getName());
event.setPackageName(mContext.getPackageName());
// JellyBean MR1 requires a source view to set the window ID.
final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event);
record.setSource(this);
// Sends the event directly through the accessibility manager. If your
// application only targets SDK 14+, you should just call
// getParent().requestSendAccessibilityEvent(this, event);
mA11yManager.sendAccessibilityEvent(event);
}

Related

How to change/override the Checkbox content description value, when accessibility turned on?

I have a checkbox in my activity and provided android:contentDescription="selected". Also in java class provided as below.
checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
checkbox.setContentDescription(b ? "Selected" : "Not Selected");
}
});
When i turn on talkback and selected the checkbox, then it says "Checked/Not Checked" instead of "Selected/Not Selected".
It is taking default values of OS (Varies in different manufacturers and OS version) but not provided values. Is there any way, we can fix this issue?
So I ran into this issue a while back and found a rather hacky workaround.
Create and use a subclass of CheckBox like this and replace the strings:
public class CustomCheckBox extends CheckBox {
// constructors...
#Override
public CharSequence getAccessibilityClassName() {
// override to disable the "checkbox" readout
return CustomCheckBox.class.getSimpleName();
}
#Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
// by setting checkable to false the default checked/unchecked readouts are disabled
info.setCheckable(false);
// ...and then you can set whatever you want as a text
info.setText(getStateDescription());
}
#Override
public void setChecked(boolean checked) {
if (checked == isChecked()) return;
super.setChecked(checked);
// since we've disabled the checked/unchecked readouts
// we are forced to manually announce changes to the state
announceForAccessibility(getStateDescription());
}
private String getStateDescription() {
if (isChecked()) {
return "Custom checked description";
} else {
return "Custom unchecked description";
}
}
}
Also I should start this off by saying I haven't tried the stuff mentioned below, but it seems that Android R (API 30) adds an official way to override this by adding setStateDescription(CharSequence) to CompoundButton Source Doc
/**
* This function is called when an instance or subclass sets the state description. Once this
* is called and the argument is not null, the app developer will be responsible for updating
* state description when checked state changes and we will not set state description
* in {#link #setChecked}. App developers can restore the default behavior by setting the
* argument to null. If {#link #setChecked} is called first and then setStateDescription is
* called, two state change events will be merged by event throttling and we can still get
* the correct state description.
*
* #param stateDescription The state description.
*/
#Override
public void setStateDescription(#Nullable CharSequence stateDescription) {
mCustomStateDescription = stateDescription;
if (stateDescription == null) {
setDefaultStateDescritption();
} else {
super.setStateDescription(stateDescription);
}
}

Google voice Actions: When the Activity.isVoiceInteraction is true?

Context:
I'm trying to integrate the google voice actions in my app. I have seen and understood (or at least that what I think) the google codelabs-io2015 example and in this example if you don't modify the code everything works as expected. The problem starts when you attempt to adapt this example to your real use case.
The problem:
So, my problem is that I'm trying to implement the search voice action but Activity#isVoiceInteraction is always false. I don't finally understand when and why the activity is (and when it is not) linked to a voice interactor.
Research:
Looking into the source code of the Activity ,Activity#isVoiceInteraction and Activity#getVoiceInteractor API level 23, I have found the following:
/**
* Check whether this activity is running as part of a voice interaction with the user.
* If true, it should perform its interaction with the user through the
* {#link VoiceInteractor} returned by {#link #getVoiceInteractor}.
*/
public boolean isVoiceInteraction() {
return mVoiceInteractor != null;
}
,
/**
* Retrieve the active {#link VoiceInteractor} that the user is going through to
* interact with this activity.
*/
public VoiceInteractor getVoiceInteractor() {
return mVoiceInteractor;
}
and the mVoiceInteractor is only initialize on attach function as shown below:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
...
mLastNonConfigurationInstances = lastNonConfigurationInstances;
if (voiceInteractor != null) {
if (lastNonConfigurationInstances != null) {
mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
} else {
mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
Looper.myLooper());
}
}
...
}

NullPointerException in android.view.accessibility.CaptioningManager$1.onChange

I've seen strange crash reports from my app.
android.view.accessibility.CaptioningManager$1.onChange (CaptioningManager.java:226)
android.database.ContentObserver.onChange (ContentObserver.java:145)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:703)
http://crashes.to/s/db9e325f0f5
It looks like that there is a problem when accessibility functions are enabled. But how I can detect on what UI element or screen that error appears?
I tried to enable accessibility on my own device and navigate through all application screens, but don't receive an exeption.
EDIT
Can this error be caused by using Span in TextView?
// welcome text
TextView welcome = (TextView) view.findViewById(R.id.home_user_name);
welcome.setText(Html.fromHtml(getString(R.string.home_welcome_text, accountManager.getActiveUser())));
// change...
welcome.append(" ");
SpannableString str = SpannableString.valueOf(getString(R.string.home_user_change));
str.setSpan(new URLSpan(getString(R.string.home_user_change)) {
#Override
public void onClick(View view) {
mGuiHandler.sendEmptyMessage(MESSAGE_CHANGE_USER);
}
}, 0, str.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
welcome.append(str);
welcome.setMovementMethod(LinkMovementMethod.getInstance());
First, this isn't part of the accessibility service APIs. It is part of the View's implementation of accessibility. See the google code project. CaptioningManager is in the core/java/android/view/accessibility package. So, this crash is happening regardless of whether accessibility is on or not, or the very least, independent of what accessibility service may be on.
In Captioning Manager on line 235 (the version on Google Code is out of date, but pretty close.). The onChange function is like this:
#Override
public void onChange(boolean selfChange, Uri uri) {
final String uriPath = uri.getPath();
final String name = uriPath.substring(uriPath.lastIndexOf('/') + 1);
if (Secure.ACCESSIBILITY_CAPTIONING_ENABLED.equals(name)) {
notifyEnabledChanged();
} else if (Secure.ACCESSIBILITY_CAPTIONING_LOCALE.equals(name)) {
notifyLocaleChanged();
} else if (Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE.equals(name)) {
notifyFontScaleChanged();
} else {
// We only need a single callback when multiple style properties
// change in rapid succession.
mHandler.removeCallbacks(mStyleChangedRunnable);
mHandler.post(mStyleChangedRunnable);
}
}
This is being called by the ContentObserver class, from this point:
/**
* Dispatches a change notification to the observer. Includes the changed
* content Uri when available and also the user whose content changed.
*
* #param selfChange True if this is a self-change notification.
* #param uri The Uri of the changed content, or null if unknown.
* #param userId The user whose content changed. Can be either a specific
* user or {#link UserHandle#USER_ALL}.
*
* #hide
*/
public void onChange(boolean selfChange, Uri uri, int userId) {
onChange(selfChange, uri);
}
Notice in the documentation for the ContentObserver class explicitly states that the uri can be null, but the CaptioningManager immediately calls getPath without checking fi the value is null. This is why it is crashing. The uri passed to onChange is null.
Now, this is where it gets a little fuzzy right. The rest is private, not available on Google Code. SO, we can only guess as to what zygote is doing. Although, it likely wouldn't be helpful, even if we could see it.
Now, what can we glean from this. In the documentation for the CaptioningManager we see the following explanation for its purpose:
Contains methods for accessing and monitoring preferred video
captioning state and visual properties.
So, based on all of this, check any URIs or other properties of any video and perhaps other media elements in your application...

Accessibility function implementation problems in Android

I'm developing application that views books. There is a screen (Activity) which shows a book. It has custom view, something similar to ViewSwitcher and every page is a bitmap that is rendered by a custom View.
Now I should implement accessibility function - book should be read by the phone (audio).
I've read Accessibility section here https://developer.android.com/guide/topics/ui/accessibility/index.html but it is not clear enough.
I use SupportLibrary for accessibility management and now I have this code in ViewGroup (which manages book pages). Code 1:
private class EditionPagesViewSwitcherAccessibilityDelegate extends AccessibilityDelegateCompat {
private int mPageCount;
private double[] mPageRange;
#Override
public void onInitializeAccessibilityEvent(final View host, final AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(host, event);
event.setClassName(EditionPagesViewSwitcher.class.getName());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
event.setScrollable(canScroll());
}
if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED && updatePageValues()) {
event.setItemCount(mPageCount);
// we use +1 because of user friendly numbers (from 1 not 0)
event.setFromIndex((int) (mPageRange[0] + 1));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
event.setToIndex((int) (mPageRange[1] + 1));
}
}
}
#Override
public void onInitializeAccessibilityNodeInfo(final View host, final AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName(EditionPagesViewSwitcher.class.getName());
info.setScrollable(canScroll());
info.setLongClickable(true);
if (canScrollForward()) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
}
if (canScrollBackward()) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
}
}
#Override
public boolean performAccessibilityAction(final View host, final int action, final Bundle args) {
if (super.performAccessibilityAction(host, action, args)) {
return true;
}
switch (action) {
case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: {
if (canScrollForward()) {
showNext();
return true;
}
}
return false;
case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: {
if (canScrollBackward()) {
showPrevious();
return true;
}
}
return false;
}
return false;
}
Here is code from page view Code 2:
#Override
public void onInitializeAccessibilityEvent(final View host, final AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(host, event);
event.setClassName(EditionPageView.class.getName());
if (hasText()) {
event.getText().add(getPageRangeText());
final String trimText = mSurfaceUpdateData.getPageText().trim();
if (trimText.length() > MAX_TEXT_LENGTH) {
event.getText().add(trimText.substring(0, MAX_TEXT_LENGTH));
// event.getText().add(trimText.substring(MAX_TEXT_LENGTH, trimText.length()));
}
else {
event.getText().add(trimText);
}
}
}
#Override
public void onInitializeAccessibilityNodeInfo(final View host, final AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName(EditionPageView.class.getName());
}
Because page text data loads asynchronous first time accessibility don't have any text while executes onInitializeAccessibilityEvent code. And then when data have been loaded I fire AccessibilityEvent.TYPE_VIEW_SELECTED and AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED events. Then onInitializeAccessibilityEvent executes again and phone "read" book text.
So my questions:
Is my Accessibility implementation right? May be it is design wrong? Because I didn't find any good tutorial about this feature.
Why I need to use SDK versions checks in Support implementations in Code 1? Why support implementation doesn't handle it correctly?
Is firing TYPE_VIEW_SELECTED and TYPE_VIEW_TEXT_CHANGED really needed? Or may be some other code should be implemented?
The main question. In Code 2 there is commented code line. This code statement substring text to be less then MAX_TEXT_LENGTH (it's 3800) because if text is bigger nothing is played. Nothing. Is it accessibility restriction? Any other text that is less then this value is played well.
Does anyone know where I can find any good tutorial? (yes I saw samples).
Does anyone have any custom realizations to look through?
UPDATED
Well. Here is some answers:
As I can see TYPE_VIEW_SELECTED and TYPE_VIEW_TEXT_CHANGED events are not needed if you don't want this text to be read as soon as you get it.
On Nexus 7 all large text is played well (text up to 8000 symbols), so this issue doesn't reproduce on it, but on Samsung Galaxy Tab 10.1 (Android 4.0.4) and Genymotion emulator of Tab 10.1 with Android 4.3 does. And this is strange...
4.. According to the documentation of String.substring()
The first argument you pass is the start index in the original string, the second argument is the end index in the original string.
Example:
String text = "Hello";
partOfText = text.substring(2,text.length() - 1);
partOfText equals to "llo" (the first char is index 0)
So by putting your constant MAX_TEXT_LENGTH as a first argument, it would start at index 3800 to take out the substring.
http://developer.android.com/reference/java/lang/String.html#substring(int)
You are right MAX_TEXT_LENGTH is 3800.
About your doubt,
this code:
event.getText().add(trimText.substring(MAX_TEXT_LENGTH, trimText.length()));
}
you are trying to substring "trimText" from MAX_TEXT_LENGTH to trimText.length() !
Supposing that trimText = "STACK", trimText.length() = 5, then trimText.substring(3800,5) is going to be ?
At first, this doesn't have sense, using correctly would be like this:
trimText.substring(0,2) = "ST";

Android - cannot capture backspace/delete press in soft. keyboard

I am overriding the onKeyDown method of the view (openGL surface view) to capture
all of the key presses. The problem is that on several devices the KEYCODE_DEL is not
captured. I have tried adding an onKeyListener to the view, and that captured everything except backspace key.
There has to be a way to listen to this key press event, but how?
11/12/2014 UPDATE: Changed scope of fix to not limit to < API level 19, since at a third party keyboard still has the bug beyond 19.
1/9/2014 UPDATE: I've devised an approach, with code, to resolve all Google Keyboard (LatinIME) KEYCODE_DEL problems, specifically
issues 42904 and 62306.
The enhancement in Turix's answer has been incorporated, with permission, into my own code here. Turix's improvements removed need to inject garbage characters into the Editable buffer by instead finding an incremental way to ensure that exactly one character was always in that buffer.
I've used (similar) code to this in a deployed app that you're welcome to test:
https://play.google.com/store/apps/details?id=com.goalstate.WordGames.FullBoard.trialsuite]
INTRODUCTION:
The workaround presented below is intended to work for all versions of the Google Keyboard, both past and future, so far as these two bugs are concerned. This workaround does not require that an app remain stuck targeting API level 15 or below, which some apps have restricted themselves to in order to take advantage of compatibility code that gets around issue 42904.
These problems are only present as bugs for a view that has implemented the override for onCreateInputConnection(), and which returns TYPE_NULL to the invoking IME (in the inputType member of the EditorInfo argument passed to that method by the IME). It is only by doing this that a view can reasonably expect that key events (including KEYCODE_DEL) will be returned to it from a soft keyboard. Consequently, the workaround presented here requires the TYPE_NULL InputType.
For apps not using TYPE_NULL, there are various overrides in the BaseInputConnection-derived object returned by a view from its onCreateInputConnection() override, that are invoked by the IME when the user performs edits, instead of the IME generating key events. This (non TYPE_NULL) approach is usually superior, because the soft keyboard's capabilities now extend far beyond the mere tapping of keys, to things like voice input, completion, etc. Key events are an older method, and those implementing LatinIME at Google have said that they would like to see the use of TYPE_NULL (and key events) go away.
If discontinuing the use of TYPE_NULL is an option, then I would urge you to proceed with the recommended approach of using the InputConnection override methods instead of key events (or, more simply, by using a class derived from EditText, which does that for you).
Nonetheless, TYPE_NULL behavior is not being officially discontinued, and thus the failure of LatinIME to generate KEYCODE_DEL events under certain circumstances is indeed a bug. I offer the following workaround to address this problem.
OVERVIEW:
The problems that apps have had in receiving KEYCODE_DEL from LatinIME are due to TWO known bugs, as reported here:
https://code.google.com/p/android/issues/detail?id=42904
(listed as WorkingAsIntended, but the problem is, I maintain, a bug inasmuch as it causes a failure to support KEYCODE_DEL event generation for apps targeting API level 16 and above that have specifically listed an InputType of TYPE_NULL. The problem is fixed in the latest releases of LatinIME, but there are past releases in the wild that still exhibit this bug, and so apps using TYPE_NULL and targeting API Level 16 or above will still need a workaround that can be performed from within the app.
and here:
http://code.google.com/p/android/issues/detail?id=62306
(presently listed as fixed but not yet released - FutureRelease - but even once it is released, we will still need a workaround that can be performed from within the app to deal with the past releases that will persist "in the wild").
Consistent with this thesis (that the problems experienced with KEYCODE_DEL events are due to bugs in LatinIME), I have found that when using an external hardware keyboard, and also when using the third party SwiftKey soft keyboard, these problems do not occur, while they do occur for specific versions of LatinIME.
One or the other (but not both at once) of these problems is present in some LatinIME releases. Consequently, it is difficult for developers to know during testing whether they have worked around all KEYCODE_DEL problems, and sometimes when an Android (or Google Keyboard) update is performed, a problem will no longer be reproducible in testing. Nonetheless, the LatinIME versions that cause the problem will be present on quite a number of devices in use. This has forced me to dig into the AOSP LatinIME git repo to determine the exact scope of each of the two problems (i.e., the specific LatinIME, and Android, versions for which each of the two issues may be present). The workaround code below has been restricted to those specific versions.
The workaround code presented below includes extensive comments which should help you to understand what it is attempting to accomplish. Following the presentation of the code, I will provide some additional discussion, which will include the specific Android Open Source Project (AOSP) commits at which each of the two bugs was introduced, and at which it disappeared, and also the Android versions that might include the affected Google Keyboard releases.
I would warn anyone thinking of using this approach to perform their own testing to verify that it works for their particular app. I think that it will work in general, and have tested it on a number of devices and LatinIME versions, but the reasoning is complicated, so proceed with caution. If you find any problems, please post a comment below.
CODE:
Here, then, is my workaround for both of the two problems, with an explanation included in the comments to the code:
First, include the following class (edited to taste) in your app, in its own source file InputConnectionAccomodatingLatinIMETypeNullIssues.java:
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
/**
*
* #author Carl Gunther
* There are bugs with the LatinIME keyboard's generation of KEYCODE_DEL events
* that this class addresses in various ways. These bugs appear when the app
* specifies TYPE_NULL, which is the only circumstance under which the app
* can reasonably expect to receive key events for KEYCODE_DEL.
*
* This class is intended for use by a view that overrides
* onCreateInputConnection() and specifies to the invoking IME that it wishes
* to use the TYPE_NULL InputType. This should cause key events to be returned
* to the view.
*
*/
public class InputConnectionAccomodatingLatinIMETypeNullIssues extends BaseInputConnection {
//This holds the Editable text buffer that the LatinIME mistakenly *thinks*
// that it is editing, even though the views that employ this class are
// completely driven by key events.
Editable myEditable = null;
//Basic constructor
public InputConnectionAccomodatingLatinIMETypeNullIssues(View targetView, boolean fullEditor) {
super(targetView, fullEditor);
}
//This method is called by the IME whenever the view that returned an
// instance of this class to the IME from its onCreateInputConnection()
// gains focus.
#Override
public Editable getEditable() {
//Some versions of the Google Keyboard (LatinIME) were delivered with a
// bug that causes KEYCODE_DEL to no longer be generated once the number
// of KEYCODE_DEL taps equals the number of other characters that have
// been typed. This bug was reported here as issue 62306.
//
// As of this writing (1/7/2014), it is fixed in the AOSP code, but that
// fix has not yet been released. Even when it is released, there will
// be many devices having versions of the Google Keyboard that include the bug
// in the wild for the indefinite future. Therefore, a workaround is required.
//
//This is a workaround for that bug which just jams a single garbage character
// into the internal buffer that the keyboard THINKS it is editing even
// though we have specified TYPE_NULL which *should* cause LatinIME to
// generate key events regardless of what is in that buffer. We have other
// code that attempts to ensure as the user edites that there is always
// one character remaining.
//
// The problem arises because when this unseen buffer becomes empty, the IME
// thinks that there is nothing left to delete, and therefore stops
// generating KEYCODE_DEL events, even though the app may still be very
// interested in receiving them.
//
//So, for example, if the user taps in ABCDE and then positions the
// (app-based) cursor to the left of A and taps the backspace key three
// times without any evident effect on the letters (because the app's own
// UI code knows that there are no letters to the left of the
// app-implemented cursor), and then moves the cursor to the right of the
// E and hits backspace five times, then, after E and D have been deleted,
// no more KEYCODE_DEL events will be generated by the IME because the
// unseen buffer will have become empty from five letter key taps followed
// by five backspace key taps (as the IME is unaware of the app-based cursor
// movements performed by the user).
//
// In other words, if your app is processing KEYDOWN events itself, and
// maintaining its own cursor and so on, and not telling the IME anything
// about the user's cursor position, this buggy processing of the hidden
// buffer will stop KEYCODE_DEL events when your app actually needs them -
// in whatever Android releases incorporate this LatinIME bug.
//
// By creating this garbage characters in the Editable that is initially
// returned to the IME here, we make the IME think that it still has
// something to delete, which causes it to keep generating KEYCODE_DEL
// events in response to backspace key presses.
//
// A specific keyboard version that I tested this on which HAS this
// problem but does NOT have the "KEYCODE_DEL completely gone" (issue 42904)
// problem that is addressed by the deleteSurroundingText() override below
// (the two problems are not both present in a single version) is
// 2.0.19123.914326a, tested running on a Nexus7 2012 tablet.
// There may be other versions that have issue 62306.
//
// A specific keyboard version that I tested this on which does NOT have
// this problem but DOES have the "KEYCODE_DEL completely gone" (issue
// 42904) problem that is addressed by the deleteSurroundingText()
// override below is 1.0.1800.776638, tested running on the Nexus10
// tablet. There may be other versions that also have issue 42904.
//
// The bug that this addresses was first introduced as of AOSP commit tag
// 4.4_r0.9, and the next RELEASED Android version after that was
// android-4.4_r1, which is the first release of Android 4.4. So, 4.4 will
// be the first Android version that would have included, in the original
// RELEASED version, a Google Keyboard for which this bug was present.
//
// Note that this bug was introduced exactly at the point that the OTHER bug
// (the one that is addressed in deleteSurroundingText(), below) was first
// FIXED.
//
// Despite the fact that the above are the RELEASES associated with the bug,
// the fact is that any 4.x Android release could have been upgraded by the
// user to a later version of Google Keyboard than was present when the
// release was originally installed to the device. I have checked the
// www.archive.org snapshots of the Google Keyboard listing page on the Google
// Play store, and all released updates listed there (which go back to early
// June of 2013) required Android 4.0 and up, so we can be pretty sure that
// this bug is not present in any version earlier than 4.0 (ICS), which means
// that we can limit this fix to API level 14 and up. And once the LatinIME
// problem is fixed, we can limit the scope of this workaround to end as of
// the last release that included the problem, since we can assume that
// users will not upgrade Google Keyboard to an EARLIER version than was
// originally included in their Android release.
//
// The bug that this addresses was FIXED but NOT RELEASED as of this AOSP
// commit:
//https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+
// /b41bea65502ce7339665859d3c2c81b4a29194e4/java/src/com/android
// /inputmethod/latin/LatinIME.java
// so it can be assumed to affect all of KitKat released thus far
// (up to 4.4.2), and could even affect beyond KitKat, although I fully
// expect it to be incorporated into the next release *after* API level 19.
//
// When it IS released, this method should be changed to limit it to no
// higher than API level 19 (assuming that the fix is released before API
// level 20), just in order to limit the scope of this fix, since poking
// 1024 characters into the Editable object returned here is of course a
// kluge. But right now the safest thing is just to not have an upper limit
// on the application of this kluge, since the fix for the problem it
// addresses has not yet been released (as of 1/7/2014).
if(Build.VERSION.SDK_INT >= 14) {
if(myEditable == null) {
myEditable = new EditableAccomodatingLatinIMETypeNullIssues(
EditableAccomodatingLatinIMETypeNullIssues.ONE_UNPROCESSED_CHARACTER);
Selection.setSelection(myEditable, 1);
}
else {
int myEditableLength = myEditable.length();
if(myEditableLength == 0) {
//I actually HAVE seen this be zero on the Nexus 10 with the keyboard
// that came with Android 4.4.2
// On the Nexus 10 4.4.2 if I tapped away from the view and then back to it, the
// myEditable would come back as null and I would create a new one. This is also
// what happens on other devices (e.g., the Nexus 6 with 4.4.2,
// which has a slightly later version of the Google Keyboard). But for the
// Nexus 10 4.4.2, the keyboard had a strange behavior
// when I tapped on the rack, and then tapped Done on the keyboard to close it,
// and then tapped on the rack AGAIN. In THAT situation,
// the myEditable would NOT be set to NULL but its LENGTH would be ZERO. So, I
// just append to it in that situation.
myEditable.append(
EditableAccomodatingLatinIMETypeNullIssues.ONE_UNPROCESSED_CHARACTER);
Selection.setSelection(myEditable, 1);
}
}
return myEditable;
}
else {
//Default behavior for keyboards that do not require any fix
return super.getEditable();
}
}
//This method is called INSTEAD of generating a KEYCODE_DEL event, by
// versions of Latin IME that have the bug described in Issue 42904.
#Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
//If targetSdkVersion is set to anything AT or ABOVE API level 16
// then for the GOOGLE KEYBOARD versions DELIVERED
// with Android 4.1.x, 4.2.x or 4.3.x, NO KEYCODE_DEL EVENTS WILL BE
// GENERATED BY THE GOOGLE KEYBOARD (LatinIME) EVEN when TYPE_NULL
// is being returned as the InputType by your view from its
// onCreateInputMethod() override, due to a BUG in THOSE VERSIONS.
//
// When TYPE_NULL is specified (as this entire class assumes is being done
// by the views that use it, what WILL be generated INSTEAD of a KEYCODE_DEL
// is a deleteSurroundingText(1,0) call. So, by overriding this
// deleteSurroundingText() method, we can fire the KEYDOWN/KEYUP events
// ourselves for KEYCODE_DEL. This provides a workaround for the bug.
//
// The specific AOSP RELEASES involved are 4.1.1_r1 (the very first 4.1
// release) through 4.4_r0.8 (the release just prior to Android 4.4).
// This means that all of KitKat should not have the bug and will not
// need this workaround.
//
// Although 4.0.x (ICS) did not have this bug, it was possible to install
// later versions of the keyboard as an app on anything running 4.0 and up,
// so those versions are also potentially affected.
//
// The first version of separately-installable Google Keyboard shown on the
// Google Play store site by www.archive.org is Version 1.0.1869.683049,
// on June 6, 2013, and that version (and probably other, later ones)
// already had this bug.
//
//Since this required at least 4.0 to install, I believe that the bug will
// not be present on devices running versions of Android earlier than 4.0.
//
//AND, it should not be present on versions of Android at 4.4 and higher,
// since users will not "upgrade" to a version of Google Keyboard that
// is LOWER than the one they got installed with their version of Android
// in the first place, and the bug will have been fixed as of the 4.4 release.
//
// The above scope of the bug is reflected in the test below, which limits
// the application of the workaround to Android versions between 4.0.x and 4.3.x.
//
//UPDATE: A popular third party keyboard was found that exhibits this same issue. It
// was not fixed at the same time as the Google Play keyboard, and so the bug in that case
// is still in place beyond API LEVEL 19. So, even though the Google Keyboard fixed this
// as of level 19, we cannot take out the fix based on that version number. And so I've
// removed the test for an upper limit on the version; the fix will remain in place ad
// infinitum - but only when TYPE_NULL is used, so it *should* be harmless even when
// the keyboard does not have the problem...
if((Build.VERSION.SDK_INT >= 14) // && (Build.VERSION.SDK_INT < 19)
&& (beforeLength == 1 && afterLength == 0)) {
//Send Backspace key down and up events to replace the ones omitted
// by the LatinIME keyboard.
return super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
&& super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
}
else {
//Really, I can't see how this would be invoked, given that we're using
// TYPE_NULL, for non-buggy versions, but in order to limit the impact
// of this change as much as possible (i.e., to versions at and above 4.0)
// I am using the original behavior here for non-affected versions.
return super.deleteSurroundingText(beforeLength, afterLength);
}
}
}
Next, take each View-derived class that needs to receive key events from the LatinIME soft keyboard, and edit it as follows:
First, create an override to onCreateInputConnection() in the view that is to receive
key events as follows:
#Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
//Passing FALSE as the SECOND ARGUMENT (fullEditor) to the constructor
// will result in the key events continuing to be passed in to this
// view. Use our special BaseInputConnection-derived view
InputConnectionAccomodatingLatinIMETypeNullIssues baseInputConnection =
new InputConnectionAccomodatingLatinIMETypeNullIssues(this, false);
//In some cases an IME may be able to display an arbitrary label for a
// command the user can perform, which you can specify here. A null value
// here asks for the default for this key, which is usually something
// like Done.
outAttrs.actionLabel = null;
//Special content type for when no explicit type has been specified.
// This should be interpreted (by the IME that invoked
// onCreateInputConnection())to mean that the target InputConnection
// is not rich, it can not process and show things like candidate text
// nor retrieve the current text, so the input method will need to run
// in a limited "generate key events" mode. This disables the more
// sophisticated kinds of editing that use a text buffer.
outAttrs.inputType = InputType.TYPE_NULL;
//This creates a Done key on the IME keyboard if you need one
outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
return baseInputConnection;
}
Second, make the following changes to your onKey() handler for the view:
this.setOnKeyListener(new OnKeyListener() {
#Override public
boolean onKey(View v, int keyCode, KeyEvent event) {
if(event.getAction() != KeyEvent.ACTION_DOWN) {
//We only look at ACTION_DOWN in this code, assuming that ACTION_UP is redundant.
// If not, adjust accordingly.
return false;
}
else if(event.getUnicodeChar() ==
(int)EditableAccomodatingLatinIMETypeNullIssues.ONE_UNPROCESSED_CHARACTER.charAt(0))
{
//We are ignoring this character, and we want everyone else to ignore it, too, so
// we return true indicating that we have handled it (by ignoring it).
return true;
}
//Now, just do your event handling as usual...
if(keyCode == KeyEvent.KEYCODE_ENTER) {
//Trap the Done key and close the keyboard if it is pressed (if that's what you want to do)
InputMethodManager imm = (InputMethodManager)
mainActivity.getSystemService(Context.INPUT_METHOD_SERVICE));
imm.hideSoftInputFromWindow(LetterRack.this.getWindowToken(), 0);
return true;
}
else if(keyCode == KeyEvent.KEYCODE_DEL) {
//Backspace key processing goes here...
return true;
}
else if((keyCode >= KeyEvent.KEYCODE_A) && (keyCode <= KeyEvent.KEYCODE_Z)) {
//(Or, use event.getUnicodeChar() if preferable to key codes).
//Letter processing goes here...
return true;
}
//Etc. } };
Finally, we need to define a class for our editable that ensures that there is always at least one
character in our editable buffer:
import android.text.SpannableStringBuilder;
public class EditableAccomodatingLatinIMETypeNullIssues extends SpannableStringBuilder {
EditableAccomodatingLatinIMETypeNullIssues(CharSequence source) {
super(source);
}
//This character must be ignored by your onKey() code.
public static CharSequence ONE_UNPROCESSED_CHARACTER = "/";
#Override
public SpannableStringBuilder replace(final int
spannableStringStart, final int spannableStringEnd, CharSequence replacementSequence,
int replacementStart, int replacementEnd) {
if (replacementEnd > replacementStart) {
//In this case, there is something in the replacementSequence that the IME
// is attempting to replace part of the editable with.
//We don't really care about whatever might already be in the editable;
// we only care about making sure that SOMETHING ends up in it,
// so that the backspace key will continue to work.
// So, start by zeroing out whatever is there to begin with.
super.replace(0, length(), "", 0, 0);
//We DO care about preserving the new stuff that is replacing the stuff in the
// editable, because this stuff might be sent to us as a keydown event. So, we
// insert the new stuff (typically, a single character) into the now-empty editable,
// and return the result to the caller.
return super.replace(0, 0, replacementSequence, replacementStart, replacementEnd);
}
else if (spannableStringEnd > spannableStringStart) {
//In this case, there is NOTHING in the replacementSequence, and something is
// being replaced in the editable.
// This is characteristic of a DELETION.
// So, start by zeroing out whatever is being replaced in the editable.
super.replace(0, length(), "", 0, 0);
//And now, we will place our ONE_UNPROCESSED_CHARACTER into the editable buffer, and return it.
return super.replace(0, 0, ONE_UNPROCESSED_CHARACTER, 0, 1);
}
// In this case, NOTHING is being replaced in the editable. This code assumes that there
// is already something there. This assumption is probably OK because in our
// InputConnectionAccomodatingLatinIMETypeNullIssues.getEditable() method
// we PLACE a ONE_UNPROCESSED_CHARACTER into the newly-created buffer. So if there
// is nothing replacing the identified part
// of the editable, and no part of the editable that is being replaced, then we just
// leave whatever is in the editable ALONE,
// and we can be confident that there will be SOMETHING there. This call to super.replace()
// in that case will be a no-op, except
// for the value it returns.
return super.replace(spannableStringStart, spannableStringEnd,
replacementSequence, replacementStart, replacementEnd);
}
}
That completes the source changes that I've found seem to handle both problems.
ADDITIONAL NOTES:
The problem described by Issue 42904 was introduced in the LatinIME version delivered with API level 16. Prior to that, KEYCODE_DEL events were generated regardless of whether TYPE_NULL was used. In the LatinIME released with Jelly Bean, this generation was discontinued, but no exception was made for TYPE_NULL, and so TYPE_NULL behavior was effectively disabled for apps targeted above API level 16. There was, however, compatibility code added that allowed apps that had a targetSdkVersion < 16 to continue to receive KEYCODE_DEL events, even without TYPE_NULL. See this AOSP commit at line 1493:
https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+/android-4.1.1_r1/java/src/com/android/inputmethod/latin/LatinIME.java
Therefore, you could work around this problem by setting targetSdkVersion in your app to 15 or lower.
As of commit 4.4_r0.9 (just prior to the 4.4 release), this problem was fixed by adding a test for isTypeNull() to the conditions guarding KEYCODE_DEL generation. Unfortunately, a new bug (62306) was introduced at exactly that point which caused the entire clause wrapping KEYCODE_DEL generation to be skipped if the user had typed backspace as many times as she had typed other characters. This led to a failure to generate KEYCODE_DEL under those circumstances, even with TYPE_NULL, and even with targetSdkVersion <= 15. This caused apps that had previously been able to get correct KEYCODE_DEL behavior via compatibility code (targetSdkVersion <= 15) to suddenly experience this problem when users upgraded their copies of Google Keyboard (or performed an OTA that contained a new version of Google Keyboard). See this AOSP git file at line 2146 (the clause including "NOT_A_CODE"):
https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+/android-4.4_r0.9/java/src/com/android/inputmethod/latin/LatinIME.java
This problem has persisted in released versions of Google Keyboard to the present time (1/7/2014). It has been fixed in the repo, but as of this writing has not been released.
That unreleased commit can be found here (the git commit containing this merges a commit titled "Send backspace as an event when TYPE_NULL"), at line 2110 (you can see that the "NOT_A_CODE" clause that used to prevent our reaching the clause that generates KEYCODE_DEL has been removed):
https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+/b41bea65502ce7339665859d3c2c81b4a29194e4/java/src/com/android/inputmethod/latin/LatinIME.java
When this fix is released, that version of the Google Keyboard will no longer have either of these two problems affecting TYPE_NULL. However, there will still be older versions installed on particular devices for the indefinite future. Hence, the problem will still need a workaround. Eventually, as more people upgrade to a higher level than the last one not including the fix, this workaround will be needed less and less. But it is already scoped to phase itself out (once you make the indicated changes to put the final limit on the scope, when the final fix has actually been released so that you know what it actually is).
Looks like a bug with Android:
Issue 42904:
KEYCODE_DEL event is not delivered to EditText in SDK 16 and above.
Issue 42904 # code.google.com
(This answer is meant as an addendum to the accepted answer posted here by Carl.)
While greatly appreciative of the research and understanding of the two bugs, I had a few troubles with the workaround posted here by Carl. The main issue I had was that, although Carl's comment block says that the KeyEvent.ACTION_MULTIPLE path in onKey() would only be taken on "the first event received after selecting the letter rack", for me, every single key event took that path. (I discovered by looking at the BaseInputConnection.java code for API-level-18 that this is because the entire Editable text is used in sendCurrentText() each time. I'm not sure why it worked for Carl but not me.)
So, inspired by Carl's solution, I adapted it to not have this problem. My solution to issue 62306 (linked to in Carl's answer) tries to achieve the same basic effect of "tricking" the IME into thinking that there is always more text that can be backspaced over. However, it does this by making sure the Editable has exactly one character in it. To do so, you need to extend the underlying class that implements the Editable interface, SpannedStringBuilder, in a way similar to the following:
private class MyEditable extends SpannableStringBuilder
{
MyEditable(CharSequence source) {
super(source);
}
#Override
public SpannableStringBuilder replace(final int start, final int end, CharSequence tb, int tbstart, int tbend) {
if (tbend > tbstart) {
super.replace(0, length(), "", 0, 0);
return super.replace(0, 0, tb, tbstart, tbend);
}
else if (end > start) {
super.replace(0, length(), "", 0, 0);
return super.replace(0, 0, DUMMY_CHAR, 0, 1);
}
return super.replace(start, end, tb, tbstart, tbend);
}
}
Basically, whenever the IME attempts to add a character to the Editable (by calling replace()), that character replaces whatever singleton character is there. Meanwhile, if the IME attempts to remove what's there, the replace() override instead replaces what's there with a singleton "dummy" character (which should be something your app will ignore) to maintain the length of 1.
This means that the implementations of getEditable() and onKey() can be slightly simpler than what Carl posted above. For example, assuming the MyEditable class above is implemented as an inner class, getEditable() becomes something like:
#Override
public Editable getEditable() {
if (Build.VERSION.SDK_INT < 14)
return super.getEditable();
if (mEditable == null) {
mEditable = this.new MyEditable(DUMMY_CHAR);
Selection.setSelection(mEditable, 1);
}
else if (m_editable.length() == 0) {
mEditable.append(DUMMY_CHAR);
Selection.setSelection(mEditable, 1);
}
return mEditable;
}
Note that with this solution, there's no need to maintain a 1024-character long string. Nor, is there any danger of "backspacing too much" (as discussed in Carl's comments about holding down the backspace key).
For completeness, onKey() becomes something like:
#Override
public boolean onKey(View v, int keyCode, KeyEvent event)
{
if (event.getAction() != KeyEvent.ACTION_DOWN)
return false;
if ((int)DUMMY_CHAR.charAt(0) == event.getUnicodeChar())
return true;
// Handle event/keyCode here as normal...
}
Finally, I should note that all of the above is meant as a workaround to issue 62306 only. I had no problems with the solution to the other issue, 42904, as posted by Carl (overriding deleteSurroundingText()) and would recommend using it as he posted it.
INTRODUCTION:
After testing both #Carl's and #Turix's solutions I noticed that:
Carl's solution does not work well with unicode characters or character sequences, since these seem to be delivered with the ACTION_MULTIPLE event, which makes it difficult to distinguish between the "dummy" characters and the actual character.
I was not able to get deleteSurroundingText working in the latest version of Android on my Nexus 5 (4.4.2). I tested targeting several different sdk versions, but none of them worked. Perhaps Google has decided to yet again change the logic behind the DEL key...
Therefore, I've come up with the following combined solution, using both Carl's and Turix's answers. My solution works by combining Carl's idea of a long dummy character prefix to make DEL work, but using Turix's solution for a custom Editable to generate proper key events.
RESULTS:
I have tested this solution on several devices with different versions of Android and different keyboards. All of the below test cases work for me. I have not found a case where this solution does not work.
Nexus 5 (4.4.2) with Standard Google Keyboard
Nexus 5 (4.4.2) with SwiftKey
HTC One (4.2.2) with Standard HTC Keyboard
Nexus One (2.3.6) with Standard Google Keyboard
Samsung Galaxy S3 (4.1.2) with Standard Samsung Keyboard
I have also tested targeting different sdk versions:
Target 16
Target 19
If this solution also works for you, then ple
THE VIEW:
public class MyInputView extends EditText implements View.OnKeyListener {
private String DUMMY;
...
public MyInputView(Context context) {
super(context);
init(context);
}
private void init(Context context) {
this.context = context;
this.setOnKeyListener(this);
// Generate a dummy buffer string
// Make longer or shorter as desired.
DUMMY = "";
for (int i = 0; i < 1000; i++)
DUMMY += "\0";
}
#Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
MyInputConnection ic = new MyInputConnection(this, false);
outAttrs.inputType = InputType.TYPE_NULL;
return ic;
}
#Override
public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
int action = keyEvent.getAction();
// Catch unicode characters (even character sequeneces)
// But make sure we aren't catching the dummy buffer.
if (action == KeyEvent.ACTION_MULTIPLE) {
String s = keyEvent.getCharacters();
if (!s.equals(DUMMY)) {
listener.onSend(s);
}
}
// Catch key presses...
if (action == KeyEvent.ACTION_DOWN) {
switch (keyCode) {
case KeyEvent.KEYCODE_DEL:
...
break;
case KeyEvent.KEYCODE_ENTER:
...
break;
case KeyEvent.KEYCODE_TAB:
...
break;
default:
char ch = (char)keyEvent.getUnicodeChar();
if (ch != '\0') {
...
}
break;
}
}
return false;
}
}
THE INPUT CONNECTION:
public class MyInputConnection extends BaseInputConnection {
private MyEditable mEditable;
public MyInputConnection(View targetView, boolean fullEditor) {
super(targetView, fullEditor);
}
private class MyEditable extends SpannableStringBuilder {
MyEditable(CharSequence source) {
super(source);
}
#Override
public SpannableStringBuilder replace(final int start, final int end, CharSequence tb, int tbstart, int tbend) {
if (tbend > tbstart) {
super.replace(0, length(), "", 0, 0);
return super.replace(0, 0, tb, tbstart, tbend);
}
else if (end > start) {
super.replace(0, length(), "", 0, 0);
return super.replace(0, 0, DUMMY, 0, DUMMY.length());
}
return super.replace(start, end, tb, tbstart, tbend);
}
}
#Override
public Editable getEditable() {
if (Build.VERSION.SDK_INT < 14)
return super.getEditable();
if (mEditable == null) {
mEditable = this.new MyEditable(DUMMY);
Selection.setSelection(mEditable, DUMMY.length());
}
else if (mEditable.length() == 0) {
mEditable.append(DUMMY);
Selection.setSelection(mEditable, DUMMY.length());
}
return mEditable;
}
#Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
// Not called in latest Android version...
return super.deleteSurroundingText(beforeLength, afterLength);
}
}
I have faced similar issues where KEYCODE_DEL was not being received on touch of backspace key. It depends on the Soft Input keyboard i think, because my issue was happening only in case of some third party keyboards(swype i think) and not with default google keyboard.
Owing to #Carl's thoughts, I came to a solution which is working properly for any input type. Below I give a complete working sample app consisting of 2 classes: MainActivity and CustomEditText:
package com.example.edittextbackspace;
import android.app.Activity;
import android.os.Bundle;
import android.text.InputType;
import android.view.ViewGroup.LayoutParams;
public class MainActivity extends Activity
{
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
CustomEditText edittext = initEditText();
setContentView(edittext);
}
private CustomEditText initEditText()
{
CustomEditText editText = new CustomEditText(this)
{
#Override
public void backSpaceProcessed()
{
super.backSpaceProcessed();
editTextBackSpaceProcessed(this);
}
};
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
editText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
editText.setText("1212");
return editText;
}
private void editTextBackSpaceProcessed(CustomEditText customEditText)
{
// Backspace event is called and properly processed
}
}
package com.example.edittextbackspace;
import android.content.Context;
import android.text.Editable;
import android.text.Selection;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.EditText;
import java.util.ArrayList;
import java.util.List;
public class CustomEditText extends EditText implements View.OnFocusChangeListener, TextWatcher
{
private String LOG = this.getClass().getName();
private int _inputType = 0;
private int _imeOptions = 5 | EditorInfo.IME_FLAG_NO_EXTRACT_UI;
private List<String> _lastComposingTextsList = new ArrayList<String>();
private BaseInputConnection _inputConnection = null;
private String _lastComposingText = "";
private boolean _commitText = true;
private int _lastCursorPosition = 0;
private boolean _isComposing = false;
private boolean _characterRemoved = false;
private boolean _isTextComposable = false;
public CustomEditText(Context context)
{
super(context);
setOnFocusChangeListener(this);
addTextChangedListener(this);
}
#Override
public InputConnection onCreateInputConnection(final EditorInfo outAttrs)
{
CustomEditText.this._inputConnection = new BaseInputConnection(this, false)
{
#Override
public boolean deleteSurroundingText(int beforeLength, int afterLength)
{
handleEditTextDeleteEvent();
return super.deleteSurroundingText(beforeLength, afterLength);
}
#Override
public boolean setComposingText(CharSequence text, int newCursorPosition)
{
CustomEditText.this._isTextComposable = true;
CustomEditText.this._lastCursorPosition = getSelectionEnd();
CustomEditText.this._isComposing = true;
if (text.toString().equals(CustomEditText.this._lastComposingText))
return true;
else
CustomEditText.this._commitText = true;
if (text.length() < CustomEditText.this._lastComposingText.length())
{
CustomEditText.this._lastComposingText = text.toString();
try
{
if (text.length() > 0)
{
if (CustomEditText.this._lastComposingTextsList.size() > 0)
{
if (CustomEditText.this._lastComposingTextsList.size() > 0)
{
CustomEditText.this._lastComposingTextsList.remove(CustomEditText.this._lastComposingTextsList.size() - 1);
}
}
else
{
CustomEditText.this._lastComposingTextsList.add(text.toString().substring(0, text.length() - 1));
}
}
int start = Math.max(getSelectionStart(), 0) - 1;
int end = Math.max(getSelectionEnd(), 0);
CustomEditText.this._characterRemoved = true;
getText().replace(Math.min(start, end), Math.max(start, end), "");
}
catch (Exception e)
{
Log.e(LOG, "Exception in setComposingText: " + e.toString());
}
return true;
}
else
{
CustomEditText.this._characterRemoved = false;
}
if (text.length() > 0)
{
CustomEditText.this._lastComposingText = text.toString();
String textToInsert = Character.toString(text.charAt(text.length() - 1));
int start = Math.max(getSelectionStart(), 0);
int end = Math.max(getSelectionEnd(), 0);
CustomEditText.this._lastCursorPosition++;
getText().replace(Math.min(start, end), Math.max(start, end), textToInsert);
CustomEditText.this._lastComposingTextsList.add(text.toString());
}
return super.setComposingText("", newCursorPosition);
}
#Override
public boolean commitText(CharSequence text, int newCursorPosition)
{
CustomEditText.this._isComposing = false;
CustomEditText.this._lastComposingText = "";
if (!CustomEditText.this._commitText)
{
CustomEditText.this._lastComposingTextsList.clear();
return true;
}
if (text.toString().length() > 0)
{
try
{
String stringToReplace = "";
int cursorPosition = Math.max(getSelectionStart(), 0);
if (CustomEditText.this._lastComposingTextsList.size() > 1)
{
if (text.toString().trim().isEmpty())
{
getText().replace(cursorPosition, cursorPosition, " ");
}
else
{
stringToReplace = CustomEditText.this._lastComposingTextsList.get(CustomEditText.this._lastComposingTextsList.size() - 2) + text.charAt(text.length() - 1);
getText().replace(cursorPosition - stringToReplace.length(), cursorPosition, text);
}
CustomEditText.this._lastComposingTextsList.clear();
return true;
}
else if (CustomEditText.this._lastComposingTextsList.size() == 1)
{
getText().replace(cursorPosition - 1, cursorPosition, text);
CustomEditText.this._lastComposingTextsList.clear();
return true;
}
}
catch (Exception e)
{
Log.e(LOG, "Exception in commitText: " + e.toString());
}
}
else
{
if (!getText().toString().isEmpty())
{
int cursorPosition = Math.max(getSelectionStart(), 0);
CustomEditText.this._lastCursorPosition = cursorPosition - 1;
getText().replace(cursorPosition - 1, cursorPosition, text);
if (CustomEditText.this._lastComposingTextsList.size() > 0)
{
CustomEditText.this._lastComposingTextsList.remove(CustomEditText.this._lastComposingTextsList.size() - 1);
}
return true;
}
}
return super.commitText(text, newCursorPosition);
}
#Override
public boolean sendKeyEvent(KeyEvent event)
{
int keyCode = event.getKeyCode();
CustomEditText.this._lastComposingTextsList.clear();
if (keyCode > 60 && keyCode < 68 || !CustomEditText.this._isTextComposable || (CustomEditText.this._lastComposingTextsList != null && CustomEditText.this._lastComposingTextsList.size() == 0))
{
return super.sendKeyEvent(event);
}
else
return false;
}
#Override
public boolean finishComposingText()
{
if (CustomEditText.this._lastComposingTextsList != null && CustomEditText.this._lastComposingTextsList.size() > 0)
CustomEditText.this._lastComposingTextsList.clear();
CustomEditText.this._isComposing = true;
CustomEditText.this._commitText = true;
return super.finishComposingText();
}
#Override
public boolean commitCorrection(CorrectionInfo correctionInfo)
{
CustomEditText.this._commitText = false;
return super.commitCorrection(correctionInfo);
}
};
outAttrs.actionLabel = null;
outAttrs.inputType = this._inputType;
outAttrs.imeOptions = this._imeOptions;
return CustomEditText.this._inputConnection;
}
#Override
public boolean onKeyDown(int keyCode, KeyEvent keyEvent)
{
if (keyCode == KeyEvent.KEYCODE_DEL)
{
int cursorPosition = this.getSelectionEnd() - 1;
if (cursorPosition < 0)
{
removeAll();
}
}
return super.onKeyDown(keyCode, keyEvent);
}
#Override
public void setInputType(int type)
{
CustomEditText.this._isTextComposable = false;
this._inputType = type;
super.setInputType(type);
}
#Override
public void setImeOptions(int imeOptions)
{
this._imeOptions = imeOptions | EditorInfo.IME_FLAG_NO_EXTRACT_UI;
super.setImeOptions(this._imeOptions);
}
public void handleEditTextDeleteEvent()
{
int end = Math.max(getSelectionEnd(), 0);
if (end - 1 >= 0)
{
removeChar();
backSpaceProcessed();
}
else
{
removeAll();
}
}
private void removeAll()
{
int startSelection = this.getSelectionStart();
int endSelection = this.getSelectionEnd();
if (endSelection - startSelection > 0)
this.setText("");
else
nothingRemoved();
}
private void removeChar()
{
KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
super.onKeyDown(event.getKeyCode(), event);
}
public void nothingRemoved()
{
// Backspace didn't remove anything. It means, a cursor of the editText was in the first position. We can use this method, for example, to switch focus to a previous view
}
public void backSpaceProcessed()
{
// Backspace is properly processed
}
#Override
protected void onSelectionChanged(int selStart, int selEnd)
{
if (CustomEditText.this._isComposing)
{
int startSelection = this.getSelectionStart();
int endSelection = this.getSelectionEnd();
if (((CustomEditText.this._lastCursorPosition != selEnd && !CustomEditText.this._characterRemoved) || (!CustomEditText.this._characterRemoved && CustomEditText.this._lastCursorPosition != selEnd)) || Math.abs(CustomEditText.this._lastCursorPosition - selEnd) > 1 || Math.abs(endSelection - startSelection) > 1)
{
// clean autoprediction words
CustomEditText.this._lastComposingText = "";
CustomEditText.this._lastComposingTextsList.clear();
CustomEditText.super.setInputType(CustomEditText.this._inputType);
}
}
}
#Override
public void onFocusChange(View v, boolean hasFocus)
{
if (!hasFocus) {
CustomEditText.this._lastComposingText = "";
CustomEditText.this._lastComposingTextsList.clear();
}
}
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after)
{
int startSelection = getSelectionStart();
int endSelection = getSelectionEnd();
if (Math.abs(endSelection - startSelection) > 0)
{
Selection.setSelection(getText(), endSelection);
}
}
#Override
public void afterTextChanged(Editable s)
{
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count)
{
super.onTextChanged(s, start, before, count);
}
}
UPDATE:
I updated the code, because it was not working properly when Text Prediction is enabled on some devices like Samsung Galaxy S6 (thanks #Jonas that he informed about this issue in the comment below) and using InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS doesn't help in this case. I tested this solution on lots of devices, but still not sure whether it works properly for all. I hope I will get some reviews from you in case of any improper behaviour of the EditText.
I think you may find that you can intercept the key if you override the dispatchKeyEvent method of the appropriate view/activity (in my case, the main activity was fine).
For example I'm developing an app for a device which has hardware scroll keys, and I was surprised to discover the onKeyUp/onKeyDown methods never get called for them. Instead, by default the key press goes through a bunch of dispatchKeyEvents until it invokes a scroll method somewhere (in my case, bizarrely enough, one key press invokes a scroll methods on each of two separate scrollable views--how annoying).
What if you checked for like the decimal number for the backspace character?
I think its like '/r' (decimal number 7) or something, at least for ASCII.
EDIT:
I guess Android uses UTF-8, so this decimal number would be 8.
http://www.fileformat.info/info/unicode/char/0008/index.htm
Given the response by Umair you may consider applying a workaround here:
Capture a touch event that it's NOT a key event and happens around the lower-right part of the screen while the keyboard is shown.
How to get the Touch position in android?
Is there a way to tell if the soft-keyboard is shown?
Hope that helps
InputFilter called for backspace and if edittext is empty.
editText.setFilters(new InputFilter[]{new InputFilter() {
#Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
if(source.equals("")) {
//a backspace was entered
}
return source;
}
}});
This is old post and giving my suggestions in case somebody is in need of super quick hack/implementation.
The simplest work around I came out with is to implement TextWatcher too along with on OnKeyListener and in onTextChanged compare with the previous existing string whether it is reduced by one character.
The benefit of this is it works on any type of keyboard with no long coding process easily.
For instance my editText holds only one character, so I compared characterSequence if it is empty string, then by that we can acknowledge that Delete key is pressed.
Below is the code explaining the same:
#Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
if(charSequence.toString().equals("")) //Compare here for any change in existing string by single character with previous string
{
//Carry out your tasks here it comes in here when Delete Key is pressed.
}
}
Note: In this case my edittext contains only single character so I'm comparing charSequesnce with empty string(since pressing delete will make it empty), for your needs you need to modify it and compare(Like after pressing key substring is part of the original string) it with existing string. Hope it helps.

Categories

Resources