Detect the volume key press in background [duplicate] - java

I know how to listen to volume buttons in an activity. But can I do that in a background service? If yes, how to do that?

It is possible. Use code below (for newer Android versions, especially Marshmallow, see bottom of the answer):
public class SettingsContentObserver extends ContentObserver {
int previousVolume;
Context context;
public SettingsContentObserver(Context c, Handler handler) {
super(handler);
context=c;
AudioManager audio = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
previousVolume = audio.getStreamVolume(AudioManager.STREAM_MUSIC);
}
#Override
public boolean deliverSelfNotifications() {
return super.deliverSelfNotifications();
}
#Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
AudioManager audio = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
int currentVolume = audio.getStreamVolume(AudioManager.STREAM_MUSIC);
int delta=previousVolume-currentVolume;
if(delta>0)
{
Logger.d("Ściszył!"); // volume decreased.
previousVolume=currentVolume;
}
else if(delta<0)
{
Logger.d("Zrobił głośniej!"); // volume increased.
previousVolume=currentVolume;
}
}
}
Then in your service onCreate register it with:
mSettingsContentObserver = new SettingsContentObserver(this,new Handler());
getApplicationContext().getContentResolver().registerContentObserver(android.provider.Settings.System.CONTENT_URI, true, mSettingsContentObserver );
Then unregister in onDestroy:
getApplicationContext().getContentResolver().unregisterContentObserver(mSettingsContentObserver);
Note that this example judges by change of media volume, if you want to use other volume, change it!
UPDATE:
Above method supposedly doesn't work on Marshmallow, BUT there's much better way now since MediaSession was introduced! So first you have to migrate your code to MediaController/MediaSession pattern and then use this code:
private VolumeProviderCompat myVolumeProvider = null;
myVolumeProvider = new VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_RELATIVE, maxVolume, currentVolume) {
#Override
public void onAdjustVolume(int direction) {
// <0 volume down
// >0 volume up
}
};
mSession.setPlaybackToRemote(myVolumeProvider);
Somehow volume button presses are detected even with screen off (just be sure to register proper media button intent receiver if applicable for your platform!)
UPDATE 2 since GalDude requested some more info on getting media MediaSession/MediaController. Sorry, but since I stopped using Java it will be in Kotlin:
lateinit var mediaSession: MediaSessionCompat // you have to initialize it in your onCreate method
val kontroler: MediaControllerCompat
get() = mediaSession.controller // in Java it's just getController() on mediaSession
// in your onCreate/start method:
mediaSession = MediaSessionCompat(this, "YourPlayerName", receiver, null)
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
mediaSession.isActive = true
if (ratingIsWorking) // note: rating crashes on some machines you have to check it!
mediaSession.setRatingType(RatingCompat.RATING_5_STARS)
mediaSession.setCallback(object : MediaSessionCompat.Callback() {
...
// here you have to implement what happens with your player when play/pause/stop/ffw etc. is requested - see exaples elsewhere
})
// onDestroy/exit method:
mediaSession.isActive = false
mediaSession.release()

Unfortunately, this is another area of Android where there are like five different ways to "solve the problem", but most of them don't work very well. For my own sanity, I'll attempt to list all the different approaches below.
Solutions
1) MediaSession (from Service)
Answer by Denis Kniazhev: https://stackoverflow.com/a/43304591/2441655
Drawbacks:
Requires Android API level 21+ (Android 5.0+).
2) android.media.VOLUME_CHANGED_ACTION (from Service)
Answer by Nikhil: https://stackoverflow.com/a/44040282/2441655
Drawbacks:
Not an official part of the SDK: https://stackoverflow.com/a/8974510/2441655
Ignores first-press of volume-key (since it only shows the volume-bar).
Ignores volume-up key when at 100%, and volume-down key when at 0%.
3) ContentObserver (from Service)
Answer by ssuukk: https://stackoverflow.com/a/15292255/2441655 (first part)
Drawbacks:
Doesn't work in newer versions of Android: comment by dsemi
Ignores first-press of volume-key (since it only shows the volume-bar).
Ignores volume-up key when at 100%, and volume-down key when at 0%.
4) AudioManager.registerMediaButtonEventReceiver (from Service)
Answer by Joe: https://stackoverflow.com/a/11510564/2441655
Drawbacks:
Doesn't work on most roms: comment by elgui
5) onKeyDown (from Activity)
Answer by dipali: https://stackoverflow.com/a/21086563/2441655
Drawbacks:
Doesn't work if screen is off, in different app, etc.
6) dispatchKeyEvent (from Activity)
Answer by Maurice Gavin: https://stackoverflow.com/a/11462962/2441655
Drawbacks:
Doesn't work if screen is off, in different app, etc.
Conclusion
The solution I'm currently using is #1, because:
It's an official part of the SDK.
It is usable from a service. (ie. regardless of what app you're in)
It captures every volume-key press, regardless of current-volume/ui-state.
It works when the screen is off.
Let me know if you find any others -- or if you've found more drawbacks to some of them!

The AOSP Music app has a Service (MediaPlaybackService) that responds to volume key events by registering a BroadcastReceiver (MediaButtonIntentReceiver).
Here's the code snippet where it registers the receiver:
mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
ComponentName rec = new ComponentName(getPackageName(),
MediaButtonIntentReceiver.class.getName());
mAudioManager.registerMediaButtonEventReceiver(rec);
Also, don't forget about manifest:
<receiver android:name="com.android.music.MediaButtonIntentReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
</intent-filter>
</receiver>
This works even if the Music app is not in the foreground. Isn't that what you want?

I was able to make it work on android 5+ devices using MediaSession. However,ContentObserver suggested by #ssuukk didn't work for me on both 4.4 and 7.0 devices (at least on ROMs that I've been testing on).
Here is a full example which works on android 5+.
Service:
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.support.v4.media.VolumeProviderCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
public class PlayerService extends Service {
private MediaSessionCompat mediaSession;
#Override
public void onCreate() {
super.onCreate();
mediaSession = new MediaSessionCompat(this, "PlayerService");
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 0) //you simulate a player which plays something.
.build());
//this will only work on Lollipop and up, see https://code.google.com/p/android/issues/detail?id=224134
VolumeProviderCompat myVolumeProvider =
new VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_RELATIVE, /*max volume*/100, /*initial volume level*/50) {
#Override
public void onAdjustVolume(int direction) {
/*
-1 -- volume down
1 -- volume up
0 -- volume button released
*/
}
};
mediaSession.setPlaybackToRemote(myVolumeProvider);
mediaSession.setActive(true);
}
#Override
public IBinder onBind(Intent intent) {
return null;
}
#Override
public void onDestroy() {
super.onDestroy();
mediaSession.release();
}
}
In AndroidManifest.xml:
<application ...>
...
<service android:name=".PlayerService"/>
</application>
In your activity:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
startService(new Intent(this, PlayerService.class));
}
There are several things to be aware of:
It intercepts volume buttons completely so while this code is running you won't be able to adjust ring volume using volume buttons. This might be possible to fix, I just didn't try.
If you run the example as-is the volume buttons will remain controlled by the app even when the screen is off and the app has been removed from "Recent Apps" list. You'll have to go to Settings->Applications, find the app and force stop it to get volume buttons back.

