How to 'interrupt' an action from being performed in AccessibilityService? - android

What I am trying to do/What I have done: I am trying to make a very basic version of TalkBack for visually impaired users. I have made a simple accessibility service that reads the contentDescription of a button clicked by the user and reads it out loud.
#Override
public void onAccessibilityEvent(AccessibilityEvent event) {
// get the source node of the event
AccessibilityNodeInfo source = event.getSource();
if (source == null) {
return;
}
// Check if a button is clicked and speak out the content
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED
&& BUTTON_CLASS_NAME.equals(source.getClassName()) {
Log.d("Button clicked", source.getViewIdResourceName().toString());
convertTextToSpeech(source.getContentDescription().toString());
}
if (source != null)
source.recycle();
return;
}
The problem: But, this way the user can't listen to the button's description BEFORE actually performing the action that fires when the button is clicked. By the time the user listens to the description, the button has already been clicked and an action has been performed.
Question: How do I interrupt the action (eg: opening a new activity after clicking the button) from being performed so that the user can safely explore the current views on the screen with the feedback I provide without starting new activities or firing other actions?
Sort of like what happens in Talkback: the user single taps to hear the description, and double taps to perform the action. How does Talkback prevent an action from happening unless the user double taps? I have looked into TouchExplorationMode but I guess it is mostly used for gestures rather than clicks.

You are looking in the wrong place. Once onAccessibilityEvent receives an event, the action has already taken place. It is simply informing you, a click event has occurred. It is already much too late to stop it.
You do indeed want TouchExplorationMode. Here's a quick and dirty implementation of TalkBack's handling, and how it causes the UI to behave the way it does, without a lot of the extra junk and exception handling. I've only contained the portions of this that matter for this feature. There's of course a lot of other necessary scaffolding, but this would distract from the key elements.
Contents of serviceConfig.xml:
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagRequestTouchExplorationMode"
android:canRequestTouchExplorationMode="true"
/>
Partial Contents of A11yService.java
#Override
public void onAccessibilityEvent(AccessibilityEvent e) {
switch (e.getEventType()) {
case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: {
e.getSource().performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
}
}
}
TalkBack then intercepts gestures in onGesture, and for swipe right or swipe left, captures them, and does the same action (accessibility focus) just on the next or previous element in the AccessibilityNode traversal. Easy peasy!

Related

How to control what the home button on my android wearable device does when pressed?

I have a certain activity that begins when I tap my smart watch's screen. There is a timer and bunch of stuff that happens, but the process is crucial, so I am handling certain cases or things that might happen that would disturb the flow of things.
So basically, I want to prevent the home button of my watch to exit the app and go to the homescreen while my timer is running. I keep looking this up and most people say to override the onBackPressed method. But this was for the back button, and I I realized the button is a home button not a back button.
frameLayout.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN)
{
clicked = clicked + 1;
if (clicked == 2)
{
Toast.makeText(toolandmode.this, "Clicks:" + clicked, Toast.LENGTH_SHORT).show();
startTimer();
}
else if (clicked >= 4)
{
Toast.makeText(toolandmode.this, "Clicks:" + clicked, Toast.LENGTH_SHORT).show();
AlertMessage();
}
}
return true;
}
});
this is the main method I use
Just override the onBackPressed function.
#Override
public void onBackPressed ()
{
//Control the flow
}
Use third part library in case if it solves your problem.
Here is the link:
https://github.com/shaobin0604/Android-HomeKey-Locker
The general consensus is that you can't override the home button behavior on a Wear OS device, just like you can't override the home button on an Android phone. This is by design to prevent developers from preventing the user to leave an application. Even if there is a hacky way to do it, this is not officially supported and may stop working at any time in future OS versions. I highly suggest not doing it since it goes against the basic navigation model of the device.
More details, and some workarounds for common use cases where people think they need to override the home button can be found in this blog post.

How to show "up" / "back" button in Xamarin.Forms?

