Accessibility Event occurs only once after enabling the Accessibility Service - android

I've just started coding my app which uses Accessibility Service. I'll explain my problem in detail.
Below is my onServiceConnected method of MyAccessibilityService class
protected void onServiceConnected() {
super.onServiceConnected();
AccessibilityServiceInfo info = getServiceInfo();
info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED | AccessibilityEvent.WINDOWS_CHANGE_ADDED;
info.packageNames = new String[]
{THIRD_PARTY_APP_PACKAGE};
info.notificationTimeout = 100;
this.setServiceInfo(info);
}
The app is detecting events in onAccessibilityEvent() method
public void onAccessibilityEvent(AccessibilityEvent event) {
AccessibilityNodeInfo source = event.getSource();
if (source == null) {
return;
}
Toast.makeText(this, "Event Occured", Toast.LENGTH_SHORT).show();
}
Now when I open the third party app, I'm getting the Toast "Event occured". Now I close the app and when I open it again, the method is not called and I don't get any Toast. To make it working again, I have to disable the accessibility service of my app in my phone's Settings and again enable it.
I know I'm missing something and my only question is what should be the additional part of code or what modifications I need in order to detect the event every time I open the third party app?

Have you tried getting rid of the notification timeout? You probably don't need it, and it isn't the best-tested API.

Related

NFCTagDiscovered Intent not fired when phone is already in contact with tag and then listening is invoked

I'm working on an NFC based app developed in Xamarin for Android.
The app has a 'Connect' button and the NFC scanning process starts only when that button
is tapped.
With proper intents configured, whenever an NFC tag is detected, HandleNewIntent() method gets called and NFC read procedure is followed.
internal void HandleNewIntent(Intent intent)
{
try
{
if (intent.Action == NfcAdapter.ActionTagDiscovered || intent.Action == NfcAdapter.ActionNdefDiscovered)
{
_currentTag = intent.GetParcelableExtra(NfcAdapter.ExtraTag) as Tag;
if (_currentTag != null)
{
Task.Run(() => { ReadDataFromTag(_currentTag); });
}
}
}
catch (Exception ex)
{
//Display error
}
}
In normal cases this works fine. However, if the phone is kept in contact with the NFC tag and then the 'Connect' button is tapped on, then the TagDiscovered intent never gets fired. User has to take the phone away, bring it back in contact with the NFC tag and then only the event gets fired.
I observed the same behaviour with generic NFC apps on play store, and on 2 different Android phones.
Looks like Android keeps the NFC of phone tied up when in contact with NFC tag because of which the intents are not detected. Is there anything to be done to release such links (if any) before initiating new NFC connection?
All NFC is handled by the System NFC service and System App and is normally triggered by a Tag coming in to range and then an Intent being delivered to the right app immediately, therefore the system NFC service has already delivered the Intent to another App before you App has had the button pressed.
You don't show all the code that you do to setup your Apps' NFC configuration but as you are talking about Intents you are probably using the old and unreliable enableForegroundDispatch API which has a number of issues.
While it is more normal to enable foreground NFC detection as soon as you App is resumed and then process the Tag on immediate detection, it is possible to store the Tag object until a button is pressed.
I suggest that you use the better and newer enableReaderMode API that has less of a problem because it is a more direct interface to the NFC hardware and thus you use case seems to work using the code below.
public class MainActivity extends AppCompatActivity implements NfcAdapter.ReaderCallback{
private NfcAdapter mNfcAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void Button(View view) {
Log.v("TAG", "DetailedScoresActivity:onTagDiscovered:Start");
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
if(mNfcAdapter!= null) {
Bundle options = new Bundle();
options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250);
mNfcAdapter.enableReaderMode(this,
this,
NfcAdapter.FLAG_READER_NFC_A |
NfcAdapter.FLAG_READER_NFC_B |
NfcAdapter.FLAG_READER_NFC_F |
NfcAdapter.FLAG_READER_NFC_V |
NfcAdapter.FLAG_READER_NFC_BARCODE |
NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK |
NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS,
options);
}
}
public void onTagDiscovered(Tag tag) {
Log.v("TAG", "onTagDiscovered:Start");
}
#Override
protected void onPause() {
super.onPause();
if(mNfcAdapter!= null)
mNfcAdapter.disableReaderMode(this);
}
}
For me you can place a Tag against the phone and then start the App and as soon as the Button is clicked and `enableReaderMode` is enabled then `onTagDiscovered` is triggered with the Tag object.