Judging by the couple of other questions about this topic, no.
Other question 1,
Other question 2
Services simply do not receive KeyEvent callbacks.

You need to play blank sound from service then only you can listen to volume changes. Following worked for me
Steps
1. Put blank.mp3 in raw folder (Download from here)
2. Start media at onStartCommand()
private MediaPlayer mediaPlayer;
public MyService() {
}
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
........
mediaPlayer = MediaPlayer.create(this, R.raw.blank);
mediaPlayer.setLooping(true);
mediaPlayer.start();
.......
return START_STICKY;
}
3. You must choose to stop and release mediaplayer. It's better to do so in onDestroy()
#Override
public void onDestroy() {
mediaPlayer.stop();
mediaPlayer.release();
super.onDestroy();
}
4. Create Broadcast receiver that will listen for volume changes
int volumePrev = 0;
private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
if ("android.media.VOLUME_CHANGED_ACTION".equals(intent.getAction())) {
int volume = intent.getIntExtra("android.media.EXTRA_VOLUME_STREAM_VALUE",0);
Log.i(TAG, "volume = " + volume);
if (volumePrev < volume) {
Log.i(TAG, "You have pressed volume up button");
} else {
Log.i(TAG, "You have pressed volume down button");
}
volumePrev = volume;
}
}
};
5. Register the broadcast receiver in onStartCommand()
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
.....
IntentFilter filter = new IntentFilter();
filter.addAction("android.media.VOLUME_CHANGED_ACTION");
registerReceiver(broadcastReceiver, filter);
....
return START_STICKY;
}
6. Unregister broadccast receiver in onDestroy()
#Override
public void onDestroy() {
.....
unregisterReceiver(broadcastReceiver);
.....
super.onDestroy();
}
That's all

This requires Lollipop (v5.0/API 21) or higher
My goal was to adjust system volume from a Service. Any action can be taken on press though.
public class VolumeKeyController {
private MediaSessionCompat mMediaSession;
private final Context mContext;
public VolumeKeyController(Context context) {
mContext = context;
}
private void createMediaSession() {
mMediaSession = new MediaSessionCompat(mContext, KeyUtil.log);
mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mMediaSession.setPlaybackState(new Builder()
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 0)
.build());
mMediaSession.setPlaybackToRemote(getVolumeProvider());
mMediaSession.setActive(true);
}
private VolumeProviderCompat getVolumeProvider() {
final AudioManager audio = mContext.getSystemService(Context.AUDIO_SERVICE);
int STREAM_TYPE = AudioManager.STREAM_MUSIC;
int currentVolume = audio.getStreamVolume(STREAM_TYPE);
int maxVolume = audio.getStreamMaxVolume(STREAM_TYPE);
final int VOLUME_UP = 1;
final int VOLUME_DOWN = -1;
return new VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_RELATIVE, maxVolume, currentVolume) {
#Override
public void onAdjustVolume(int direction) {
// Up = 1, Down = -1, Release = 0
// Replace with your action, if you don't want to adjust system volume
if (direction == VOLUME_UP) {
audio.adjustStreamVolume(STREAM_TYPE,
AudioManager.ADJUST_RAISE, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
}
else if (direction == VOLUME_DOWN) {
audio.adjustStreamVolume(STREAM_TYPE,
AudioManager.ADJUST_LOWER, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
}
setCurrentVolume(audio.getStreamVolume(STREAM_TYPE));
}
};
}
// Call when control needed, add a call to constructor if needed immediately
public void setActive(boolean active) {
if (mMediaSession != null) {
mMediaSession.setActive(active);
return;
}
createMediaSession();
}
// Call from Service's onDestroy method
public void destroy() {
if (mMediaSession != null) {
mMediaSession.release();
}
}
}