I am trying to work out how to show the "up" arrow in Xamarin.Forms without a pushing a page onto the stack. I.E. I just want to perform an action when the back button is pressed. I am completely stuck on this so any help would be appreciated.
I have tried creating a custom renderer which handles a view property called DisplayHomeAsBack. Which in the renderer calls the following:
FormsAppCompatActivity context = ((FormsAppCompatActivity)Forms.Context);
Android.Support.V7.App.ActionBar actionBar = context.SupportActionBar;
if (actionBar != null)
{
actionBar.SetDisplayHomeAsUpEnabled(element.DisplayHomeAsBack);
}
Unfortunately it seems this does absolutely nothing, even though all online tutorials and stackoverflow question for android suggest this method.
The plan is that I can then use the "OnBackButtonPressed" override in MasterDetailPage, which should allow me to perform this action. Unfortunately displaying the back button has been the larger hurdle so far!
Any idea of a better way to do this or how I can get the current mechanism to work?
EDIT
I have created a project and uploaded it to this question on the Xamarin support forums, if it helps.
http://forums.xamarin.com/discussion/comment/186330#Comment_186330
Sorry to keep you waiting so long!
Warning that I did not actually run this code and changed it from my own so I would be surprised if it worked perfectly without some changes.
So below should add a back button where there was not one before (so like when there is not really a page to go back to) and then we will add a custom action to perform when it gets pressed.
I would suggest you push a new page onto the stack without using animation so it is transparent to the user and also makes all of this much simpler, but if you absolutely do not want to do that, the below method should work.
MainActivity:
//Use this to subscribe to the event which will create the back button
public override bool OnCreateOptionsMenu(IMenu menu) {
if(menu != null && App.AppMasterPage != null) { //You will need this to make sure you are on your MasterDetailPage, just store a global reference to it in the App class or where ever
Xamarin.Forms.MessagingCenter.Unsubscribe<string>(this, "CreateBackButton");
Xamarin.Forms.MessagingCenter.Subscribe<string>(this, "CreateBackButton", stringWeWillNotUse => { //Use this to subscribe to the event that creates the back button, then when you want the back button to show you just run Xamarin.Forms.MessagingCenter.Send<string>(this, "CreateBackButton")
ActionBar.DisplayOptions = ActionBarDisplayOptions.ShowTitle | ActionBarDisplayOptions.ShowHome | ActionBarDisplayOptions.UseLogo | ActionBarDisplayOptions.HomeAsUp; //You may need to play with these options to get it working but the important one is 'HomeAsUp' which should add the back button
});
} else {
Xamarin.Forms.MessagingCenter.Unsubscribe<string>(this, "CreateBackButton");
}
return base.OnCreateOptionsMenu(menu);
}
Now the next step is do do a custom action when it is pressed. I think you can either override OnBackPressed() or OnOptionsItemSelected() in MainActivity or maybe you can override the MasterDetailPage method. I am not sure.
Which ever one works for you, inside of that override, I would simply check to see if you are on your App.AppMasterPage like we did above, and if so, send a MessagingCenter message which your App.AppMasterPage has already subscribed to in order for it to handle the custom action.
If you get stuck let me know!
I know it sounds like a bit of a hack, but the best "solution" I have found so far is to add a page behind the current page (behind the root) so it is not visible. Then when the user presses the back button, handle it by removing that page.

Do Button events in Android really have to be explicitly disabled?

So I'm new to Android and have this wee app that has a variety of Buttons. The buttons do a variety of things, but of particular interest are the buttons that intent to another activity.
Because as I'm happily programming and testing along, I discover that I can double- and sometimes triple-tap these buttons.
I look for methods on the Button object that will allow me to specify the number of clicks that the button is allowed or whether the button should be (even briefly) disabled after a click. I find nothing of the sort.
Incredulous, I begin googling for a high-level discussion of this strange behavior. I find no interesting discussions, just suggestions about how to handle the issue on every single button in my app.
With a heavy sigh, I surrender to the time demands of my project, and add private variables to my activities (no static locals in Java. crap.), which the click-handling method uses to tell whether it's already busy handling a button click.
But still I wonder. Do Button events in Android really have to be explicitly disabled?
Edit: I'm looking for an answer of the form: "Yes (or no), and I know they have to be explicitly disabled because X".
The platform can't assume you only want to allow the button to be clicked once, or how frequently you should be able to click it. Just add logic to disable the button once you've clicked it, e.g.:
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick (View v) {
v.setEnabled(false);
//Do other stuf
}
});
You can use droidQuery to set a button or a set of buttons to handle the first click event, then not any future click events. For example:
$.with(button).one("click", new Function() {
#Override
public void invoke($ d, Object... params) {
//Do stuff - this will only happen on the first click event
}
});

How to tell the activity has been covered by the notification area?

