How to monitor all input from a Bluetooth HID gadget - android

I'm trying to implement an app that (mis-) uses a Bluetooth camera shutter release gadget for a completely different purpose. Here's the gadget in question:
http://www.amazon.co.uk/iPazzPort-Bluetooth-Shutter-Android-Smartphones/dp/B00MRTFB4M
As far as I can determine this uses Bluetooth v3 and is an HID device. It apparently fires the camera app shutter by simulating "volume up" (or maybe "volume down"?). Anyway, it does seem to work quite well, although sometimes you have to press the button twice - I think that maybe the first press reestablishes Bluetooth connection and the second, and subsequent, presses then just work.
I've tested it with two different devices running Android 2.3. I do want to be backwards-compatible to that version of Android.
What I want to do is to monitor all input from this device somehow, so my app can detect when the button has been pressed and then do what it wants to use the device for. (It's a kind of panic alarm system so you can press the button to indicate you need help.)
I don't want to get involved in trying to communicate with the device via Bluetooth. Android is already doing that, and it's working, and what I've read about Bluetooth and the HID protocol makes me want to avoid it if at all possible.)
I've tried overriding onKeyDown() and onKeyUp() and dispatchKeyEvent(). Sometimes they get called, sometimes they don't. And when they get called I'm seeing unexpected keyCodes like 66 (Enter) and 8 ("1").
What I'm asking is, is there some way to monitor all input from this Bluetooth HID device, without having to get involved in Bluetooth HID protocol support?

I never found a real answer to my question as such, but fortunately I found a work-around.
This particular Bluetooth gadget always connects to the paired device, sends some text, and then disconnects. So what I'm doing is creating a BroadcastReceiver to get Bluetooth connection (and disconnect) events, and using that to activate the alarm.
// Class used to receive Bluetooth connection and disconnect events. This checks the action is as
// expected (probably unnecessary) and that a request type has been selected, and sends the
// activation message to OutBack Server if so. (Monitoring the disconnect events is probably
// unnecessary, but is done just in case that saves a situation where a connection has been
// missed, or something.)
public class BluetoothReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context androidContext, Intent androidIntent) {
String actionOrNull = androidIntent.getAction();
if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(actionOrNull) ||
BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(actionOrNull)) {
Log.d(TAG, "BluetoothReceiver.onReceive() " + actionOrNull);
if (_btnActivate.isEnabled()) {
sendRequestActivationToServer();
}
}
}
}
...
// Reference to the object used to monitor Bluetooth connections and disconnections
private BluetoothReceiver _bluetoothReceiver = null;
...
// Last lifecycle method called before fragment becomes active. This is apparently the
// recommended place to register a receiver object, and is used to register a receiver object to
// monitor Bluetooth connections and disconnections.
#Override
public void onResume() {
super.onResume();
_bluetoothReceiver = new BluetoothReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
getActivity().registerReceiver(_bluetoothReceiver, intentFilter);
}
...
// First lifecycle method called when a fragment is on its way to being paused or destroyed. This
// is apparently the recommended place to unregister a receiver object, and is used to unregister
// the receiver object that monitors Bluetooth connections and disconnections.
#Override
public void onPause() {
super.onPause();
if (_bluetoothReceiver != null) {
getActivity().unregisterReceiver(_bluetoothReceiver);
_bluetoothReceiver = null;
}
}
This was inspired by this question and answer: How to receive intents for Bluetooth devices working?

Related

Android Background Bluetooth Processing: What's the best approach?