#venryx: Solution 1 no longer works in Android 12
#ssuukk: I can confirm #venryx's comment that SettingsContentObserver does not get triggered if the volume is already at min or max.
#bikram: I created a VolumeButtonHelper class that uses this approach. Although it does use an undocumented SDK feature, it still works in 2022. I have extensively researched this topic and this was the only solution I could find.
class VolumeButtonHelper(private var context: Context,
private var stream: Int? = null,
enabledScreenOff: Boolean)
{
companion object
{
const val VOLUME_CHANGE_ACTION = "android.media.VOLUME_CHANGED_ACTION"
const val EXTRA_VOLUME_STREAM_TYPE = "android.media.EXTRA_VOLUME_STREAM_TYPE"
}
enum class Direction
{
Up,
Down,
Release
}
private lateinit var mediaPlayer: MediaPlayer
private var volumeBroadCastReceiver: VolumeBroadCastReceiver? = null
private var volumeChangeListener: VolumeChangeListener? = null
private val audioManager: AudioManager? =
context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
private var priorVolume = -1
private var volumePushes = 0.0
private var longPressReported = false
var doublePressTimeout = 350L
var buttonReleaseTimeout = 100L
var minVolume = -1
private set
var maxVolume = -1
private set
var halfVolume = -1
private set
var currentVolume = -1
private set
init
{
if (audioManager != null)
{
minVolume = audioManager.getStreamMinVolume(STREAM_MUSIC)
maxVolume = audioManager.getStreamMaxVolume(STREAM_MUSIC)
halfVolume = (minVolume + maxVolume) / 2
/*************************************
* BroadcastReceiver does not get triggered for VOLUME_CHANGE_ACTION
* if the screen is off and no media is playing.
* Playing a silent media file solves that.
*************************************/
if (enabledScreenOff)
{
mediaPlayer =
MediaPlayer.create(context,
R.raw.silence)
.apply {
isLooping = true
setWakeMode(context, PARTIAL_WAKE_LOCK)
start()
}
}
}
else
Log.e(TAG, "Unable to initialize AudioManager")
}
fun registerVolumeChangeListener(volumeChangeListener: VolumeChangeListener)
{
if (volumeBroadCastReceiver == null)
{
this.volumeChangeListener = volumeChangeListener
volumeBroadCastReceiver = VolumeBroadCastReceiver()
if (volumeBroadCastReceiver != null)
{
val filter = IntentFilter()
filter.addAction(VOLUME_CHANGE_ACTION)
context.registerReceiver(volumeBroadCastReceiver, filter)
}
else
Log.e(TAG, "Unable to initialize BroadCastReceiver")
}
}
fun unregisterReceiver()
{
if (volumeBroadCastReceiver != null)
{
context.unregisterReceiver(volumeBroadCastReceiver)
volumeBroadCastReceiver = null
}
}
fun onVolumePress(count: Int)
{
when (count)
{
1 -> volumeChangeListener?.onSinglePress()
2 -> volumeChangeListener?.onDoublePress()
else -> volumeChangeListener?.onVolumePress(count)
}
}
interface VolumeChangeListener
{
fun onVolumeChange(direction: Direction)
fun onVolumePress(count: Int)
fun onSinglePress()
fun onDoublePress()
fun onLongPress()
}
inner class VolumeBroadCastReceiver : BroadcastReceiver()
{
override fun onReceive(context: Context, intent: Intent)
{
if (stream == null ||
intent.getIntExtra(EXTRA_VOLUME_STREAM_TYPE, -1) == stream)
{
currentVolume = audioManager?.getStreamVolume(STREAM_MUSIC) ?: -1
if (currentVolume != -1)
{
if (currentVolume != priorVolume)
{
if (currentVolume > priorVolume)
volumeChangeListener?.onVolumeChange(Up)
else if (currentVolume < priorVolume)
volumeChangeListener?.onVolumeChange(Down)
priorVolume = currentVolume
}
volumePushes += 0.5 // For some unknown reason (to me), onReceive gets called twice for every button push
if (volumePushes == 0.5)
{
CoroutineScope(Dispatchers.Main).launch {
delay(doublePressTimeout - buttonReleaseTimeout)
buttonDown()
}
}
}
}
}
private fun buttonDown()
{
val startVolumePushes = volumePushes
CoroutineScope(Dispatchers.Main).launch {
delay(buttonReleaseTimeout)
val currentVolumePushes = volumePushes
if (startVolumePushes != currentVolumePushes)
{
if (volumePushes > 2 && !longPressReported)
{
longPressReported = true
volumeChangeListener?.onLongPress()
}
buttonDown()
}
else
{
onVolumePress(volumePushes.toInt())
volumeChangeListener?.onVolumeChange(Release)
volumePushes = 0.0
longPressReported = false
}
}
}
}
}
Instantiate that class in a Service (with the appropriate wake lock):
class ForegroundService : Service()
{
private lateinit var volumeButtonHelper: VolumeButtonHelper
companion object
{
var wakeLock: WakeLock? = null
const val TAG = "VolumeButtonHelper"
const val ACTION_FOREGROUND_WAKELOCK = "com.oliverClimbs.volumeButtonHelper.FOREGROUND_WAKELOCK"
const val ACTION_FOREGROUND = "com.oliverClimbs.volumeButtonHelper.FOREGROUND"
const val WAKELOCK_TAG = "com.oliverClimbs.volumeButtonHelper:wake-service"
const val CHANNEL_ID = "Running in background"
}
override fun onBind(p0: Intent?): IBinder?
{
return null
}
override fun onCreate()
{
super.onCreate()
volumeButtonHelper = VolumeButtonHelper(this,
STREAM_MUSIC,
enabledScreenOff = true)
volumeButtonHelper.registerVolumeChangeListener(
object : VolumeButtonHelper.VolumeChangeListener
{
override fun onVolumeChange(direction: VolumeButtonHelper.Direction)
{
Log.i(TAG, "onVolumeChange: $direction")
}
override fun onVolumePress(count: Int)
{
Log.i(TAG, "onVolumePress: $count")
}
override fun onSinglePress()
{
Log.i(TAG, "onSinglePress")
}
override fun onDoublePress()
{
Log.i(TAG, "onDoublePress")
}
override fun onLongPress()
{
Log.i(TAG, "onLongPress")
}
})
}
#SuppressLint("WakelockTimeout")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int
{
super.onStartCommand(intent, flags, startId)
if (intent?.action == ACTION_FOREGROUND || intent?.action == ACTION_FOREGROUND_WAKELOCK)
startForeground(R.string.foreground_service_started,
Notification.Builder(this, CHANNEL_ID).build())
if (intent?.action == ACTION_FOREGROUND_WAKELOCK)
{
if (wakeLock == null)
{
wakeLock = getSystemService(PowerManager::class.java)?.newWakeLock(
PARTIAL_WAKE_LOCK,
WAKELOCK_TAG)
wakeLock?.acquire()
}
else
{
releaseWakeLock()
}
}
return START_STICKY
}
private fun releaseWakeLock()
{
wakeLock?.release()
wakeLock = null
}
override fun onDestroy()
{
super.onDestroy()
releaseWakeLock()
stopForeground(STOP_FOREGROUND_REMOVE)
volumeButtonHelper.unregisterReceiver()
}
}
Start the Service from your Activity:
class MainActivity : AppCompatActivity()
{
private var configurationChange = false
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (!configurationChange)
startService(Intent(ForegroundService.ACTION_FOREGROUND_WAKELOCK).setClass(this,
ForegroundService::class.java))
}
override fun onDestroy()
{
Log.d(TAG, "MainActivity: onDestroy")
configurationChange =
if (isChangingConfigurations)
true
else
{
stopService(Intent(this, ForegroundService::class.java))
false
}
super.onDestroy()
}
}
I have shared the full project at github.com/oliverClimbs/VolumeButtonDemo.