How to restrict NFC communication to only one screen in an Android app

I'm working on a large app that wants to add NFC communication. This app has many manifests, one for the main shell app and one each for the many separate modules. Initially I registered the NFC service on the main manifest and it works fine. The issue is that the NFC service now triggers anytime the app is open and not when the user is on a specific screen.
 
So I wanted to ask, is there a way to have the NFC service register/un-register as a user navigates to/away from a specific screen? Or just a way to make it so that NFC communication is restricted to a specific screen? The size of this app is really tripping me up, I appreciate any help people can offer.
So it is not usual just to use manifest Intent filters for NFC operations though possible, usually one of the foreground API's is used instead.
The following solution works for API 19 upwards because it uses the enableReaderMode NFC API's because this is better and enables more control and is more reliable.
The basic concept is that in all Activities you claim to handle all NFC Tag types silently when the Activity is in the foreground, then for the Activity where you want the NFC reading to happen you actually do something when you are notified a Tag has been presented.
In your manifest you only have the following:
<uses-permission android:name="android.permission.NFC" />
and optionally
<uses-permission android:name="android.permission.VIBRATE" />
for user feedback.
You don't have any of the NFC Intent filters.
The in every Activity you have the following boiler plate code:-
public class MainActivity extends AppCompatActivity implements NfcAdapter.ReaderCallback{
private NfcAdapter mNfcAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
// Rest of onCreate
}
#Override
protected void onResume() {
super.onResume();
if(mNfcAdapter!= null) {
Bundle options = new Bundle();
options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250);
mNfcAdapter.enableReaderMode(this, this,
NfcAdapter.FLAG_READER_NFC_A |
NfcAdapter.FLAG_READER_NFC_B |
NfcAdapter.FLAG_READER_NFC_F |
NfcAdapter.FLAG_READER_NFC_V |
NfcAdapter.FLAG_READER_NFC_BARCODE |
NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK |
NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS,
options);
// enabling FLAG_READER_SKIP_NDEF_CHECK is optional
// depending on if you are reading NDef data or not.
}
}
#Override
protected void onPause() {
super.onPause();
if(mNfcAdapter!= null)
mNfcAdapter.disableReaderMode(this);
}
public void onTagDiscovered(Tag tag){
// Do nothing when a NFC tag is detected
}
}
In Activities where you want to handle the NFC Tag change onTagDiscovered method to do something with the Tag data.
e.g. for read Ndef data from a Tag onTagDiscovered could look like:-
// This method is run in another thread when a card is discovered
// !!!! This method cannot cannot direct interact with the UI Thread
// Use `runOnUiThread` method to change the UI from this method
public void onTagDiscovered(Tag tag) {
// Read and or write to Tag here to the appropriate Tag Technology type class
// in this example the card should be an Ndef Technology Type
Ndef mNdef = Ndef.get(tag);
// Check that it is an Ndef capable card
if (mNdef!= null) {
// If we want to read
// As we did not turn on the NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
// We can get the cached Ndef message the system read for us.
NdefMessage mNdefMessage = mNdef.getCachedNdefMessage();
// Now your own code to process the Ndef message
// Finally feedback to the user that the NFC read was a success
// Make a Sound
try {
Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
Ringtone r = RingtoneManager.getRingtone(getApplicationContext(),
notification);
r.play();
} catch (Exception e) {
// Some error playing sound
}
// Optionally Vibrate as well
Vibrator v = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
if (v != null) {
v.vibrate(500);
}
}
}
You can optionally you can also decide on when the NFC is handled when a particular Fragment is shown in your Activity or when a dialog is shown or any other state in your Activity by putting conditional clauses in your onTagDiscovered method.
e.g. Some pseudo code
public void onTagDiscovered(Tag tag) {
if correct fragment is being shown {
Handle NFC reading and feedback to user the NFC has been read
} else {
Do nothing
}
}
or
public void onTagDiscovered(Tag tag) {
if some condition is true {
Handle NFC reading and feedback to user the NFC has been read
} else {
Do nothing
}
}
Normally with Intent filter and enableForegroundDisplatch method of working with NFC the System service Handles the NFC event and notifies the user that an NFC card has been presented and then it interrupts you App by restarting the current Activity or starting another Activity from you App.
With enableReadmode and this method the NFC System Service is silent, because the Event is handled in another thread in your App, what the Activity currently is doing is not interrupted and you get full control about when you handle the NFC event or to silently ignore it.
The problem with any register/de-register approach is that the System NFC Service would always make a sound and possibly launch another App or display a default NFC content screen when you were de-registered.