I've just develop an Android app (minSdkVersion 23/ targetSdkVersion 29) that can connect to a BluetoothLE device to obtain data periodically.
Now, in the MainActivity (not the first activity), I do the following registering the broadcastReciever:
public class StatusActivity extends AppCompatActivity {
BleService mBleService;
BleScanCallback mScanCallback;
BroadcastReceiver mBroadcastReceiver;
#Override
protected void onCreate(Bundle savedInstanceState) {
mBroadcastReceiver = new LibBleBroadcastReceiver(this);
IntentFilter intentFilter = new IntentFilter()
intentFilter.addAction(BleService.ACTION_GATT_CONNECTED);
intentFilter.addAction(BleService.ACTION_GATT_DISCONNECTED);
intentFilter.addAction(BleService.ACTION_GATT_SERVICES_DISCOVERED);
intentFilter.addAction(BleService.ACTION_DATA_AVAILABLE);
intentFilter.addAction(BleService.ACTION_DID_WRITE_CHARACTERISTIC);
intentFilter.addAction(BleService.ACTION_DID_FAIL);
registerReceiver(mBroadcastReceiver, intentFilter);
mScanCallback = new LibBleScanCallback(this);
intent = new Intent(this, BleService.class);
connection = new LibBleServiceConnection(this);
startService(intent);
if (!bindService(intent, connection, Context.BIND_AUTO_CREATE)) {
throw new IllegalStateException("bindService not successful");
}
}
...
public void onDeviceDiscovered(String device_address){
device_connected.activateNotifications(mBleService, connected_device);
scheduleTaskExecutor.scheduleAtFixedRate(new Runnable() {
public void run() {
device_connected.requestTemperature(mBleService, connected_device);
}
}, 0, 30, TimeUnit.MINUTES);
}
...
}
In the AndroidManifest.xml it's declared the BleService:
<service android:name=".bluetooth.BleService" android:enabled="true" />
And then, the user select a device (from a bluetooth scan) and connect to it to obtain data from BLE device.
Once is connected and discovered (services and characteristics), I schedule a task every 30 minutes to obtain data from the device.
All the callbacks that are executed when the device is connected/discovered/dataRecieved are in the StatusActivity not in the BleService (intent) to make changes in the UI (although in the background it would not be necessary).
In the other hand, I have to mantain always a bakground process too, because my app, starts a BLE Advertising to make the phone an LE Device, so always have to be "powered on" to make others devices find the phone.
The problem is that when I put the app in Background or I kill the app this schedule doesn't execute or execute a few times until the process is killed by android.
What would be the best approach to execute this service in background mode? Considering that if the app is killed, the device (I think) will be disconnected so I should connect another time to the device (I save the MAC address to reconnect so it's not a problem) and execute the requestData method? WorkManager/JobScheduler/AlarmManager/ForegroundService?
Could someone help me to know how to implement and understand the Background lifecycle and how to access all data I need to manage in background?
Thank you!
The only working solution for working in Background is ForegroundService, other ones will be destroyed when your device will enter Doze mode. You can find more detailed information in this article, it describes all the obstacles in background working while scanning BLE devices.

Accepting a Call via Bluetooth Headset

i am working on a VoIP-Android-App. I would like to accept and decline Calls via a connnected Bluetooth Headset in an Activity.
What I have tried so far:
Using a Media Session to receive Media Button clicks.
Problem: If we start BluetoothSCO we do not receive any Media Button clicks. If we do not start BluetoothSCO we do receive Media Button clicks but we cannot differentiate long and short button clicks because downtime is always 0, the keycode is always KEYCODE_MEDIA_PLAY and the ACTION_DOWN is immediately followed by ACTION_UP. Those problems only occur if we are connected via Bluetooth. If we are connnected over a cable Headset we do get the appropriate keycodes (KEYCODE_HEADSETHOOK) and the downtime is not 0.
Using a BroadcastReceiver to listen for Bluetooth SCO connection changes.
private val scoReceiver = object : BroadcastReceiver() {
fun onReceive(context: Context, intent: Intent) {
val state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1)
val previousState = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_PREVIOUS_STATE, -1)
if (state == AudioManager.SCO_AUDIO_STATE_DISCONNECTED && previousState == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
Log.e(TAG, "SCO Disconnected")
hangupCall()
}
}
}
protected fun onStart() {
super.onStart()
val intentFilter = IntentFilter()
intentFilter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
registerReceiver(scoReceiver, intentFilter)
}
With this approach i can detect when the user wants to hang up the call, for example a long press on the bluetooth headset because this triggers the SCO to disconnect.
Problem: We can not detect if the user wants to accept an incoming call.
Using dispatchKeyEvent, onKeyDown and onKeyUp.
Problem: They never get called at all.
Does anyone has any advice or a best practice how to correctly handle bluetooth headsets? Any help is very appreciated. Thanks in advance!
During normal and virtual voice call (including ringing) all events of Bluetooth headset unit buttons are processed by Bluetooth Headset Service internaly and not broadcasted as button events. Bluetooth Headset Service redirects these events into Telecom framework (answer/hangupCall).
These events are handled internally in HeadsetStateMachine (under packages/apps/Bluetooth).
These events are forwarded to IBluetoothHeadsetPhone interface. The single application to which all the events are forwarded is defined at run-time by following binding code in HeadsetStateMachine.java. This is to allow phone manufacturers to forward them to custom phone application instead of default one in cases where default one is not used.
Intent intent = new Intent(IBluetoothHeadsetPhone.class.getName());
intent.setComponent(intent.resolveSystemService(context.getPackageManager(), 0));
if (intent.getComponent() == null || !context.bindService(intent, mConnection, 0)) {
Log.e(TAG, "Could not bind to Bluetooth Headset Phone Service");
}
To make the events get forwarded to your application instead of default phone application you would have to modify aosp code.
You would need to intercept the events at one of HeadsetStateMachine , BluetoothHeadsetPhone proxy or the phone application.
Unfortunately what you are looking for is currently not possible without modifying aosp code. Some headsets like Plantronics have custom BT events which are forwarded to all applications - some of the existing VoIP applications support these custom intents to support at-least answering calls for some of the headsets.
You should use android Telecom API and implement android.telecom.ConnectionService and android.telecom.Connection where you should override onAnswer() callback which will be called when you try to answer a call via bluetooth headset.
For more details read docs - https://developer.android.com/guide/topics/connectivity/telecom/selfManaged