As for me accessibility service only works as expected
class KeyService : AccessibilityService() {
override fun onServiceConnected() {}
override fun onAccessibilityEvent(event: AccessibilityEvent) {}
override fun onKeyEvent(event: KeyEvent): Boolean {
when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> {
when (event.action) {
KeyEvent.ACTION_DOWN -> {
}
KeyEvent.ACTION_UP -> {
}
}
}
KeyEvent.KEYCODE_VOLUME_DOWN -> {
when (event.action) {
KeyEvent.ACTION_DOWN -> {
}
KeyEvent.ACTION_UP -> {
}
}
}
}
return super.onKeyEvent(event)
}
override fun onInterrupt() {}
}
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityFlags="flagRequestFilterKeyEvents"
android:canRequestFilterKeyEvents="true"
android:description="#string/app_name" />
<service
android:name=".KeySrvice"
android:exported="true"
android:label="#string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<meta-data
android:name="android.accessibilityservice"
android:resource="#xml/key" />
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
</service>
inline fun <reified T : Service> Context.hasAccessibility(): Boolean {
var enabled = 1
try {
enabled = Secure.getInt(contentResolver, Secure.ACCESSIBILITY_ENABLED)
} catch (ignored: Throwable) {
}
if (enabled == 1) {
val name = ComponentName(applicationContext, T::class.java).flattenToString()
val services = Secure.getString(contentResolver, Secure.ENABLED_ACCESSIBILITY_SERVICES)
return services?.contains(name) ?: false
}
return false
}
if (!hasAccessibility<KeyService>()) {
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
}

Android doesn't document APIs on interacting with volume buttons in that case. So I guess the answer is no…

checkout Controlling Your App’s Volume and Playback ...This will help to solve your problem... multiple applications might want to listen for button presses from background, this may be the reason why KeyEvents can only be handled by Activities as they are the interface to the user pressing the keys.

Note: this only works for Activities, and not Services as the question states.
Depending on the context in which the callback is required an alternative solution might be available.
To be capable of detecting the volume button an Activity would need to override the dispatchKeyEvent function. For this to be present in multiple activities could could write a superclass containing the overridden function which is extended by all subsequent activities.
Here is the code for detecting Volume Up/Down key presses:
// Over-ride this function to define what should happen when keys are pressed (e.g. Home button, Back button, etc.)
#Override
public boolean dispatchKeyEvent(KeyEvent event)
{
if (event.getAction() == KeyEvent.ACTION_DOWN)
{
switch (event.getKeyCode())
{
case KeyEvent.KEYCODE_VOLUME_UP:
// Volume up key detected
// Do something
return true;
case KeyEvent.KEYCODE_VOLUME_DOWN:
// Volume down key detected
// Do something
return true;
}
}
return super.dispatchKeyEvent(event);
}

Been googling around this problem 10 years later, and I dug out this here:
https://stackoverflow.com/a/59064820
By providing an AccessibilityService, it is possible to listen to the volume buttons, outside the activity, even when the phone is locked, even before it is sent down to the specific apps (just tested it). Only downside: The user must activate this service manually in the settings.