AccessibilityService setServiceInfo method's changes don't seem to take effect outside onServiceConnected

Quoting the docs for setServiceInfo
"You can call this method any time but the info will be picked up after the system has bound to this service and when this method is called thereafter."
Accessibility has been enabled for my app and I have set the serviceInfo inside the onServiceConnected method and I am receiving events for those apps and all is well.
Now ,the problem is I'm trying to modify the package list by calling setServiceInfo outside the onServiceConnected method, but the changes are not taking effect( i.e i'm still receiving events from the packages in the package list specified earlier which are absent in the modified package list).
#Override
protected void onServiceConnected() {
AccessibilityServiceInfo accessibilityServiceInfo = new AccessibilityServiceInfo();
accessibilityServiceInfo.packageNames = new String[]{"packageA","packageB"};
accessibilityServiceInfo.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
| AccessibilityEvent.TYPE_VIEW_SCROLLED | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
| AccessibilityEvent.TYPE_VIEW_FOCUSED | AccessibilityEvent.TYPE_VIEW_SELECTED
| AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
accessibilityServiceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
accessibilityServiceInfo.notificationTimeout = 100;
accessibilityServiceInfo.flags = (AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS | accessibilityServiceInfo.flags);
setServiceInfo(accessibilityServiceInfo);
}
#Override
public void onAccessibilityEvent(final AccessibilityEvent event) {
AccessibilityServiceInfo serviceInfo = getServiceInfo();
serviceInfo.packageNames = new String[]{"packageA"};
setServiceInfo(serviceInfo);
}
accessibility_service_config.xml
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:canRetrieveWindowContent="true"
android:description="#string/accessibility_service_description"
/>
So, the question is how do I add/remove packages inside the Accessibility Service depending on the Accessibility events I receive later?
You can't exclude package names by adding them to that list.
serviceInfo.packageNames = null
Get events for all packages.
serviceInfo.packageNames = {"some.package"}
Get events ONLY for some.package. This list is not exclusions, it's a list of things to only include.
Now, if you could keep a list of all packages that you want to get events for, you could do this. Setting service info dynamically is just dandy! But, TBH, you're making this way too difficult and not using this API the way its intended. If you want to exclude only a few things, it is very ill advised to save a list of every other package name in the world, to get their events. It seems to me like you need your own logic filter package names. I would try this instead:
#Override
public void onAccessibilityEvent(AccessibilityEvent e) {
//Do the filtering yourself.
ArrayList<String> includedPackageNames = youlist;
if (!includedPackageNames.contains(e.getPackageName())) return;
//Do the rest of your stuff here.
}
EDIT:
You have some issues with your service info stuff. When you do this line:
#Override
protected void onServiceConnected() {
AccessibilityServiceInfo accessibilityServiceInfo = new AccessibilityServiceInfo(); //This one!
...
}
You overwrite all of the information in your service_config.xml file as well as any default values that you DID NOT set in there. You should let service_config.xml handle your static initialization and only make dynamic changes by modifying the fetched serviceInfo object. OR, at the very least, fetch the one that has been built from your service config.
Change that line to this:
//Get the object that has been initialized with settings from your service_config.xml file
AccessibilityServiceInfo accessibilityServiceInfo = getServiceInfo();
OR, better yet, delete your entire onServiceConnected function, and do it all statically in your service_config.xml file.
Encountered the same issue recently. After looking into source code (SDK 8.1), it seems the API description gives the wrong expectation that set packageNames to some app list then set it null would make the service receive all events again.
The reason is in two methods below. In #1 when you it calls setServiceInfo(info) where the info.mPackageNames is null, here it just skips it and never set service.mPackageNames to null. In #2, when mPackageNames already includes the apps you added before. So later mPackageNames.isEmpty() will always be false.
http://androidxref.com/8.1.0_r33/xref/frameworks/base/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java#mPackageNames
1
line 2786 - 2788
if (packageNames != null) {
mPackageNames.addAll(Arrays.asList(packageNames));
}
2
1428 Set<String> packageNames = service.mPackageNames;
1429 String packageName = (event.getPackageName() != null)
1430 ? event.getPackageName().toString() : null;
1431
1432 return (packageNames.isEmpty() || packageNames.contains(packageName));

