在后台服务中监听音量按钮?

53

我知道如何在一个Activity中监听音量按钮。但是我能在后台服务中做到吗?如果可以,应该怎么做?


你需要监听音量按钮还是音量的变化? - Stobor
1
请注意,目前此功能在 Android 12(后台)上无法正常工作:https://issuetracker.google.com/issues/201546605 - Merthan Erdem
有一个应用程序可以做到所有的事情。它叫做Volumee。不过我不太确定它是如何工作的。 - Robin Dijkhof
14个回答

34

这是可能的。使用下面的代码(对于较新的Android版本,特别是Marshmallow,请参见答案底部):

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;
        }
    }
}

然后在你的服务 onCreate 方法中注册它:

mSettingsContentObserver = new SettingsContentObserver(this,new Handler());
getApplicationContext().getContentResolver().registerContentObserver(android.provider.Settings.System.CONTENT_URI, true, mSettingsContentObserver );

然后在 onDestroy 中取消注册:

getApplicationContext().getContentResolver().unregisterContentObserver(mSettingsContentObserver);

请注意,此示例是根据媒体音量的更改进行判断的,如果要使用其他音量,请更改它!
更新:
上述方法据说在 Marshmallow上不起作用,但是现在有更好的方法,因为 MediaSession 已经被介绍了!因此,首先您需要将代码迁移到MediaController/MediaSession模式,然后使用此代码:
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);

屏幕关闭时仍然可以检测到音量按钮按下(只需确保针对您的平台注册适当的媒体按钮意图接收器!)

更新2:由于GalDude请求有关获取MediaSession/MediaController的更多信息,抱歉,但由于我停止使用Java,它将用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()

3
当屏幕关闭时,音量键是否会被检测到?或者你的解决方案仅适用于屏幕开启时的情况? - Basher51
1
这段代码在应用程序处于暂停状态时有效。如何在将应用程序从运行状态中移除后检测音量变化? - Karthick
1
这在安卓6.0(即Marshmallow)中已不再可能,所有音量设置都已从android.provider.Settings.System中删除。- http://developer.android.com/sdk/api_diff/23/changes/android.provider.Settings.System.html - dsemi
1
补充说明:VolumeProviderCompat 仅适用于API 21及以上版本。在我的Nexus 5X上,观察系统设置确实起作用,就像在旧平台上一样。 - MH.
在最后一个 Kotlin 示例中,你从哪里获得了 receiver 变量? - Denys Kniazhev-Support Ukraine
显示剩余9条评论

20

很遗憾,这是 Android 中另一个有五种不同方法“解决问题”的领域之一,但它们中的大多数效果都不太好。为了保持自己的理智,我将尝试列出下面所有不同的方法。

解决方案

1)MediaSession(来自 Service)

由 Denis Kniazhev 提供答案: https://dev59.com/o2kw5IYBdhLWcg3wE2dA#43304591

缺点:

  1. 需要 Android API 级别 21+ (Android 5.0+)。

2)android.media.VOLUME_CHANGED_ACTION(来自 Service)

由 Nikhil 提供答案: https://dev59.com/AaHia4cB1Zd3GeqPZtez#44040282

缺点:

  1. 不是 SDK 的官方部分:https://dev59.com/AV_Va4cB1Zd3GeqPUpbW#8974510
  2. 忽略音量键的第一次按下(因为它只显示音量条)。
  3. 当音量为 100% 时忽略音量加键,当音量为 0% 时忽略音量减键。

3)ContentObserver(来自 Service)

由ssuukk回答: https://dev59.com/o2kw5IYBdhLWcg3wE2dA#15292255 (第一部分)

缺点:

  1. 在较新版本的Android中不起作用:dsemi的评论
  2. 忽略音量键的第一次按下(因为它只显示音量条)。
  3. 当音量为100%时,会忽略音量增加键,当音量为0%时,会忽略音量减小键。

4)AudioManager.registerMediaButtonEventReceiver(来自服务)

由Joe回答:https://dev59.com/o2kw5IYBdhLWcg3wE2dA#11510564

缺点:

  1. 在大多数rom上不起作用:elgui的评论

5)onKeyDown(来自Activity)