MyService.java
public class MyService extends Service {
private BroadcastReceiver vReceiver;
#Override
public void onCreate() {
super.onCreate();
vReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
FileLog.e("Something just happens");
}
};
registerReceiver(vReceiver, new IntentFilter("android.media.VOLUME_CHANGED_ACTION"));
}
#Nullable
#Override
public IBinder onBind(Intent intent) {
return null;
}
#Override
public void onDestroy() {
unregisterReceiver(vReceiver);
}
}
AndroidManifest.xml
<application>
...
<service android:name=".MyService" android:exported="true"/>
</application>
onCreate || onStartActivity
public void onCreate(){
....
startService(new Intent(this, MyService.class));
}
that solution like little bit same as Answer
but working on Android 33+

Related

Unable to scan or discover BT and BLE devices in android

I am a complete novice in Java and Android. I am trying to create a test app to listen for BLE and BT devices nearby. I have another device where I wrote some logic to broadcast its BLE beacons. I verified it using a playstore app. Now I am trying to write my own app on Android.
I have been reading the Android developer pages for guidance. I have literally followed every step of the following pages
https://developer.android.com/guide/topics/connectivity/bluetooth/setup
https://developer.android.com/guide/topics/connectivity/bluetooth/permissions
https://developer.android.com/guide/topics/connectivity/bluetooth/find-bluetooth-devices
https://developer.android.com/guide/topics/connectivity/bluetooth/find-ble-devices
Also, Note that I have used BARE MINIMUM CODE from the Android Developers page So here is what I have done.
1. First off I have added my permissions under AndroidManifest
Note1 : I am deploying this app to My phone running Android 11
Note2 : All this code is written inside MainActivity. I have not created any other activity class
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
2. Next I check if my BT is enabled.
if (bluetoothAdapter == null) {
blefinder.append("\nDEVICE DOES NOT SUPPORT BLUETOOTH");
}
else {
blefinder.append("\nDEVICE SUPPORTS BLUETOOTH");
}
I get the success message that BT is of course enabled
3. Next I check if my device supports BLE
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
blefinder.append("\nBLE NOT SUPPORTED ON THIS DEVICE : ");
finish();
}
else{
blefinder.append("\nBLE IS SUPPORTED ON THIS DEVICE : ");
}
I get the message that BLE is supported
4. Next I list my already paired/bonded devices
For this I call ListPairedAndBondedDevices(); in onCreate() itself right after the above steps. Function Definition Below.
private void ListPairedAndBondedDevices(){
#SuppressLint("MissingPermission") Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
if (pairedDevices.size() > 0) {
// There are paired devices. Get the name and address of each paired device.
blefinder.append("\nPAIRED/BONDED DEVICES");
for (BluetoothDevice device : pairedDevices) {
blefinder.append("\n" + device.getName() + " | " + device.getAddress());
}
}
}
This also works like a charm and prints out my paired devices. The next 2 parts is where I face the problem.
5. The Problem Step | Part 1:
Here I register a Broadcast receiver to discover all BT devices in the vicinity. I've unbonded my BT headphones and kept it in pairing mode to verify this.
ListPairedAndBondedDevices(); // From previous code snippet
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); // New code statement
registerReceiver(BTReceiver, filter);// New code statement
Broadcast Receiver implementation
private final BroadcastReceiver BTReceiver = new BroadcastReceiver() {
#SuppressLint("MissingPermission")
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// Discovery has found a device. Get the BluetoothDevice
// object and its info from the Intent.
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
blefinder.append("\n" + device.getName() + " | " + device.getAddress());
}
}
};
So This part didn't Work :(
If you see above, I am registering the BTReceiver in onCreate right after listing the already paired devices (by calling ListPairedAndBondedDevices()).
When I ran the debugger, this broadcast receiver never gets called.
6. The Problem Step | Part 2:
Right after this I try to scan for BLE Devices as well by callin scanLeDevice()
ListPairedAndBondedDevices(); // From previous snippet
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); // From previous snippet
registerReceiver(BTReceiver, filter);// From previous snippet
scanLeDevice(); // ---------------->>> CALLING THIS FUNCTION TO SCAN FOR BLE DEVICES
Implementation of scanLeDevice()
private void scanLeDevice() {
if (!scanning) {
// Stops scanning after a predefined scan period.
handler.postDelayed(new Runnable() {
#Override
public void run() {
scanning = false;
bluetoothLeScanner.stopScan(leScanCallback);
blefinder.append("\nSTOPPING BLE SCAN... TIMEOUT REACHED");
}
}, SCAN_PERIOD);
scanning = true;
bluetoothLeScanner.startScan(leScanCallback);
} else {
scanning = false;
bluetoothLeScanner.stopScan(leScanCallback);
blefinder.append("\nSTOPPING BLE SCAN");
}
}
Unfortunately this also fails. The debugger tells me that this part of the code is getting called.
And after 30 seconds of SCAN_PERIOD (The TIMEOUT that I've set), I get the message that the scanning has stopped (STOPPING BLE SCAN)
Now I have implemented the leScanCallback as well (i.e the Device Scan Callback)
private ScanCallback leScanCallback =
new ScanCallback() {
#Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
blefinder.append("SOMETHING GOT SCANNED?");
blefinder.append("\n"+result.getDevice().toString());
// leDeviceListAdapter.addDevice(result.getDevice());
// leDeviceListAdapter.notifyDataSetChanged();
}
};
Notice that I am not using a ListAdapter since I have no idea about that concept. Hence for starters I am just trying to dump the results in a TextView represented by blefinder . This blefinder prints all the other texts so there is nothing wrong with that TextView variable. When I ran using the, debugger, it is not entering into the leScanCallback piece of code definition at all, even after 30 seconds, after scanLeDevice() function is executed.
I am a little lost here. Is there something I may be missing or doing wrong. It is supposed to be a simple, list the ble/bt devices around my vicinity.
I am happy to share any further information if I have missed. Just let me know in the comments.
Assuming you've done with the permissions that I've mentioned in the comments, we can implement a clean bluetooth LE scanner object and then use it in the UI.
First we implement a result consumer interface in order to deliver the results to the consumers which call the BleScanner.scan() method.
public interface ScanResultConsumer {
public void onDeviceFound(BluetoothDevice device, byte[] scanRecord, int rssi);
public void onScanningStarted();
public void onScanningStopped();
}
Now we need to implement the scanner object that manages the scanning events:
public class BleScanner {
private static final String TAG = BleScanner.class.getSimpleName();
private BluetoothLeScanner leScanner = null;
private BluetoothAdapter bleAdapter = null;
private Handler uiHandler = new Handler(Looper.getMainLooper);
private ScanResultConsumer scanResultConsumer;
private boolean scanning = false;
private final ArrayList<BluetoothDevice> foundDeviceList = new ArrayList<>();
public BleScanner(Context context) {
final BluetoothManager bluetoothManager = (BluetoothManager)
context.getSystemService(Context.BLUETOOTH_SERVICE);
bleAdapter = bluetoothManager.getAdapter();
if(bleAdapter == null) {
Log.d(TAG, "No bluetooth hardware.");
}
else if(!bleAdapter.isEnabled()){
Log.d(TAG, "Blutooth is off.");
}
}
public void scan(ScanResultConsumer scanResultConsumer, long scanTime){
foundDeviceList.clear();
if (scanning){
Log.d(TAG, "Already scanning.");
return;
}
Log.d(TAG, "Scanning...");
if(leScanner == null){
leScanner = bleAdapter.getBluetoothLeScanner();
}
if(scanTimeMs > 0) {
uiHandler.postDelayed(()-> {
if (scanning) {
Log.d(TAG, "Scanning is stopping.");
if(leScanner != null)
leScanner.stopScan(scanCallBack);
else
Log.d(TAG,"Scanner null");
setScanning(false);
}
}, scanTimeMs);
}
this.scanResultConsumer = scanResultConsumer;
leScanner.startScan(scanCallBack);
setScanning(true);
}
private final ScanCallback scanCallBack = new ScanCallback() {
#Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
if (!scanning){
return;
}
if(foundDeviceList.contains(result.getDevice())) {
// This device has already been found
return;
}
// New device found, add it to the list in order to prevent duplications
foundDeviceList.add(result.getDevice());
if(scanResultConsumer != null) {
uiHandler.post(() -> {
scanResultConsumer.onDeviceFound(result.getDevice(),
result.getScanRecord().getBytes(), result.getRssi());
});
}
}
};
public boolean isScanning(){
return scanning;
}
void setScanning(boolean scanning){
this.scanning = scanning;
uiHandler.post(() -> {
if(scanResultConsumer == null) return;
if(!scanning){
scanResultConsumer.onScanningStopped();
// Nullify the consumer in order to prevent UI crashes
scanResultConsumer = null;
} else{
scanResultConsumer.onScanningStarted();
}
});
}
}
Finally we can use this clean implementation in anywhere we need. But do note that a context must be provided in order to create a BleScanner object.
public class MainActivity extends AppCompatActivity {
private BleScanner bleScanner;
private Button buttonScan
// Other codes...
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Other codes...
bleScanner = new BleScanner(getApplicationContext());
// Other codes...
// For example if you want to start scanning on a button press
// Let's say you have a button called buttonScan and initiated it
buttonScan = findViewById(R.id.scan_button);
buttonScan.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
bleScanner.scan(new ScanResultConsumer {
#Override
public void onDeviceFound(BluetoothDevice device, byte[] scanRecord, int rssi) {
// TODO Here you have a newly found device, do something.
}
#Override
q public void onScanningStarted() {
// TODO Scanning has just started, you may want to make some UI changes.
}
#Override
public void onScanningStopped() {
// TODO Scanning has just stopped, you may want to make some UI changes.
}
});
}
});
}
}
Note: I written this code in a plain editor not in Android Studio. So there may be some errors, let me know if any.
First you should check if your app was granted the location permission(s) in the Settings app > Apps <your_app> > permissions. Some permissions (like ACCESS_*_LOCATION and BLUETOOTH_ADMIN) need to be requested at runtime and granted by the user through a popup. Normally you should get a SecurityException or a logcat warning when trying to execute code requiring permissions which your app doesn't have, but it's not uncommon for android to skip over error handling.
Consider using this method to start the scan in order check its result code for potential additional info about what is (not) going on.
You might also get some clues by logging all actions received in BTReceiver.onReceive(), not just action found.
Lastly check if the location settings on your device to ensure that bluetooth scanning is turned on (Settings app > location > wifi and bluetooth scanning )

