Listen to volume buttons in background service?
Asked Answered
A

14

53

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?

Aeri answered 14/4, 2012 at 13:44 Comment(3)
Do you need to listen for the volume button, or for a change in volume?Specimen
Note that this currently won't work on Android 12 (in the background): issuetracker.google.com/issues/201546605Pindling
There is an app that does it all. It is called Volumee. I'm not sure how it works though.Finback
P
12

Judging by the couple of other questions about this topic, no.

Other question 1, Other question 2

Services simply do not receive KeyEvent callbacks.

Potbellied answered 9/7, 2012 at 14:37 Comment(2)
See my answer here to see how the AOSP Music app was able to implement it :)Appalachian
There are ways to receive volume-button events from a service. In fact, there are four different ways! See my answer here for a summary of the different approaches.Nihon
P
34

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()
Perr answered 8/3, 2013 at 10:50 Comment(14)
@ssuukk: Will volume keys be detected when the screen is off? OR does you solution work for just the case when the screen is ON?Basilisk
This code works while the application is in pause state. How to detect the volume change after removing application from running state.?Watersick
This is no longer possible w/ Android Marshmallow, all volume settings have been removed from android.provider.Settings.System. - developer.android.com/sdk/api_diff/23/changes/…Celenacelene
It IS still possible. I'll update my answer in a moment!Perr
Side note: VolumeProviderCompat will only work on API 21(+). And on my Nexus 5X, observing the system settings does actually work, just like on older platforms.Mir
Might be ROM dependent, it does work on all devices I've checked. Have you tried several key presses instead of one long?Perr
Can you provide a code example for the Matshmallow case? How do I get the MediaSession\Controller?Rosner
Where do you initialize mSession in Update 1 of your answer?Burleson
In my Service's onCreate methodPerr
In the last kotlin example, where did you get receiver variable from?Pulchritudinous
Oh, sorry. It's a standard receiver code: <code> val receiver = ComponentName(packageName, MediaButtonIntentReceiver::class.java.name)</code>Perr
where i have to use MediaControllerCompat? @PerrOtic
This doesn't seem to work at all for me, even in an Activity. I don't see kontroler being used. Is it important? Can you please tell me what I'm doing wrong? Here's the project: s000.tinyupload.com/?file_id=99484997837082357593Dimorphism
All you have to do is implement proper behavior in your VolumeProviderCompat and apply it to your MediaSession, see above. Only this part is important.Perr
N
20

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://mcmap.net/q/410018/-listen-to-volume-buttons-in-background-service

Drawbacks:

  1. Requires Android API level 21+ (Android 5.0+).

2) android.media.VOLUME_CHANGED_ACTION (from Service)

Answer by Nikhil: https://mcmap.net/q/412042/-keep-activity-alive-in-background-until-user-doesn-39-t-kill-or-close-app

Drawbacks:

  1. Not an official part of the SDK: https://mcmap.net/q/89634/-how-to-receive-volume-changed-events-for-the-voice-in-call-stream-type
  2. Ignores first-press of volume-key (since it only shows the volume-bar).
  3. Ignores volume-up key when at 100%, and volume-down key when at 0%.

3) ContentObserver (from Service)

Answer by ssuukk: https://mcmap.net/q/410018/-listen-to-volume-buttons-in-background-service (first part)

Drawbacks:

  1. Doesn't work in newer versions of Android: comment by dsemi
  2. Ignores first-press of volume-key (since it only shows the volume-bar).
  3. Ignores volume-up key when at 100%, and volume-down key when at 0%.

4) AudioManager.registerMediaButtonEventReceiver (from Service)

Answer by Joe: https://mcmap.net/q/410018/-listen-to-volume-buttons-in-background-service

Drawbacks:

  1. Doesn't work on most roms: comment by elgui

5) onKeyDown (from Activity)

Answer by dipali: https://mcmap.net/q/412043/-how-can-i-launch-an-android-app-on-upon-pressing-the-volume-up-or-volume-down-button

Drawbacks:

  1. Doesn't work if screen is off, in different app, etc.

6) dispatchKeyEvent (from Activity)

Answer by Maurice Gavin: https://mcmap.net/q/410018/-listen-to-volume-buttons-in-background-service

Drawbacks:

  1. Doesn't work if screen is off, in different app, etc.

Conclusion