Usually, Android calls onPause on your Activity when it starts being obscured or hidden, followed by onStop when it's no longer shown at all. In my game, I pause the game in onPause, so the user doesn't lose the game while looking elsewhere.
However, when the user drags down the notification bar, it covers my Activity, but neither onPause nor onStop are called. This doesn't seem to be mentioned in the documentation. The game ticks away in the background with nobody looking at it. Is there some other way to tell my Activity has been obscured when this happens, so I can pause the game before the user loses? I can't find anything at all about this on the Android Developers site.
The onWindowFocusChanged(boolean hasFocus) of Activity should serve the required purpose. It is called with false when the notification area is dragged down, and with true once the area is dragged back up. The corresponding Android documentation states that this method "is the best indicator of whether this activity is visible to the user". It also explicitly states that the callback is triggered when the "status bar notification panel" is shown.
It is important to note that this method is also called in other situations. A good example is the display of an AlertDialog. onWindowFocusChanged is even called when the activity itself shows an AlertDialog. This might require consideration, depending on if your game uses AlertDialogs or something else which causes a focus change.
In a scenario similar to the one described in this question, we've used the onWindowFocusChanged method successfully, e.g. on a Nexus 5 with Android 4.4, or a Sony Xperia Tablet with Android 4.1.
Since the StatusBarManager isn't part of the official API, I find it unlikely that there is a way to detect it. Even using reflection, none of the statusbar-classes seem to have a hook for listeners.
If it is feasible, you could deactivate the statusbar. Otherwise, I think you are out of luck :(
Interacting with the status bar has 2 cases:
Case 1: If your activity is already hiding the status bar, and the user pulls down the status bar area without showing the notification: this can be handled by registering OnSystemUiVisibilityChangeListener listener to get notified of system UI visibility changes
boolean mStatusBarShown;
View decorView = getWindow().getDecorView();
decorView.setOnSystemUiVisibilityChangeListener
(new View.OnSystemUiVisibilityChangeListener() {
#Override
public void onSystemUiVisibilityChange(int visibility) {
// Note that system bars will only be "visible" if none of the
// LOW_PROFILE, HIDE_NAVIGATION, or FULLSCREEN flags are set.
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
// TODO: The system bars are visible. Make any desired
// adjustments to your UI, such as showing the action bar or
// other navigational controls.
mStatusBarShown = true;
} else {
// TODO: The system bars are NOT visible. Make any desired
// adjustments to your UI, such as hiding the action bar or
// other navigational controls.
mStatusBarShown = false;
}
}
});
Case 2: If the status bar is already shown to the user, and the user pulls it down to show the notification area; to detect that, you need to override onWindowFocusChanged(boolean hasFocus) in the
activity, where hasFocus parameter value will be 'false' in case the user pulls the status bar down, and when the user pushes the status bar up again; then the onWindowFocusChanged will be invoked again but this time hasFocus will be true
#Override
public void onWindowFocusChanged(boolean hasFocus) {
// handle when the user pull down the notification bar where
// (hasFocus will ='false') & if the user pushed the
// notification bar back to the top, then (hasFocus will ='true')
if (!hasFocus) {
Log.i("Tag", "Notification bar is pulled down");
} else {
Log.i("Tag", "Notification bar is pushed up");
}
super.onWindowFocusChanged(hasFocus);
}
Check this link for more info.

Understanding setOnClickListener, View.OnClickListener(), onClick(View v) as it relates to the Model-View-Controller concept

I'm clear on how the id of my xml button gets cast as a Button and ultimately to the sayIt field, however...
Button sayIt = (Button) findViewById(R.id.sayit);
...is it setOnClickListener that "registers" with the Controller to be notified when the button is clicked? If so, then is View.OnClickListener() and its onClick(View v) method where Controller first tells my code, hey I've been clicked and this is what gets kicked up the food chain?
sayIt.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// Does something cool
}
});
For most use cases, yes, View.OnClickListener#onClick() is the standard "do something when this thing is clicked" idiom. I say most, because the actual underlying implementation is a bit more involved than that, and there are different ways in which the touch event travels up the view hierarchy, before being detected as a "click" event, and propagating back down the hierarchy as a click event -- but unless you're implementing custom views, and need to do custom touch-based tracking, you generally don't need to worry about those events.
For example, if you set a View.OnTouchListener on the view, you get every touch event (down, motion, up, and in supported devices, even multiple pointers). In the onTouchEvent() handler, if you return true, it tells the view "I was interested in this motion event, and I have consumed the event; therefore pretend that the event never happened and stop propagating/processing it" -- by doing that, you would actually interfere with the standard OnClickListener click event detection.
But in most cases, if you want something to happen because you clicked on a Button (surprise, surprise :), View.OnClickListener and View#setOnClickListener() are what you want.

Categories

Resources