Android NFC Reader in MVP - onNewIntent not firing

I have a working NFC reader/writer code. Using the same code, I added the reader function in another app which is following MVP architecture.
The activity is named NFCReaderActivity. A separate NFC class is created (NFCReader), which implements Sensor interface.
The app is supposed to work both in the foreground and launch showing the NFC tag info. The launch part is working fine and app launches and reads the tag and shows its content.
However, in the foreground, on scanning, it does nothing. I only hear the scan beep but no onNewIntent is firing.
Below are the log entries captured for foreground and launch actions. There is a difference in the class names:
When not launching
I/ActivityManager: START u0 {act=android.nfc.action.NDEF_DISCOVERED typ=application/com.abc.vi flg=0x14008000 cmp=com.abc.vi/.ui.reader.NFCReader (has extras)} from uid 10038 on display 0
When launching
I/ActivityManager: START u0 {act=android.nfc.action.NDEF_DISCOVERED typ=application/com.abc.vi cmp=com.abc.vi/.ui.reader.NFCReaderActivity (has extras)} from uid 1027 on display 0
Activity
onCreate
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i(TAG, "__onCreate__ " );
setContentView(R.layout.activity_nfc_reader);
VI.setNFCReaderActivityContext(this); //VI is the Application class
ButterKnife.bind(this);
presenter = new ReaderPresenter(this);
}
onNewIntent
#Override
public void onNewIntent(Intent intent) {
Log.i(TAG, "__onNewIntent__ " );
// onResume gets called after this to handle the intent
// setIntent(intent);
presenter.onNewIntent(intent);
}
onResume, onPause
#Override
protected void onResume() {
super.onResume();
Log.i(TAG, "__onResume__ " );
presenter.onResume();
}
#Override
protected void onPause() {
super.onPause();
Log.i(TAG, "__onPause__ " );
presenter.onPause();
}
Presenter
ReaderPresenter(ReaderContract.View view) {
this.view = view;
initSensor();
}
#Override
public void initSensor() {
nfcReader = new NFCReader(VI.getNFCReaderActivityContext(), this); //VI is the Application class
}
#Override
public void onNewIntent(Intent intent) {
nfcReader.resolveIntent(intent);
}
#Override
public void onResume() {
nfcReader.onResume();
}
#Override
public void onPause() {
nfcReader.onPause();
}
#Override
public void onDestroy() {
speech.onDestroy();
}
NFCReader
public class NFCReader implements Sensors {
private static final String TAG = NFCReader.class.getSimpleName();
private NfcAdapter nfcAdapter;
private PendingIntent nfcPendingIntent;
private NFCReaderActivity activity;
private ReaderPresenter presenter;
NFCReader(NFCReaderActivity nfcReaderActivity, ReaderPresenter readerPresenter) {
this.activity = nfcReaderActivity;
this.presenter = readerPresenter;
init();
}
#Override
public void init() {
//Initialize NFC adapter
nfcAdapter = NfcAdapter.getDefaultAdapter(activity);
nfcPendingIntent = PendingIntent.getActivity(activity, 0, new Intent(activity,
getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP), 0);
}
public void onResume() {
if (nfcAdapter != null) {
nfcAdapter.enableForegroundDispatch(activity, nfcPendingIntent, null, null);
// if NFC not enabled
if (!nfcAdapter.isEnabled()) {
new AlertDialog.Builder(activity)
.setPositiveButton(activity.getString(R.string.update_setting_btn),
(dialog, which) -> {
Intent setNfc = new Intent(Settings.ACTION_WIRELESS_SETTINGS);
activity.startActivity(setNfc);
})
.setOnCancelListener(
dialog -> activity.finish()
)
.create().show();
}
resolveIntent(activity.getIntent());
} else {
Toast.makeText(VI.getAppContext(),
activity.getString(R.string.error_no_nfc_found), Toast.LENGTH_LONG).show();
}
}
public void onPause() {
if (nfcAdapter != null) {
nfcAdapter.disableForegroundDispatch(activity);
}
}
public void resolveIntent(Intent intent){
Log.i(TAG, "__resolveIntent__");
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
NdefMessage[] messages = null;
Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
if (rawMsgs != null) {
messages = new NdefMessage[rawMsgs.length];
for (int i = 0; i < rawMsgs.length; i++) {
messages[i] = (NdefMessage) rawMsgs[i];
}
}
if ((messages != null ? messages[0] : null) != null) {
StringBuilder result = new StringBuilder();
byte[] payload = messages[0].getRecords()[0].getPayload();
for (byte aPayload : payload) {
result.append((char) aPayload);
}
Log.i(TAG,"Decoded --> "+result.toString());
presenter.getData(result.toString());
}
}
}
}
Manifest
<activity android:name=".ui.reader.NFCReaderActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="#string/mime_type" />
</intent-filter>
</activity>
UPDATE
I moved all the code from NFCReader class to NFCReaderActivity and both foreground and launch modes are working. The issue is with MVP architecture. How to convert it back to MVP?
You seem to register the pending intent for the wrong (actually an invalid) component (not your activity class). The reason is that when you create the PendingIntent that you assign to nfcPendingIntent, you use getClass() to obtain the class of the NFCReader instance. Instead you would need to use activity.getClass() to obtain the class of your activity component.