getAccessibilityButtonController Android Accessibility Service

I have been looking at the new methods available for Accessibility in Android O. I ran across this new method called getAccessibilityButtonController, I am unsure precisely what it does and an intended use. I know that in Android O there is a navigation button that can be used for an accessibility service. Does this accessibility button only launch the accessibility service, or could it have other functionality within the service such as to do specific tasks? I am curious possible uses for the accessibility and the getAccessibilityButtonController methods. Thank you for your time.
It can do pretty much anything you want it to. From the android accessibility doc, the button allows you to register a callback that has an onClicked method. If you enable the button and provide said callback you can execute whatever you'd like in the context of that callback.
Edit: The android documentation has been updated so the following should no longer be necessary.
Note that if you read the doc there's currently an example that has a call to getAccessibilityButtonController() within onCreate(). This is incorrect because the controller isn't valid until onServiceConnected is called. I've modified the example below to show something that should work.
private AccessibilityButtonController mAccessibilityButtonController;
private AccessibilityButtonController
.AccessibilityButtonCallback mAccessibilityButtonCallback;
private boolean mIsAccessibilityButtonAvailable;
#Override
protected void onServiceConnected() {
mAccessibilityButtonController = getAccessibilityButtonController();
mIsAccessibilityButtonAvailable =
mAccessibilityButtonController.isAccessibilityButtonAvailable();
if (!mIsAccessibilityButtonAvailable) {
return;
}
AccessibilityServiceInfo serviceInfo = getServiceInfo();
serviceInfo.flags
|= AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
setServiceInfo(serviceInfo);
mAccessibilityButtonCallback =
new AccessibilityButtonController.AccessibilityButtonCallback() {
#Override
public void onClicked(AccessibilityButtonController controller) {
Log.d("MY_APP_TAG", "Accessibility button pressed!");
// Add custom logic for a service to react to the
// accessibility button being pressed.
}
#Override
public void onAvailabilityChanged(
AccessibilityButtonController controller, boolean available) {
if (controller.equals(mAccessibilityButtonController)) {
mIsAccessibilityButtonAvailable = available;
}
}
};
if (mAccessibilityButtonCallback != null) {
mAccessibilityButtonController.registerAccessibilityButtonCallback(
mAccessibilityButtonCallback, null);
}
}

Detect a new Android notification

In the Android app that I'm working on, I'd like to be able to detect when a new status bar notification appears, regardless of if it was caused by my app. To be more specific, I want to count the number of notifications in a given time frame.
Is this even possible, and if so, how?
Actually, it is possible, I use it in my app.
For Android 4.2 and below:
You need to register an AccessibilityService and make sure the user enables the service.
Example for a service:
public class InstantMessenger extends AccessibilityService {
#Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) {
//Do something, eg getting packagename
final String packagename = String.valueOf(event.getPackageName());
}
}
#Override
protected void onServiceConnected() {
if (isInit) {
return;
}
AccessibilityServiceInfo info = new AccessibilityServiceInfo();
info.eventTypes = AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED;
info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;
setServiceInfo(info);
isInit = true;
}
#Override
public void onInterrupt() {
isInit = false;
}
}
Example for checking if your Service is activated
For Android 4.3 and above:
Use the Notification Listener API
The new Notification Listener API in Android 4.3 enables you to do this.
With this there is less need for the accessibility hack. It also allows you to dismiss notifications.

Categories

Resources