由dipali回答:https://dev59.com/QGEi5IYBdhLWcg3wpdnJ#21086563

缺点:

  1. 如果屏幕关闭、在其他应用程序中等,则无法工作。

6) dispatchKeyEvent(来自Activity)

Maurice Gavin的答案:https://dev59.com/o2kw5IYBdhLWcg3wE2dA#11462962

缺点:

  1. 如果屏幕关闭、在其他应用程序中等,则无法工作。

结论

我目前正在使用的解决方案是#1,因为:

  1. 它是SDK的官方部分。
  2. 可以从服务中使用它。(即无论您在哪个应用程序中)
  3. 它捕获每个音量键按下,而不考虑当前音量/界面状态。
  4. 当屏幕关闭时它能够工作。

如果您找到任何其他解决方案,请告诉我-或者如果您发现某些解决方案的更多缺点!


#1 缺点:当从另一个应用程序开始另一个媒体会话时,您会失去音量侦听器,因为您的会话不再处于活动状态。如果您找到一种方法来保持当前媒体会话处于活动状态,我很乐意采纳。我已经测试了所有以上解决方案,并且可以说这仍然是到今天为止的真实情况。 - Pomme De Terre
1
第一种解决方案似乎在较新的Android上不起作用。 - user924

19

AOSP音乐应用具有服务(MediaPlaybackService),通过注册BroadcastReceiver来响应音量键事件(MediaButtonIntentReceiver)。

以下代码片段显示了注册接收器的位置:

    mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    ComponentName rec = new ComponentName(getPackageName(),
            MediaButtonIntentReceiver.class.getName());
    mAudioManager.registerMediaButtonEventReceiver(rec);

另外,不要忘记清单文件:

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

即使在音乐应用程序不在前台时,此功能也可以正常工作。这不是你想要的吗?

3
在原生ROM中,音量按钮不被视为“媒体按钮”。我猜测这个应用程序之所以可以使用音量按钮,是因为它在一个自定义的Android ROM上运行... - elgui
@elgui,您能否提供支持您说法的文档链接? - CAMOBAP
好的,使用原生固件来创建一个接收器,监听媒体按键,按下音量按钮并观察是否没有任何反应... - elgui
这是文档链接:http://androidxref.com/4.4.4_r1/xref/frameworks/base/media/java/android/media/AudioManager.java#2118 - Goran Horia Mihail
谷歌播放音乐怎么样?它不是原始播放器,但它也可以管理媒体按钮,即使不在前台... - Massimo

13

我能够使用MediaSession在安卓5+设备上使其正常工作。然而,@ssuukk建议的ContentObserver在我测试的4.4和7.0设备上都不起作用(至少在ROM上是这样的)。

以下是一个完整的示例,可在安卓5+上运行。

服务:

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();
    }
}

AndroidManifest.xml 文件中:

<application ...>
    ...
    <service android:name=".PlayerService"/>
</application>
在你的活动中:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    startService(new Intent(this, PlayerService.class));
}

需要注意以下几点:

  • 它会完全拦截音量键,所以在代码运行时,您将无法使用音量键调整铃声音量。这可能可以修复,我只是没有尝试过。
  • 如果按照原样运行示例,则即使屏幕关闭并且应用已从“最近使用的应用程序”列表中删除,音量按钮仍将受应用控制。您需要进入设置->应用,找到该应用并强制停止才能恢复音量按钮。

2
如果您添加了这行代码,它就会起作用:`if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mediaSession.setCallback(new MediaSessionCompat.Callback(){ }); }` - VasFou
这个解决方案相当不错,但当屏幕关闭时无法工作,有什么解决办法吗?谢谢。 - Daniele
1
如果你的IDE无法识别MediaSessionCompat类,请查看此答案 https://dev59.com/oFQK5IYBdhLWcg3wVuis#52019860 - Gilian Marques
如何隐藏在此情况下显示的默认媒体音量控制。我想触发自定义音量滑块并隐藏默认的控制器。 - Zen Bhatt
我在 Android 12 上进行了检查,但它不起作用。 - user924
显示剩余5条评论

12

从其他关于此主题的问题来看,答案是否定的。

其他问题1其他问题2

服务仅仅不能接收KeyEvent回调。


