Skip to content

Commit

Permalink
Add Headphone controls support.
Browse files Browse the repository at this point in the history
  • Loading branch information
DonnKey committed Dec 20, 2021
1 parent 1061b75 commit 9539eeb
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 40 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,10 @@ Add German translations, thanks to @renarena.
Update to conform to API30 constraints. The main AudioBooks directory
is read-only to Aesop (but not to the file manager or via a PC); Aesop writes into the
Scoped Storage directory ...Android/data/github.io.donnKey.aesopPlayer/files instead.
It will play books found in the main AudioBooks directory, but can't change them.
It will play books found in the main AudioBooks directory, but can't change them.

## Version 1.2.4
Support headphone buttons: Enable start/stop and volume up/down headphone buttons.
Intentionally ignore fast-forward/rewind/next and record buttons as inconsistent with
the "simple and fumble-proof" goals of Aesop.
Unlike most apps, the buttons wake up the UI.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ android {
multiDexEnabled true
minSdkVersion 17
targetSdkVersion 30
versionCode 14
versionCode 15
versionName getVersionName()
}
buildTypes {
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>

<!-- Needed for JellyBean (and below) to handle media buttons -->
<receiver android:name=".ui.MediaKeyReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>

<!--
The provider needs to be exported so that it is available to ADB shell.
It only provides some configuration settings which aren't very sensitive information.
Expand Down
134 changes: 133 additions & 1 deletion app/src/main/java/com/donnKey/aesopPlayer/ui/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,14 @@
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
Expand All @@ -46,6 +50,7 @@

import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.KeyEvent;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
Expand All @@ -72,6 +77,8 @@

import org.greenrobot.eventbus.EventBus;

import static android.media.AudioManager.STREAM_MUSIC;

public class MainActivity extends AppCompatActivity implements SpeakerProvider {

private static final int TTS_CHECK_CODE = 1;
Expand All @@ -86,6 +93,7 @@ public class MainActivity extends AppCompatActivity implements SpeakerProvider {
private static @Nullable
SimpleDeferred<Object> ttsDeferred;
private OrientationActivityDelegate orientationDelegate;
public static final String BROADCAST_BUTTON_PRESSED = "MainActivity.MainActivityButtonPress";

@Inject
public UiControllerMain controller;
Expand All @@ -103,6 +111,8 @@ public class MainActivity extends AppCompatActivity implements SpeakerProvider {
private StatusBarCollapser statusBarCollapser;
private TextView maintenanceMessage;
private TextView newFeaturesMessage;
private AudioManager audioManager;
private ComponentName remoteControlResponder;

// Used for Oreo and up suppression of status bar.
private boolean isPaused = false;
Expand Down Expand Up @@ -174,6 +184,9 @@ protected void onCreate(Bundle savedInstanceState) {
findViewById(R.id.batteryStatusIndicator), EventBus.getDefault());

orientationDelegate = new OrientationActivityDelegate(this, globalSettings);
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
remoteControlResponder = new ComponentName(getPackageName(),
MediaKeyReceiver.class.getName());

maintenanceMessage = findViewById(R.id.maintenance_warning);
newFeaturesMessage = findViewById(R.id.new_version_message);
Expand Down Expand Up @@ -250,6 +263,13 @@ protected void onResume() {

super.onResume();

IntentFilter filter = new IntentFilter();
filter.addAction(BROADCAST_BUTTON_PRESSED);
getApplicationContext().registerReceiver(keyReceiver, filter);

audioManager.registerMediaButtonEventReceiver(
remoteControlResponder);

ColorTheme theme = globalSettings.colorTheme();
if (currentTheme != theme) {
setTheme(theme);
Expand Down Expand Up @@ -429,6 +449,8 @@ public void onWindowFocusChanged(boolean hasFocus) {

@Override
protected void onDestroy() {
audioManager.unregisterMediaButtonEventReceiver(
remoteControlResponder);
restorer.cancelRestore();
batteryStatusIndicator.shutdown();
controller.onActivityDestroy();
Expand Down Expand Up @@ -510,7 +532,7 @@ private void captureLauncher() {
// no launcher recorded... find one
if (defaultResolution == null || defaultResolution.activityInfo.packageName.contains("aesopPlayer")) {
// We don't have a default launcher to refer to... guess
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY);
@SuppressLint("QueryPermissionsNeeded") List<ResolveInfo> resolveInfos = pm.queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY);
ResolveInfo selectedInfo = resolveInfos.get(0);
for (ResolveInfo resolveInfo : resolveInfos) {
if (resolveInfo.activityInfo.packageName.contains("Resolver")) {
Expand Down Expand Up @@ -766,4 +788,114 @@ private void setTheme(@NonNull ColorTheme theme) {
currentTheme = theme;
setTheme(theme.styleId);
}

public boolean keyDown(int keyCode) {
// 'Eat' key presses so that the built-in music player doesn't see them
switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY: // 126
case KeyEvent.KEYCODE_MEDIA_PAUSE: // 127
case KeyEvent.KEYCODE_MEDIA_STOP: // 86
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: // 85
case KeyEvent.KEYCODE_VOLUME_DOWN: // 25
case KeyEvent.KEYCODE_VOLUME_UP: // 24
return true;

// Headset buttons we intentionally ignore for Aesop
case KeyEvent.KEYCODE_MEDIA_NEXT:
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
case KeyEvent.KEYCODE_MEDIA_RECORD:
case KeyEvent.KEYCODE_MEDIA_REWIND:
return true;
}
return false;
}

public boolean keyUp(int keyCode) {
// Note: When the device is asleep (screen off), MediaKeyReceiver will
// catch a button event and wake up the device. To know what the button
// event was, it uses an intent to relay the button press event here.
// (We need to know what the event was to do the right thing.)
// I was unable to find a good (and leak-safe) way to do that more directly.
// When not asleep, the button events come to onKeyUp/Down directly.
// There is a benign (because it's redundant) race, because the time between
// KeyDown and KeyUp is sufficient for the activity to awaken (sometimes),
// so the KeyUp event may arrive by either (or both?) path.
switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY: { // 126
controller.playCurrentAudiobook();
return true;
}
case KeyEvent.KEYCODE_MEDIA_PAUSE: // 127
case KeyEvent.KEYCODE_MEDIA_STOP: { // 86
controller.stopAudiobook();
return true;
}
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: { // 85
controller.togglePlay();
return true;
}
case KeyEvent.KEYCODE_VOLUME_DOWN: { // 25
final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int newTarget = audioManager.getStreamVolume(STREAM_MUSIC);
newTarget--;
if (newTarget < 0) {
newTarget = 0;
}
audioManager.setStreamVolume(STREAM_MUSIC, newTarget, 0);
return true;
}
case KeyEvent.KEYCODE_VOLUME_UP: { // 24
final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int newTarget = audioManager.getStreamVolume(STREAM_MUSIC);
newTarget++;
int max = audioManager.getStreamMaxVolume(STREAM_MUSIC);
if (newTarget > max) {
newTarget = max;
}
audioManager.setStreamVolume(STREAM_MUSIC, newTarget, 0);
return true;
}

// Headset buttons we intentionally ignore for Aesop
case KeyEvent.KEYCODE_MEDIA_NEXT:
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
case KeyEvent.KEYCODE_MEDIA_RECORD:
case KeyEvent.KEYCODE_MEDIA_REWIND:
return true;
}
return false;
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyDown(keyCode)) {
return true;
}
return super.onKeyDown(keyCode, event);
}

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyUp(keyCode)) {
return true;
}
return super.onKeyUp(keyCode, event);
}

private final BroadcastReceiver keyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if(Objects.requireNonNull(intent.getAction()).equals(BROADCAST_BUTTON_PRESSED)) {
KeyEvent event = (KeyEvent) Objects.requireNonNull(intent.getExtras()).get(Intent.EXTRA_KEY_EVENT);
assert event != null;
int keyCode = event.getKeyCode();
if (event.getAction() == KeyEvent.ACTION_DOWN) {
keyDown(keyCode);
}
else if (event.getAction() == KeyEvent.ACTION_UP) {
keyUp(keyCode);
}
}
}
};
}
68 changes: 68 additions & 0 deletions app/src/main/java/com/donnKey/aesopPlayer/ui/MediaKeyReceiver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2021 Donn S. Terry
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.donnKey.aesopPlayer.ui;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.PowerManager;
import android.view.KeyEvent;

import java.util.Objects;

import static android.content.Context.POWER_SERVICE;

@ActivityScope
public class MediaKeyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
KeyEvent event = (KeyEvent) Objects.requireNonNull(intent.getExtras()).get(Intent.EXTRA_KEY_EVENT);

PowerManager powerManager = (PowerManager) context.getSystemService(POWER_SERVICE);
// After a lot of research, I was unable to find a way to achieve what
// this does without the deprecation warning. There are other ways that
// might work (setting android:turnScreenOn or the equivalent call), but
// that requires API 27.
//noinspection deprecation - SCREEN_BRIGHT_WAKE_LOCK
PowerManager.WakeLock wl = powerManager.newWakeLock(
PowerManager.SCREEN_BRIGHT_WAKE_LOCK |
PowerManager.ACQUIRE_CAUSES_WAKEUP |
PowerManager.ON_AFTER_RELEASE,
"AesopPlayer:Key:reawaken");

// Screen will stay on 5 seconds if not touched
// and revert to normal timeouts if touched.
wl.acquire(5000);

new Handler().postDelayed(() -> {
// Tell the controller WHICH button was pressed; give it a chance to run first
Intent nextIntent = new Intent(MainActivity.BROADCAST_BUTTON_PRESSED);
nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
context.sendBroadcast(nextIntent);
}, 0);
}
}
}
Loading

0 comments on commit 9539eeb

Please sign in to comment.