How can i detect the New outgoing call From which sim in dual sim devices?

I know that i can detect new outgoing call by this receiver :
<receiver android:name=".NewOutgoingCallReceiver">
<intent-filter>
<action android:name="android.intent.action.NEW_OUTGOING_CALL" />
</intent-filter>
</receiver>
And in OnReceive method i want to know which sim making this call ?
public class NewOutgoingCallReceiver extends BroadcastReceiver
{
#Override
public void onReceive( Context context, Intent intent )
{
// here i want to check which sim is making that new call
}
}
The intent received by your broadcast receiver should have some extra information in the bundle, one of which is the 'slot' - meaning the SIM slot.
You can get this in your example above like this - this is for API 22 and above:
public class NewOutgoingCallReceiver extends BroadcastReceiver
{
#Override
public void onReceive( Context context, Intent intent )
{
//check which sim is making that new call
String callSlot = "";
Bundle bundle = intent.getExtras();
callSlot =String.valueOf(bundle.getInt("slot", -1));
if(callSlot == "0"){
//Call is from SIM slot0
} else if(callSlot =="1"){
//Call is from SIM slot 1
}
}
}
I think this will be more correct
val SIM_SLOT_NAMES = arrayOf(
"extra_asus_dial_use_dualsim",
"com.android.phone.extra.slot",
"slot",
"simslot",
"sim_slot",
"subscription",
"Subscription",
"phone",
"com.android.phone.DialingMode",
"simSlot",
"slot_id",
"simId",
"simnum",
"phone_type",
"slotId",
"slotIdx"
)
private fun getSimSlot(extras: Bundle?): Int {
for (name in SIM_SLOT_NAMES) {
if (extras?.containsKey(name) == true) {
return extras.getInt(name, 0)
}
}
return 0
}
// usage
getSimSlot(intent.extras)