9
请看我的回答,了解AOSP音乐应用是如何实现它的 :) - Joe
有多种方法可以从服务接收音量按钮事件。实际上,有四种不同的方式!请参见我的答案以获取不同方法的摘要。 - Venryx

7
你需要从服务中播放空白音频才能听到音量变化。以下方法适用于我:
步骤
1. 将 blank.mp3 放入 raw 文件夹中(从 这里 下载)。
2. 在 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. 你必须选择停止并释放mediaplayer。最好在onDestroy()中这样做。

@Override
public void onDestroy() {

    mediaPlayer.stop();
    mediaPlayer.release();

    super.onDestroy();
}

4. 创建广播接收器以侦听音量更改

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. 在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. 在onDestroy()中取消注册广播接收器

 @Override
public void onDestroy() {
    .....

    unregisterReceiver(broadcastReceiver);

    .....

    super.onDestroy();
}

就这些


1
请注意,此解决方案/操作虽然可行,但并非SDK的“官方”部分。请参见此处:https://dev59.com/AV_Va4cB1Zd3GeqPUpbW#8974510 - Venryx
这个解决方案运行良好,但在以下情况下无法工作: 当音量为100时按下音量增加键 当音量为0时按下音量减小键有更简单的解决方法。 - Aios
@Aios 在某些设备上,当您按下音量增加按钮但音量已经最大或者当您按下音量减小按钮但音量已经最小时,您将不会收到回调。我所知道的唯一解决方法是不要保持音量最大或最小。这意味着一旦达到最大音量,通过编程方式将音量降低1。 - bikram
@bikram,这个解决方案还不够清晰,我不知道为什么应用程序无法访问全局输入硬件按钮事件。现在我需要获取电源按钮事件计数,但我必须通过SCREEN_OFF和SCREEN_ON BroadcastReceiver来解决它... 这真的很糟糕... - Aios
像我之前所说的,在某些设备上,当音量处于最小值和最大值之间时,您只会从音量按钮获得回调。这意味着,当音量为0时,当音量进一步降低时,您不会收到回调;同样地,当音量为100(最大值)时,当音量进一步增加时,您也不会收到回调。要解决此问题,请在音量达到100时立即将其设置为99,并在音量达到0时立即将其设置为1。这样,您的音量将永远不会等于最小值或最大值,但您仍然可以始终获得回调。 - bikram

2

@venryx: 解决方案1在Android 12中不再适用
@ssuukk: 我可以确认@venryx的评论,如果音量已经达到最小或最大值,则SettingsContentObserver不会被触发。
@bikram: 我创建了一个VolumeButtonHelper类,使用了这种方法。虽然它使用了一个未记录的SDK功能,但它仍然在2022年起作用。我对这个主题进行了广泛的研究,这是我能找到的唯一解决方案。

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

        }
      }
    }
  }
}

在一个服务中实例化该类(使用适当的唤醒锁):

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()

  }
}

从您的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()

  }
}

我已经在github.com/oliverClimbs/VolumeButtonDemo上分享了完整的项目。


我从未在Stackoverflow上看到像你这样有用的评论,真的很棒。终于我可以在Android 12上让我的应用程序的一个非常重要的部分再次工作了。虽然我不得不使用Github类,因为这个评论中的那些类存在通知问题/异常。无论如何,这对我的应用程序非常重要:play.google.com/store/apps/details?id=com.me.btp.free,所以如果你想要免费的专业版代码,请告诉我。 - Merthan Erdem
1
@MerthanErdem:感谢你的赞美之词。我正在尽力保持Stackoverflow版本的最新状态,每当我在自己实现这个类时发现问题时。 - Oliver
当屏幕关闭时,似乎无法检测到长按操作。有人能确认一下吗? - Robin Dijkhof

1

这需要Lollipop(v5.0 / API 21)或更高版本

我的目标是从服务中调整系统音量。但是,任何操作都可以在按下时执行。

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();
        }
    }
}

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));
}

那个解决方案有点类似于Answer

但是适用于Android 33及以上版本


1

对于我来说,辅助功能服务只能按预期工作

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))
}

该解决方案需要在“辅助功能”设置中添加权限。这并不总是正确的方法。 - Aios

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接