Is it possible to create an Android Service that listens for hardware key presses?

As far as I know KeyEvents can only be handled by Activities as they are the interface to the user pressing the keys. Services run in the background and are not intended to react on user input. That's also the reason of your compiler warning "onKeyDown is undefined for the type Service". Service or any of it's Superclasses don't implement the KeyEvent.Callback interface. As a workaround you could register an Activity in your AndroidManifest.xml to react on certain system notifications such as android.intent.action.SCREEN_ON. When the power button is pressed to turn on the screen your activity could be started, initializing a service of some kind and go back to the background. If that's what you intend to do. See Intent docs for possible Actions.

Hope that helped...


This requires Lollipop (v5.0/API 21) or higher, and can only detect volume keys

It will override volume key action, so using it globally may not be desired.

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

KeyEvents require an Activity to be triggered. Hence, hardware key presses can't be detected via Services,as Services do not have a host activity. You could ask for a SystemOverlay, and create a transparent Activity. But this approach doesn't seem to work on API 26+ devices.

A workaround for this would be set up an observer through AccessibilityServices. This allows you to globally detect hardware key presses.

Note: Enabling an app as an Accessibility App can lead to major security issues, and users would be wary of enabling this. So this would be advisable in situations where your application can be "transparent" to the user with regard to the data your app will be handling. This method works for all API 21+ devices, I haven't tested on devices below this, so it may or may not work.

Steps:

  • Create an XML file, with the following options

      <accessibility-service
        android:accessibilityFlags="flagRequestFilterKeyEvents"
        android:accessibilityEventTypes="typeAllMask"
        android:accessibilityFeedbackType="feedbackAllMask"
        android:notificationTimeout="100"
        android:canRetrieveWindowContent="true"
        android:settingsActivity=""
        android:packageNames="yourpackagename"
        android:canRequestFilterKeyEvents="true"
      />
    
  • Define your AccessibilityService in the Manifest

    <service android:name=".Services.AccessibilityKeyDetector"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>
            <meta-data android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_layout" />
    </service>
    
  • Create an AccessibilityService Class

public class AccessibilityKeyDetector extends AccessibilityService {

    private final String TAG = "AccessKeyDetector";

    @Override
    public boolean onKeyEvent(KeyEvent event) {
        Log.d(TAG,"Key pressed via accessibility is: "+event.getKeyCode());
        //This allows the key pressed to function normally after it has been used by your app.
        return super.onKeyEvent(event);
    }


    @Override
    protected void onServiceConnected() {
        Log.i(TAG,"Service connected");

    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {

    }


    @Override
    public void onInterrupt() {

    }
}

  • Create a MainActivity, that handles the permission request for this.
public class MainActivity extends AppCompatActivity {

    private final String TAG = "Test";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        checkAccessibilityPermission();
    }

    public boolean checkAccessibilityPermission() {
        int accessEnabled=0;
        try {
            accessEnabled = Settings.Secure.getInt(this.getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED);
        } catch (Settings.SettingNotFoundException e) {
            e.printStackTrace();
        }
        if (accessEnabled==0) {
            /** if not construct intent to request permission */
            Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            /** request permission via start activity for result */
            startActivity(intent);
            return false;
        } else {
            return true;
        }
    }


    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        Log.d(TAG,"Key pressed");

//this prevents the key from performing the base function. Replace with super.onKeyDown to let it perform it's original function, after being consumed by your app.
        return true;

    }
}