AccessibilityService - performGlobalAction not working in own app

I'm trying to send a system back press event via the AccessibilityService and this works fine, but only if I'm not in my own app.
I'm always getting true from performGlobalAction no matter if I'm in my own app or not, but I only see that the event really is executed if I'm not in my own app but in any other one (in the sense of that the previous activity is shown or similar)
Any ideas why this happens? My app is a sidebar app with an overlay drawn on top in the WindowManager and everything is working (AccessibilityService is running and is handling my custom events and the service always returns success messages for my events, but my own app does not react to the back button event).
My service looks like following:
public class MyAccessibilityService extends AccessibilityService {
public static void sendBackIntent(Context context) {
Intent intent = new Intent(context, MyAccessibilityService.class);
intent.putExtra("action", GLOBAL_ACTION_BACK);
context.startService(intent);
}
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
Bundle extras = intent.getExtras();
Integer action = null;
if (extras != null) {
action = extras.getInt("action");
}
if (action != null) {
switch (action) {
case GLOBAL_ACTION_BACK:
boolean result = performGlobalAction(action);
L.d("Action %d executed: %b", action, result);
break;
default:
L.e("Unhandled action %d", action);
break;
}
}
return super.onStartCommand(intent, flags, startId);
}
#Override
public void onAccessibilityEvent(AccessibilityEvent event) {
}
#Override
public void onInterrupt() {
}
}
Edit
To make this clear:
I do NOT start this service via MyAccessibilityService.sendBackIntent(context), I send the intent like following: if (isAccessibilityserviceRunning) MyAccessibilityService.sendBackIntent(context)
I start my service via the system service menu by simply enabling it there and let the system start it automatically afterwards
I've setup everything for the AccessibilityService in an accessibilityservice.xml and use this to define my services settings and this is working perfectly fine as well, all events I want to receive are received reliably and correct
EDIT 2
Seems like in my case my overlay is still stealing the focus making it focusable and not has timing problems that sometimes make problems. Still, my solution can be improved by using BroadcastReceiver to communicate with the service, as the startService call is not safe as discussed in the accepted answer
It strikes me that you're doing some very strange things. It appears that you're treating your AccessibilityService as a normal Service. The part of this that suggests this is your implementation of the following to methods:
public static void sendBackIntent(Context context);
#Override
public int onStartCommand(Intent intent, int flags, int startId);
Just by the signatures of these two methods and your calling of
context.startService(intent);
Within your static method, I can tell that you don't understand AccessibilityServices and how they are supposed to perform their jobs. You cannot start your accessibility service, nor interact with it, in the way that you are attempting. Certainly you can use Accessibility Services to perform global actions, but they won't do so accurately and globally, unless you launch them correctly, from the Accessibility Services menu (you know the one where TalkBack shows up).
Your code essentially, isn't running within the Context you think it's running in. So, it runs, and does things. But, AccessibilityServices and their respective power, is in their ability to attach globally to the Operating System. The android API's won't bind an AccessibilityService properly, when you attempt to launch your service with:
context.startService(intent);
You have to launch your Accessibility Service from the Accessibility Services Settings menu.
Even if your service is already launched such a call is unsafe! There's no guarantee your users are going to start the service prior to opening your Activity. Once you have called context.startService and attempted to start your AccessibilityService in this way, it will prevent the Accessibility Settings Menu from starting your service and binding to the OS properly. In fact, once in this situation a user would have to: Turn off the Switch for your service in the Accessibility Settings Menu, force stop (perhaps even uninstall) your application, restart their device, start your service and THEN start your activity, in order for the proper behavior to be achieved.
If you don't do so, it will not bind to the OS properly and its behavior is undefined. Right now, you've essentially created a hack in the OS and are running up against said undefined behavior, that could vary WIDELY across version, manufacturer, etc, because it's behavior isn't covered in the AOSP integration tests.
In fact, you explicitly CANNOT launch Accessibility Services using the context.startService() call. This is a very important security feature of Android, as Accessibility Services can gain access to screen content, and users need fine grain control over the providers and applications they allow this access. So, while you may be getting SOME behavior, it is undefined and dangerous behavior. What you want is something like the following:
With the following service config XML:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="#string/accessibility_service_description"
android:accessibilityEventTypes="typeWindowContentChanged"
android:accessibilityFlags="flagRequestTouchExplorationMode"
android:canRetrieveWindowContent="true"
android:canRequestTouchExplorationMode="true"
android:accessibilityFeedbackType="feedbackGeneric"
android:notificationTimeout="100"
android:settingsActivity="com.service.SettingsActivity"
/>
And the following accessibility service.
class MyA11yService extends AccessibilityService {
#Override public boolean onGesture(int gestureId) {
switch (gestureId) {
case GESTURE_SWIPE_UP_AND_DOWN:
CLog.d("Performing gesture.");
performGlobalAction(GLOBAL_ACTION_BACK);
return true;
default:
return false;
}
}
}
The performGlobalAction call works just fine in any Context. Now, instead of performing this action on the SWIPE_UP_DOWN gesture, what you want to do is set up some sort of inter-process communication with the part of this that you want to be able to trigger the "global back button" action. But, that information is for another question, though if you understand the information in this post, I'm sure how you need to proceed will be clear.