Android: How to control music service play/pause from bluetooth device?

Developing a music app. In my Music Service, I have written a custom broadcast receiver. It works with Intent.ACTION_HEADSET_PLUG but not with Intent.ACTION_MEDIA_BUTTON.
Please guide on how to control music controls from bluetooth devices (Play/Pause/Next/Previous).
Code for Intent.ACTION_HEADSET_PLUG is:
#Override
public void onReceive(Context context, Intent intent) {
// aux
if(intent.getAction().equals(Intent.ACTION_HEADSET_PLUG))
{
int state = intent.getIntExtra("state", -1);
if(state == 0)
{
// Headset is unplugged. PAUSE
pauseSong();
sendBroadcast();
}
else if(state == 1)
{
// headset is plugged
resumeSong();
sendBroadcast();
}
}
}
As explained in the Media playback the right way talk, you must be registered as the 'preferred media app'. This is much easier when you use MediaSessionCompat as explained in the MediaSessionCompat video:
ComponentName mediaButtonReceiver =
new ComponentName(context, YourBroadcastReceiver.class);
MediaSessionCompat mediaSession =
new MediaSessionCompat(context,
tag, // Debugging tag, any string
mediaButtonReceiver,
null);
mediaSession.setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mediaSession.setCallback(this); // a MediaSessionCompat.Callback
// This is what enables media buttons and should be called
// Immediately after getting audio focus
mediaSession.setActive(true);
The accepted answer doesn't seem to work
I achieved it the other way with ExoPlayer
step #1
added dependencies
implementation 'com.google.android.exoplayer:exoplayer:2.13.1'
implementation 'com.google.android.exoplayer:extension-mediasession:2.13.1'
step #2
mediaSession = MediaSessionCompat(this, "Vion")
val mediaSessionConnector = MediaSessionConnector(mediaSession!!)
mediaSessionConnector.setMediaButtonEventHandler(object: MediaSessionConnector.MediaButtonEventHandler{
override fun onMediaButtonEvent(player: Player, controlDispatcher: ControlDispatcher, mediaButtonEvent: Intent): Boolean {
val event: KeyEvent = mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) ?: return false
if(event.action == KeyEvent.ACTION_UP) {
when(event.keyCode) {
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
}
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
}
KeyEvent.KEYCODE_MEDIA_PLAY -> {
}
KeyEvent.KEYCODE_MEDIA_NEXT -> {
}
}
}
return true
}
})
mediaSessionConnector.setPlayer(exoPlayer)

Detect when user connects his android device to a TV

How can I detect an micro HDMI cable connection in code?
In example, when the user connects his android device to a TV.
Use any of these two or both depending upon requirement :
/**
* Checks device switch files to see if an HDMI device/MHL device is plugged
* in, returning true if so.
*/
private boolean isHdmiSwitchSet() {
// The file '/sys/devices/virtual/switch/hdmi/state' holds an int -- if
// it's 1 then an HDMI device is connected.
// An alternative file to check is '/sys/class/switch/hdmi/state' which
// exists instead on certain devices.
File switchFile = new File("/sys/devices/virtual/switch/hdmi/state");
if (!switchFile.exists()) {
switchFile = new File("/sys/class/switch/hdmi/state");
}
try {
Scanner switchFileScanner = new Scanner(switchFile);
int switchValue = switchFileScanner.nextInt();
switchFileScanner.close();
return switchValue > 0;
} catch (Exception e) {
return false;
}
}
and Broadcastreceiver
public class HdmiListener extends BroadcastReceiver {
private static String HDMIINTENT = "android.intent.action.HDMI_PLUGGED";
#Override
public void onReceive(Context ctxt, Intent receivedIt) {
String action = receivedIt.getAction();
if (action.equals(HDMIINTENT)) {
boolean state = receivedIt.getBooleanExtra("state", false);
if (state == true) {
Log.d("HDMIListner", "BroadcastReceiver.onReceive() : Connected HDMI-TV");
Toast.makeText(ctxt, "HDMI >>", Toast.LENGTH_LONG).show();
} else {
Log.d("HDMIListner", "HDMI >>: Disconnected HDMI-TV");
Toast.makeText(ctxt, "HDMI DisConnected>>", Toast.LENGTH_LONG).show();
}
}
}
}
Declare this receiver in manifest.
<receiver android:name="__com.example.android__.HdmiListener" >
<intent-filter>
<action android:name="android.intent.action.HDMI_PLUGGED" />
</intent-filter>
</receiver>

Categories