The solution I'm currently using is #1, because:

  1. It's an official part of the SDK.
  2. It is usable from a service. (ie. regardless of what app you're in)
  3. It captures every volume-key press, regardless of current-volume/ui-state.
  4. 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!

Nihon answered 6/8, 2019 at 9:20 Comment(2)
#1 drawback: when starting another media session from another app, you loose the listener on volume since your session is not active anymore. If you find a way to keep the current media session active, I'll gladly take it. I have tested all the above solution and can say this is still true to this day..Acid
first solution seems doesn't work on newer AndroidUnderwood
A
19

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?

Appalachian answered 16/7, 2012 at 18:45 Comment(5)
volume buttons are not considered as "media buttons" in stock ROM. I guess this app is able to get volume buttons because it runs with a custom android rom...Waterfall
@Waterfall Could you please provide link to documentation that support your words?Leprosy
well, using stock rom, create a receiver which listen to media buttons, press a volume button and observe that nothing happens...Waterfall
here is the documentation link androidxref.com/4.4.4_r1/xref/frameworks/base/media/java/…Appointive
What about Google Play Music? It is not the stock player, but it also manages media buttons, also when not in foreground...Transatlantic
P
13

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.
Pulchritudinous answered 9/4, 2017 at 8:21 Comment(10)
getting nothing within onAdjustVolume. I am running on nexus 5Ap
If you add this code of line , it does work. : if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mediaSession.setCallback(new MediaSessionCompat.Callback(){ }); }Julee
This solution is pretty good but don't work when the screen is off, any solution? Thank'sOtic
@Otic It works for me when the screen is off, as it did for the answer-writer (Denis). (My phone: Android 9, Mi Mix 3)Nihon
check this answer if your IDE cant recgonize MediaSessionCompat class https://mcmap.net/q/412044/-cannot-resolve-symbol-39-mediasessioncompat-39Done
How to hide default media volume control that is being displayed in this case. I want to trigger custom volume slider and hide default one.Neckline
What if I have a YouTube music app playing something?Underwood
I checked on Android 12 and it doesn't workUnderwood
can i listen to this event on flutter too?Miramirabeau
After refactor to Android 33+ that code not working on Android TiramisuCoastward
P
12

Judging by the couple of other questions about this topic, no.

Other question 1, Other question 2

Services simply do not receive KeyEvent callbacks.

Potbellied answered 9/7, 2012 at 14:37 Comment(2)
See my answer here to see how the AOSP Music app was able to implement it :)Appalachian
There are ways to receive volume-button events from a service. In fact, there are four different ways! See my answer here for a summary of the different approaches.Nihon
B
7

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

Blodgett answered 22/9, 2017 at 6:7 Comment(5)
Note that this solution/action, while it works, is not "officially" part of the SDK. See here: https://mcmap.net/q/89634/-how-to-receive-volume-changed-events-for-the-voice-in-call-stream-typeNihon
this solution works fine, but not working when: Volume Up Pressed && Volume == 100 Volume Down Pressed && Volume == 0 Has more easy resolve for that problemCoastward
@Coastward In some devices, you will not get a callback when you press the volume up button but the volume is already maximum or when you press the volume down button but the volume is already minimum. Only the workaround I know is, not keeping volume maximum or minimum. This means once the maximum volume is reached, decrease the volume by 1 programmatically.Blodgett
@Blodgett it still not clear solution and i don't know why application cant have access to global input hardware button event. Right now i need get a power button event count, but i have to resolve that by SCREEN_OFF and SCREEN_ON BroadcastReceiver.... it is real worst....Coastward
@Coastward Like I said, on some devices, you only get a callback from the volume button when the volume is between minimum and maximum exclusively. It means, when the volume is 0, you won't get a callback when the volume is further decreased and similarly when the volume is 100 (max), you won't get a callback when the volume is further increased. To resolve this, when the volume is reached 100, immediately put it to 99 and when the volume is reached 0, immediately put it to 1. By this, your volume will never be equal to minimum or maximum but you will always get a callback.Blodgett
F
2

@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.

Fiveandten answered 7/4, 2022 at 22:47 Comment(3)
Don't remember ever seeing a comment as useful as yours on Stackoverflow, really awesome. Finally I can get a really important part of my app working on Android 12 again. Although I had to use the Github classes because the ones in this comment had notification issues/exceptions. Anyways, this is really important to my app here: play.google.com/store/apps/details?id=com.me.btp.free so let me know if you want a code for the pro version for freePindling
@MerthanErdem: Thanks for the kind words. I am trying to keep the Stackoverflow version up-to-date whenever I find issues in my own implementation of this class.Fiveandten
It seems long presses cannot be detected when the screen is off. Can anyone confirm?Finback
E
1

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();
        }
    }
}
Eparch answered 10/3, 2018 at 19:38 Comment(0)
I
1

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))
}
Interdigitate answered 17/9, 2022 at 5:50 Comment(1)
That solution require add permission inside settings Accessibility. This is not always the right approach.Coastward
C
1

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+

Coastward answered 18/12, 2022 at 19:11 Comment(1)
what do you mean? it's not gonna workUnderwood
U
0

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

Undersecretary answered 9/7, 2012 at 14:23 Comment(0)
D
0

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);
    }
Darwinism answered 13/7, 2012 at 1:28 Comment(3)
This one working fine from the activity only! Services does not list key events like Activities do.Cushman
this callback is only included in Activity class, not ServiceCatafalque
This is not an answer since the question is about the Service, not ActivityEczema
I
0

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.

Intransigeance answered 1/12, 2016 at 19:19 Comment(0)
T
0

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.

Thorpe answered 22/9, 2022 at 0:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.