Stop listening for fingerprint when screen off

A user of my app reported that when my app is listening for fingerprint authentication (I have called fingerprintManager.authenticate) and the screen is turned off (by hitting the devices power switch button), it is not possible to use the fingerprint to unlock the device.
I can also see that the onAuthenticationError callback method is called when the screen is turned off, which does not happen when I leave my activity, because I call CancellationSignal.cancel() in my onPause method. I have checked that onPause is being called.
The same behavior can be observed in the Fingerprint Dialog sample (https://github.com/xamarin/monodroid-samples/tree/master/android-m/FingerprintDialog, ported from https://github.com/googlesamples/android-FingerprintDialog)
What can I do to resolve this behavior?
EDIT: I also tried to register a broadcast receiver for android.intent.action.SCREEN_OFF which gets notified after onPause, so it's no surprise that calling cancel() in that receiver does not change anything.
My problem is similar to yours: if someone sends my app to background by pressing Home button it still holds control over Fingerprint Sensor, so no one else can use it. Calling cancel from activities onPause() isn't working:
#Override
protected void onPause() {
super.onPause();
/**
* We are cancelling the Fingerprint Authentication
* when application is going to background
*/
if (fingerprintHelper!=null && fingerprintHelper instanceof AndroidFingerprintHelper){
log.info("canceling AndroidFingerprintHelper dialog");
fingerprintHelper.cancelIdentify();
}
}
You need to call the cancel() method of your CancellationSignal in your activity's onPause() method but before super.onPause(). Otherwise you will get the warning
Rejecting your.package.name. ; not in foreground
cancelAuthentication(): reject your package name
I've searched the source code of Android FingerPrint Service and I've found this lines:
public void cancelAuthentication(final IBinder token, String opPackageName) {
if (!canUseFingerprint(opPackageName, false /* foregroundOnly */)) {
return;
}
//we don't get here
...
...
}
where canUseFingerprint is actually checks if we are foreground or no (one of the things it does):
private boolean canUseFingerprint(String opPackageName, boolean foregroundOnly) {
if (foregroundOnly && !isForegroundActivity(uid, pid)) {
Slog.v(TAG, "Rejecting " + opPackageName + " ; not in foreground");
return false;
}
//we don't get here
}
This way we can never call cancelAuth from the background. And Android thinks that we are in background right after the super.onPause(); is called. After several hours of research the only solution I've found is to swap the cancel action and the super.onPause() :
#Override
protected void onPause() {
/**
* We are cancelling the Fingerprint Authentication
* when application is going to background
*/
if (fingerprintHelper!=null && fingerprintHelper instanceof AndroidFingerprintHelper){
log.info("canceling AndroidFingerprintHelper dialog");
fingerprintHelper.cancelIdentify();
}
super.onPause();
}
Worked for me on Android M and N. Hope this helps.
I have encountered similar behavior with the Samsung Fingerprint SDK (cannot authenticate when the screen is locked or off, it's not a bug, it's by design). After reviewing this scenario - we have concluded that the best approach would be to create a notification for the user that would contain a PendingIntent that would trigger your app and start the finger print authentication process.
The notification could make the phone vibrate/beep so that the user is alerted.
Hope this helps.
In the callback method onError() also call 'stopListeningc'. Then when screen turn off, the fingerprint listener from your app will stop work, then your device will listen fingerprint.
Hope this help!

Android- Telephone app that keeps focus on outgoing & incoming phoneCall

Using this simple example to create a PhoneCall application that dials out a hard coded # and monitors phone state.
http://www.mkyong.com/android/how-to-make-a-phone-call-in-android/
Unfortunately, on making the phone call, we always switch to the actual built -in phone application.
I want to avoid this, or at the very least hide the dialer pad button. The user SHOULD NOT have the option to enter a phone#.
Does anyone know of a way to achieve this?
i.e. keep the actual built-in phone application in the background
(I would need to add buttons for speaker, and end call in the primary application)
OR
alternatively, hide just the dial pad button in the native, built-in phone application?
Here is a solution I came up with to hide the caller app shortly after the call is placed. I don't believe there is a way to make it totally transparent without re-writing the Android system. I believe this could be improved by detecting when the caller app is set up and dialing instead of the postDelayed() I'm using which could be unreliable.
EDIT: I tried making a receiver to listen for NEW_OUTGOING_CALL to restart the original Activity, but it doesn't really improve anything, the dialer app must be running for an arbitrary amount of time before it can start it's background service.
EDIT: I tried making a PhoneStateListener that listens for CALL_STATE_OFFHOOK and re starts the Activity there. This doesn't work either as it happens before the dialing app is fully ready to go into the background.
EDIT: You can look at this thread: Reflection to access advanced telephony features, but I believe Google has since locked down all methods of placing a call outside the standard app.
This solution will start the dialing, and then switch back to the original Activity after a couple of seconds.
In my manifest I have:
android:launchMode="singleInstance"
on my Activity so I don't get a new instance.
public class MainActivity extends Activity
{
....
public void clickMe(View view)
{
startService(new Intent(this, PhoneService.class));
}
}
public class PhoneService extends Service
{
#Override
public int onStartCommand(Intent intent, int flags, int startId)
{
Intent call = new Intent(Intent.ACTION_CALL);
call.setData(Uri.parse("tel:XXXXXXXXX"));
call.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(call);
Handler h = new Handler();
h.postDelayed(new Runnable() {
#Override
public void run()
{
Intent act = new Intent(PhoneService.this, MainActivity.class);
act.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(act);
}
}, 4000);
return super.onStartCommand(intent, flags, startId);
}
#Override
public IBinder onBind(Intent arg0)
{
// TODO Auto-generated method stub
return null;
}
}
I believe it impossible to provide a cleaner solution, given the constraints of the SDK.
The functionality you are wanting isn't possible without some type of hack-ish work around. The Android system only allows the Phone app to control the underlying RIL and telephony stack and the Phone app UI responds to the dial URI by presenting this user with the dial screen where they must confirm (or alter) the number. This is a security provision to prevent unwanted apps from using the telephone device without the user knowing about it. Also, due to the way the Intent system works in Android, it is possible for other apps to handle calls using SIP or other VoIP functionality (i.e. Skype). In this case the user may have set a global preference to always use the other app and you have no control over how that app behaves with the dial Intent.

Categories

Resources